diff --git a/cmd/notary/keys.go b/cmd/notary/keys.go index 328ced8f57..91469c745a 100644 --- a/cmd/notary/keys.go +++ b/cmd/notary/keys.go @@ -1,16 +1,24 @@ package main import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" "crypto/x509" "crypto/x509/pkix" + "encoding/pem" "fmt" "math" + "math/big" "net/url" "os" + "path/filepath" "time" "github.com/docker/vetinari/trustmanager" "github.com/spf13/cobra" + "github.com/spf13/viper" ) var subjectKeyID string @@ -26,6 +34,7 @@ func init() { cmdKeys.AddCommand(cmdKeysTrust) cmdKeys.AddCommand(cmdKeysList) cmdKeys.AddCommand(cmdKeysRemove) + cmdKeys.AddCommand(cmdKeysGenerate) } var cmdKeysList = &cobra.Command{ @@ -49,6 +58,13 @@ var cmdKeysTrust = &cobra.Command{ Run: keysTrust, } +var cmdKeysGenerate = &cobra.Command{ + Use: "generate [ QDN ]", + Short: "Generates a new key for a specific QDN.", + Long: "generates a new key for a specific QDN. Qualified Docker Name.", + Run: keysGenerate, +} + func keysRemove(cmd *cobra.Command, args []string) { if len(args) < 1 { cmd.Usage() @@ -114,6 +130,91 @@ func keysList(cmd *cobra.Command, args []string) { } +func keysGenerate(cmd *cobra.Command, args []string) { + if len(args) < 1 { + cmd.Usage() + fatalf("must specify a QDN") + } + + // (diogo): Validate QDNs + qualifiedDN := args[0] + + key, err := generateKey(qualifiedDN) + if err != nil { + fatalf("could not generate key: %v", err) + } + + template := newCertificate(qualifiedDN, qualifiedDN) + derBytes, err := x509.CreateCertificate(rand.Reader, template, template, key.(crypto.Signer).Public(), key) + if err != nil { + fatalf("failed to generate certificate: %s", err) + } + + certName := filepath.Join(viper.GetString("privDir"), qualifiedDN+".crt") + certOut, err := os.Create(certName) + if err != nil { + fatalf("failed to save certificate: %s", err) + } + + defer certOut.Close() + err = pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + if err != nil { + fatalf("failed to save certificate: %s", err) + } +} + +func newCertificate(qualifiedDN, organization string) *x509.Certificate { + notBefore := time.Now() + notAfter := notBefore.Add(time.Hour * 24 * 365 * 2) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + if err != nil { + fatalf("failed to generate serial number: %s", err) + } + + return &x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{organization}, + CommonName: qualifiedDN, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, + BasicConstraintsValid: true, + } +} + +func generateKey(qualifiedDN string) (crypto.PrivateKey, error) { + curve := elliptic.P384() + key, err := ecdsa.GenerateKey(curve, rand.Reader) + if err != nil { + return nil, fmt.Errorf("could not generate private key: %v", err) + } + + keyBytes, err := x509.MarshalECPrivateKey(key) + if err != nil { + return nil, fmt.Errorf("could not marshal private key: %v", err) + } + + keyName := filepath.Join(viper.GetString("privDir"), qualifiedDN+".key") + keyOut, err := os.OpenFile(keyName, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return nil, fmt.Errorf("could not write privatekey: %v", err) + } + defer keyOut.Close() + + err = pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes}) + if err != nil { + return nil, fmt.Errorf("failed to encode key: %v", err) + } + + return key, nil +} + func printCert(cert *x509.Certificate) { timeDifference := cert.NotAfter.Sub(time.Now()) subjectKeyID := trustmanager.FingerprintCert(cert) diff --git a/cmd/notary/main.go b/cmd/notary/main.go index 9329123ebf..586f50f26a 100644 --- a/cmd/notary/main.go +++ b/cmd/notary/main.go @@ -17,6 +17,7 @@ import ( const configFileName string = "config" const configPath string = ".docker/trust/" const caDir string = ".docker/trust/certificate_authorities/" +const privDir string = ".docker/trust/private/" var caStore trustmanager.X509Store @@ -49,12 +50,15 @@ func init() { // Set up the defaults for our config viper.SetDefault("caDir", path.Join(homeDir, path.Dir(caDir))) + viper.SetDefault("privDir", path.Join(homeDir, path.Dir(privDir))) // Get the final value for the CA directory finalcaDir := viper.GetString("caDir") + finalPrivDir := viper.GetString("privDir") // Ensure the existence of the CAs directory createDirectory(finalcaDir) + createDirectory(finalPrivDir) // Load all CAs that aren't expired and don't use SHA1 // We could easily add "return cert.IsCA && cert.BasicConstraintsValid" in order