diff --git a/internal/clis/hmpki/generate.go b/internal/clis/hmpki/generate.go index 4fc8a32..9cbc685 100644 --- a/internal/clis/hmpki/generate.go +++ b/internal/clis/hmpki/generate.go @@ -4,8 +4,12 @@ import ( "github.com/spf13/cobra" ) +// Needed by all subcommands. var Output string +// Needed by some subcommands. +var KeyFile string + func init() { rootCmd.AddCommand(generateCmd) diff --git a/internal/clis/hmpki/generate_ca.go b/internal/clis/hmpki/generate_ca.go new file mode 100644 index 0000000..58f8275 --- /dev/null +++ b/internal/clis/hmpki/generate_ca.go @@ -0,0 +1,107 @@ +package hmpki + +import ( + "crypto/ecdsa" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "math" + "math/big" + "os" + "time" + + "git.handmadecities.com/HMC/shhh/pkg/pki" + "github.com/spf13/cobra" +) + +func init() { + generateCmd.AddCommand(generateCACmd) + + generateCACmd.Flags().StringVar( + &KeyFile, + "key-file", + "", + "File that contains the private key that will be associated to the CA", + ) + generateCACmd.MarkFlagRequired("key-file") + generateCACmd.MarkFlagFilename("key-file") +} + +var generateCACmd = &cobra.Command{ + Use: "ca", + Short: "Generate a Certificate Authority", + RunE: func(cmd *cobra.Command, args []string) error { + // Read key from KeyFile. + keyPemBytes, err := os.ReadFile(KeyFile) + if err != nil { + return err + } + privBlock, _ := pem.Decode(keyPemBytes) + if privBlock == nil || privBlock.Type != pki.PEMTypePrivateKey { + return errors.New("Invalid pem key file, or first block is not a private key") + } + rawKey, err := x509.ParsePKCS8PrivateKey(privBlock.Bytes) + + key, ok := rawKey.(*ecdsa.PrivateKey) + if !ok { + return errors.New("key file did not contain an ecdsa private key") + } + + // Cryptographically random 64bit number as the serial. + sn, err := rand.Int(rand.Reader, big.NewInt(math.MaxInt64)) + if err != nil { + errors.New("Could not generate a ca serial number") + } + caTmpl := &x509.Certificate{ + SerialNumber: sn, + Subject: pkix.Name{ + // TODO: Accept common name as a flag. + CommonName: "Some Common Name", + }, + // Valid starting now and for 10 years. + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + // This cert is a CA. + IsCA: true, + // No intermeddiate CAs, just one root that can issue leaf certs. + MaxPathLen: 0, + MaxPathLenZero: true, + // Disambiguate `IsCA` and path len settings from their zero value. + BasicConstraintsValid: true, + // Only allow to sign new certs, and cert revokation lists. + // Can't be used as a server or client cert itself. + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + } + caBytes, err := x509.CreateCertificate( + rand.Reader, + // Same template and parent, i.e. self-signed. + caTmpl, + caTmpl, + &key.PublicKey, + key, + ) + caPEMBytes := pem.EncodeToMemory(&pem.Block{ + Type: pki.PEMTypeCertificate, + Bytes: caBytes, + }) + + caf, err := os.OpenFile( + Output, + // Write only, and fail if it alreay exists. + os.O_WRONLY|os.O_CREATE|os.O_EXCL, + pki.CertFilePerms, + ) + if err != nil { + return err + } + defer caf.Close() + if _, err = caf.Write(caPEMBytes); err != nil { + return err + } + + // Write ca.crt to Output + return nil + }, +} diff --git a/internal/clis/hmpki/generate_key.go b/internal/clis/hmpki/generate_key.go index 39e4096..2247e38 100644 --- a/internal/clis/hmpki/generate_key.go +++ b/internal/clis/hmpki/generate_key.go @@ -31,7 +31,7 @@ var generateKeyCmd = &cobra.Command{ } pemBytes := pem.EncodeToMemory(&pem.Block{ - Type: "PRIVATE KEY", + Type: pki.PEMTypePrivateKey, Bytes: der, }) diff --git a/pkg/pki/fileperms.go b/pkg/pki/fileperms.go index d703751..059cc47 100644 --- a/pkg/pki/fileperms.go +++ b/pkg/pki/fileperms.go @@ -2,4 +2,8 @@ package pki import "os" +// RW only by the owner. const PrivateKeyFilePerms os.FileMode = 0600 + +// W by owner, R by others. +const CertFilePerms os.FileMode = 0644 diff --git a/pkg/pki/pemtypes.go b/pkg/pki/pemtypes.go new file mode 100644 index 0000000..3fb516b --- /dev/null +++ b/pkg/pki/pemtypes.go @@ -0,0 +1,4 @@ +package pki + +const PEMTypePrivateKey = "PRIVATE KEY" +const PEMTypeCertificate = "CERTIFICATE"