diff --git a/internal/clis/hmpki/generate.go b/internal/clis/hmpki/generate.go index 9bf9d8e..6bd94ef 100644 --- a/internal/clis/hmpki/generate.go +++ b/internal/clis/hmpki/generate.go @@ -8,9 +8,13 @@ import ( var Name string var OutDir string -// Needed by all subcommands that generate certificates. +// Needed by subcommands that generate certificates. var CommonName string +// Needed by subcommands that generate leaf certificates. +var CAFile string +var CAKeyFile string + func init() { rootCmd.AddCommand(generateCmd) @@ -31,7 +35,6 @@ func init() { ".", "output directory for artifacts produced by this command, defaults to the current directory", ) - generateCmd.MarkPersistentFlagRequired("output-dir") generateCmd.MarkPersistentFlagDirname("output-dir") } diff --git a/internal/clis/hmpki/generate_client_cert.go b/internal/clis/hmpki/generate_client_cert.go new file mode 100644 index 0000000..51436ec --- /dev/null +++ b/internal/clis/hmpki/generate_client_cert.go @@ -0,0 +1,104 @@ +package hmpki + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "path" + "time" + + "git.handmadecities.com/HMC/shhh/pkg/pki" + "github.com/spf13/cobra" +) + +func init() { + generateCmd.AddCommand(generateClientCertCmd) + + generateClientCertCmd.Flags().StringVar( + &CAFile, + "ca", + "", + "File for the CA that will sign the leaf", + ) + generateClientCertCmd.MarkFlagRequired("ca") + generateClientCertCmd.MarkFlagFilename("ca") + + generateClientCertCmd.Flags().StringVar( + &CAKeyFile, + "ca-key", + "", + "File for the CA private key that will sign the leaf", + ) + generateClientCertCmd.MarkFlagRequired("ca-key") + generateClientCertCmd.MarkFlagFilename("ca-key") + + generateClientCertCmd.Flags().StringVar( + &CommonName, + "common-name", + "", + "Common Name for the CA", + ) + generateClientCertCmd.MarkFlagRequired("common-name") +} + +var generateClientCertCmd = &cobra.Command{ + Use: "client-cert", + Short: "Generate a leaf client certificate and private key", + RunE: func(cmd *cobra.Command, args []string) error { + ca, err := pki.ParseCertFromFile(CAFile) + if err != nil { + return err + } + caKey, err := pki.ParseKeyFromFile(CAKeyFile) + if err != nil { + return err + } + + certKey := pki.NewKey() + caTmpl := &x509.Certificate{ + SerialNumber: pki.NewCertSerialNumber(), + Subject: pkix.Name{ + CommonName: CommonName, + }, + // Leaf certs valid for 1 year. + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + // This cert is a CA. + IsCA: false, + // Disambiguate `IsCA` + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, + } + + caBytes, err := x509.CreateCertificate( + rand.Reader, + caTmpl, + // CA is the parent. + ca, + // The cert embeds its own public key. + &certKey.PublicKey, + // The CA's private key is the one signing the leaf cert. + caKey, + ) + caPEMBytes := pem.EncodeToMemory(&pem.Block{ + Type: pki.PEMTypeCertificate, + Bytes: caBytes, + }) + + // Write cert to disk. + cafn := path.Join(OutDir, Name+".crt") + err = createFileWith(cafn, caPEMBytes, pki.CertFilePerms) + if err != nil { + return err + } + // Write cert private key to disk. + keyfn := path.Join(OutDir, Name+".key") + err = createFileWith(keyfn, pki.MarshalKeyToPEM(caKey), pki.PrivateKeyFilePerms) + if err != nil { + return err + } + return nil + }, +} diff --git a/internal/clis/hmpki/generate_server_cert.go b/internal/clis/hmpki/generate_server_cert.go new file mode 100644 index 0000000..d1e2338 --- /dev/null +++ b/internal/clis/hmpki/generate_server_cert.go @@ -0,0 +1,135 @@ +package hmpki + +import ( + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "net" + "path" + "time" + + "git.handmadecities.com/HMC/shhh/pkg/pki" + "github.com/spf13/cobra" +) + +var StringIPs []string +var DNSNames []string + +func init() { + generateCmd.AddCommand(generateServerCertCmd) + + generateServerCertCmd.Flags().StringVar( + &CAFile, + "ca", + "", + "File for the CA that will sign the leaf", + ) + generateServerCertCmd.MarkFlagRequired("ca") + generateServerCertCmd.MarkFlagFilename("ca") + + generateServerCertCmd.Flags().StringVar( + &CAKeyFile, + "ca-key", + "", + "File for the CA private key that will sign the leaf", + ) + generateServerCertCmd.MarkFlagRequired("ca-key") + generateServerCertCmd.MarkFlagFilename("ca-key") + + generateServerCertCmd.Flags().StringVar( + &CommonName, + "common-name", + "", + "Common Name for the CA", + ) + generateServerCertCmd.MarkFlagRequired("common-name") + + generateServerCertCmd.Flags().StringArrayVar( + &StringIPs, + "ips", + nil, + "IP Addresses that the cert will bind", + ) + // TODO: Validate them to make sure they're valid. + generateServerCertCmd.Flags().StringArrayVar( + &DNSNames, + "dns-names", + nil, + "DNS names that the cert will bind", + ) + // At least one IP or DNS name should be provided. + generateServerCertCmd.MarkFlagsOneRequired("ips", "dns-names") +} + +var generateServerCertCmd = &cobra.Command{ + Use: "server-cert", + Short: "Generate a leaf server certificate and private key", + RunE: func(cmd *cobra.Command, args []string) error { + ca, err := pki.ParseCertFromFile(CAFile) + if err != nil { + return err + } + caKey, err := pki.ParseKeyFromFile(CAKeyFile) + if err != nil { + return err + } + var ips = make([]net.IP, 0, len(StringIPs)) + for _, sip := range StringIPs { + ip := net.ParseIP(sip) + if ip == nil { + return fmt.Errorf("ip %s is invalid", sip) + } + ips = append(ips, ip) + } + + certKey := pki.NewKey() + caTmpl := &x509.Certificate{ + SerialNumber: pki.NewCertSerialNumber(), + Subject: pkix.Name{ + CommonName: CommonName, + }, + DNSNames: DNSNames, + IPAddresses: ips, + // Leaf certs valid for 1 year. + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(1, 0, 0), + // This cert is a CA. + IsCA: false, + // Disambiguate `IsCA` + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + + caBytes, err := x509.CreateCertificate( + rand.Reader, + caTmpl, + // CA is the parent. + ca, + // The cert embeds its own public key. + &certKey.PublicKey, + // The CA's private key is the one signing the leaf cert. + caKey, + ) + caPEMBytes := pem.EncodeToMemory(&pem.Block{ + Type: pki.PEMTypeCertificate, + Bytes: caBytes, + }) + + // Write cert to disk. + cafn := path.Join(OutDir, Name+".crt") + err = createFileWith(cafn, caPEMBytes, pki.CertFilePerms) + if err != nil { + return err + } + // Write cert private key to disk. + keyfn := path.Join(OutDir, Name+".key") + err = createFileWith(keyfn, pki.MarshalKeyToPEM(caKey), pki.PrivateKeyFilePerms) + if err != nil { + return err + } + return nil + }, +}