Initial commit of RSA CLI tool, still very much WIP

This commit is contained in:
Laurence Withers 2023-03-01 21:30:16 +00:00
commit 5ac63352a7
12 changed files with 827 additions and 0 deletions

113
cmd/ca/ca.go Normal file
View 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
View 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
View 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
View 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)
}
}
}