Initial commit of RSA CLI tool, still very much WIP
This commit is contained in:
parent
6b6866077c
commit
5ac63352a7
12 changed files with 827 additions and 0 deletions
113
cmd/ca/ca.go
Normal file
113
cmd/ca/ca.go
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
package ca
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var (
|
||||
dir string
|
||||
outputFilename string
|
||||
|
||||
// signCommonFlags refers to these
|
||||
validFrom string
|
||||
validUntil string
|
||||
|
||||
// createCACommonFlags refers to these
|
||||
bits int
|
||||
excludedDomains []string
|
||||
excludedIPs []string
|
||||
maxPathLen int
|
||||
permittedDomains []string
|
||||
permittedIPs []string
|
||||
)
|
||||
|
||||
// Register the "keygen" subcommand.
|
||||
func Register(root *cobra.Command) {
|
||||
caCmd := &cobra.Command{
|
||||
Use: "ca",
|
||||
Short: "Certificate authority creation and operation",
|
||||
}
|
||||
caCmd.PersistentFlags().StringVarP(&dir, "dir", "d", "", "Directory holding CA")
|
||||
caCmd.MarkPersistentFlagRequired("dir")
|
||||
|
||||
root.AddCommand(caCmd)
|
||||
|
||||
// init command
|
||||
cmd := &cobra.Command{
|
||||
Use: "init <description>",
|
||||
Short: "Initialise a brand-new certificate authority",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: Init,
|
||||
}
|
||||
createCACommonFlags(cmd)
|
||||
signCommonFlags(cmd)
|
||||
caCmd.AddCommand(cmd)
|
||||
|
||||
// sign command
|
||||
cmd = &cobra.Command{
|
||||
Use: "sign csr.pem",
|
||||
Short: "Sign one or more CSRs",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: Sign,
|
||||
}
|
||||
cmd.Flags().StringVarP(&outputFilename, "output", "o", "", "Name of output file (default stdout).")
|
||||
signCommonFlags(cmd)
|
||||
caCmd.AddCommand(cmd)
|
||||
|
||||
// intermediate command
|
||||
cmd = &cobra.Command{
|
||||
Use: "intermediate <dir> <description>",
|
||||
Short: "Using an existing CA, create a new intermediate CA",
|
||||
Args: cobra.ExactArgs(2),
|
||||
Run: Intermediate,
|
||||
}
|
||||
createCACommonFlags(cmd)
|
||||
signCommonFlags(cmd)
|
||||
caCmd.AddCommand(cmd)
|
||||
}
|
||||
|
||||
func createCACommonFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().IntVarP(&bits, "bits", "b", 3072, "Key size in bits")
|
||||
cmd.Flags().StringSliceVar(&excludedDomains, "exclude-domain", nil,
|
||||
"Do not allow certs to be issued for named domain. Multiple may be specified")
|
||||
cmd.Flags().StringSliceVar(&excludedIPs, "exclude-cidr", nil,
|
||||
"Do not allow certs to be issued for given IP range. Multiple may be specified")
|
||||
cmd.Flags().IntVar(&maxPathLen, "max-path-len", -1,
|
||||
"Maximum path length (whether any further CAs may be issued)")
|
||||
cmd.Flags().StringSliceVar(&permittedDomains, "permit-domain", nil,
|
||||
"Allow certs to be issued only for named domain. Multiple may be specified")
|
||||
cmd.Flags().StringSliceVar(&permittedIPs, "permit-cidr", nil,
|
||||
"Allow certs to be issued only for given IP range. Multiple may be specified")
|
||||
}
|
||||
|
||||
func signCommonFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().StringVar(&validFrom, "valid-from", "", "RFC3339-format timestamp for start of cert validity")
|
||||
cmd.Flags().StringVar(&validUntil, "valid-until", "", "RFC3339-format timestamp for end of cert validity")
|
||||
}
|
||||
|
||||
func signingDates(defaultDuration time.Duration) (from, until time.Time) {
|
||||
var err error
|
||||
if validFrom == "" {
|
||||
from = time.Now().Add(-2 * time.Hour)
|
||||
} else {
|
||||
from, err = time.Parse(time.RFC3339, validFrom)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "--valid-from %s: not a valid RFC3339-format timestamp\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
if validUntil == "" {
|
||||
until = time.Now().Add(defaultDuration)
|
||||
} else {
|
||||
until, err = time.Parse(time.RFC3339, validUntil)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "--valid-until %s: not a valid RFC3339-format timestamp\n")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
86
cmd/ca/init.go
Normal file
86
cmd/ca/init.go
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
package ca
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"src.lwithers.me.uk/go/rsa/pkg/ca"
|
||||
)
|
||||
|
||||
// Init a new certificate authority from scratch.
|
||||
func Init(cmd *cobra.Command, args []string) {
|
||||
desc := args[0]
|
||||
|
||||
template := createCATemplate(desc)
|
||||
key, err := rsa.GenerateKey(rand.Reader, bits)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to generate new key (%d bits): %v\n", bits, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = ca.Create(dir, template, key)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to initialise new certificate authority: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func createCATemplate(desc string) *x509.Certificate {
|
||||
from, until := signingDates(30 * 365 * 24 * time.Hour)
|
||||
|
||||
hasTargetRules := len(permittedDomains) > 0 || len(excludedDomains) > 0
|
||||
var permittedIP, excludedIP []*net.IPNet
|
||||
if len(permittedIPs) > 0 {
|
||||
hasTargetRules = true
|
||||
for _, ipr := range permittedIPs {
|
||||
permittedIP = append(permittedIP, parseCIDR(ipr))
|
||||
}
|
||||
}
|
||||
if len(excludedIPs) > 0 {
|
||||
hasTargetRules = true
|
||||
for _, ipr := range permittedIPs {
|
||||
excludedIP = append(excludedIP, parseCIDR(ipr))
|
||||
}
|
||||
}
|
||||
|
||||
return &x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
CommonName: desc,
|
||||
},
|
||||
NotBefore: from,
|
||||
NotAfter: until,
|
||||
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
MaxPathLen: maxPathLen,
|
||||
MaxPathLenZero: (maxPathLen == 0),
|
||||
|
||||
PermittedDNSDomains: permittedDomains,
|
||||
ExcludedDNSDomains: excludedDomains,
|
||||
PermittedIPRanges: permittedIP,
|
||||
ExcludedIPRanges: excludedIP,
|
||||
PermittedDNSDomainsCritical: hasTargetRules,
|
||||
}
|
||||
}
|
||||
|
||||
func parseCIDR(s string) *net.IPNet {
|
||||
ip, ipnet, err := net.ParseCIDR(s)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if !ip.Equal(ipnet.IP) {
|
||||
fmt.Fprintf(os.Stderr, "%s: invalid IP range (did you mean %s?)\n", s, ipnet.String())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
return ipnet
|
||||
}
|
||||
37
cmd/ca/intermediate.go
Normal file
37
cmd/ca/intermediate.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package ca
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"src.lwithers.me.uk/go/rsa/pkg/ca"
|
||||
)
|
||||
|
||||
// Intermediate uses an existing CA to create a new intermediate CA in a new
|
||||
// directory.
|
||||
func Intermediate(cmd *cobra.Command, args []string) {
|
||||
newCADir := args[0]
|
||||
desc := args[1]
|
||||
|
||||
ca, err := ca.Open(dir)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
template := createCATemplate(desc)
|
||||
key, err := rsa.GenerateKey(rand.Reader, bits)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to generate new key (%d bits): %v\n", bits, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
_, err = ca.CreateIntermediate(newCADir, template, key)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to initialise new intermediate CA: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
87
cmd/ca/sign.go
Normal file
87
cmd/ca/sign.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
package ca
|
||||
|
||||
import (
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"src.lwithers.me.uk/go/rsa/pkg/ca"
|
||||
"src.lwithers.me.uk/go/rsa/pkg/pemfile"
|
||||
)
|
||||
|
||||
// Sign one or more CSRs.
|
||||
func Sign(cmd *cobra.Command, args []string) {
|
||||
csrFilename := args[0]
|
||||
from, until := signingDates(365 * 24 * time.Hour)
|
||||
|
||||
// open the CA
|
||||
ca, err := ca.Open(dir)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// parse the CSR
|
||||
csr, err := pemfile.ReadCSR(csrFilename)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
pubKey, ok := csr.PublicKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
fmt.Fprintf(os.Stderr, "%s: public key is not RSA\n", csrFilename)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// build the certificate template
|
||||
var (
|
||||
keyUsage x509.KeyUsage
|
||||
extKeyUsage []x509.ExtKeyUsage
|
||||
)
|
||||
switch {
|
||||
// TODO: CLI flags
|
||||
case len(csr.DNSNames) == 0 && len(csr.IPAddresses) == 0:
|
||||
extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageClientAuth)
|
||||
default:
|
||||
extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageServerAuth)
|
||||
}
|
||||
|
||||
template := &x509.Certificate{
|
||||
KeyUsage: keyUsage,
|
||||
ExtKeyUsage: extKeyUsage,
|
||||
Subject: csr.Subject,
|
||||
NotBefore: from,
|
||||
NotAfter: until,
|
||||
DNSNames: csr.DNSNames,
|
||||
IPAddresses: csr.IPAddresses,
|
||||
}
|
||||
|
||||
// sign the certificate
|
||||
cert, auditDir, err := ca.Sign(template, pubKey)
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
raw := pem.EncodeToMemory(&pem.Block{
|
||||
Type: pemfile.TypeX509Certificate,
|
||||
Bytes: cert.Raw,
|
||||
})
|
||||
|
||||
// TODO: copy CSR to audit dir
|
||||
_ = auditDir
|
||||
|
||||
// write CSR to output file
|
||||
switch outputFilename {
|
||||
case "", "-":
|
||||
os.Stdout.Write(raw)
|
||||
default:
|
||||
if err := os.WriteFile(outputFilename, raw, 0600); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue