From 21b2160d55455afcbcf4799cdc2c74db83b69980 Mon Sep 17 00:00:00 2001 From: Tim Heckman Date: Sat, 22 Oct 2016 02:53:28 -0700 Subject: create intermediate CA certificates + bug fixes **Note**: This change introduces breaking changes to the `easypki` API: * The `GenerateCertificate` function had a typo in its name. It used to be `GenerateCertifcate` (missing an `i`). * The `GenerateCertificate` function now takes a struct as a parameter, making it easier to use. The main reason behind this change was to provide the ability to generate intermediate CA certificates. This will allow people and organizations to use `easypki` to create a multi-layered tree of trust. In addition to that, the ability to set the maximum path length on CA certificates was added to make the keys safer (less prone for abuse). You can now generate intermediate certificates using the `--intermediate` flag. This flag effectively creates a new CA certificate, within the CA, but doesn't overwrite the `ca.crt` or `ca.key` file. Instead, it uses the same logic as regular certificates and saves the cert and key within the `issued/` and `private/` directories respectively. It's suggested that the `--max-path-len` flag be used when generating CA certificates. You can now set the maximum path depth for a CA certificate by using the `--max-path-len` flag. If you want to generate an offline root CA and ensure that your intermediates cannot generate valid intermediate CA certificates themselves, you would set `--max-path-len 1` when generating the root CA. It's recommended to always use this flag when generating CA certificates, otherwise that certificate will be valid for an "infinite" number of intermediate certificates. With the features above added, a few bugs were discovered in the certificates being generated by `easypki`. Specifically we needed to fix some issues with the KeyUsage and ExtKeyUsage settings of the certs. While troubleshooting an issue with Consul, trying to do verification of a TLS chain generated by `easypki`, I ran in to a situation where the certificates were failing to validate. It turns out there were a few issues that caused this to happen. I found an issue on Hashicorp's Vault project referencing a similar issue with CA certificates generated by Vault itself. This guided me to the first bug that needed patching: * https://github.com/hashicorp/vault/pull/852 >Assign ExtKeyUsageAny to CA certs to help with validation with the >Windows Crypto API and Go's validation logic The solution: when generating CAs, we now set the `ExtKeyUsage` to `ExtKeyUsageAny`. This will mark the CA certificate as being valid for any usage. Some X.509 validation systems require that all certificates in the chain contain the requested usage, including in Go. The second was that the server certificates weren't being assigned `ExtKeyUsageClientAuth` causing issues with applications trying to use the certificates as both client and server certificates. The fix is to also give server certificates `ExtKeyUsageClientAuth`. Upon investigation of certificates deployed for public Internet services, it seems that `ExtKeyUsageClientAuth` is pretty common in server certificates. fixes #2 fixes #3 fixes #4 --- cmd/easypki/main.go | 30 +++++++++++++---- pkg/easypki/easyca.go | 89 +++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 89 insertions(+), 30 deletions(-) diff --git a/cmd/easypki/main.go b/cmd/easypki/main.go index 0c69340..381bc9c 100644 --- a/cmd/easypki/main.go +++ b/cmd/easypki/main.go @@ -74,16 +74,18 @@ func createBundle(c *cli.Context) { NotAfter: time.Now().AddDate(0, 0, c.Int("expire")), } - if c.Bool("ca") { + intCA := c.Bool("intermediate") + + if intCA || c.Bool("ca") { template.IsCA = true - filename = "ca" + + if !intCA { + filename = "ca" + } } else if c.Bool("client") { - template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth) template.EmailAddresses = c.StringSlice("email") } else { // We default to server - template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth) - IPs := make([]net.IP, 0, len(c.StringSlice("ip"))) for _, ipStr := range c.StringSlice("ip") { if i := net.ParseIP(ipStr); i != nil { @@ -93,7 +95,14 @@ func createBundle(c *cli.Context) { template.IPAddresses = IPs template.DNSNames = c.StringSlice("dns") } - err := easypki.GenerateCertifcate(c.GlobalString("root"), filename, template) + err := easypki.GenerateCertificate(&easypki.GenerationRequest{ + PKIRoot: c.GlobalString("root"), + Name: filename, + Template: template, + MaxPathLen: c.Int("max-path-len"), + IsIntermediateCA: intCA, + IsClientCertificate: c.Bool("client"), + }) if err != nil { log.Fatal(err) } @@ -170,6 +179,15 @@ func parseArgs() { Name: "ca", Usage: "certificate authority", }, + cli.BoolFlag{ + Name: "intermediate", + Usage: "intermediate certificate authority; implies --ca", + }, + cli.IntFlag{ + Name: "max-path-len", + Usage: "intermediate maximum path length", + Value: -1, // default to less-than 0 when not defined + }, cli.BoolFlag{ Name: "client", Usage: "generate a client certificate (default is server)", diff --git a/pkg/easypki/easyca.go b/pkg/easypki/easyca.go index 0c908d2..c38a295 100644 --- a/pkg/easypki/easyca.go +++ b/pkg/easypki/easyca.go @@ -66,22 +66,36 @@ func GeneratePrivateKey(path string) (*rsa.PrivateKey, error) { return key, nil } -func GenerateCertifcate(pkiroot, name string, template *x509.Certificate) error { +// GenerationRequest is a struct for providing configuration to +// GenerateCertifcate when actioning a certification generation request. +type GenerationRequest struct { + PKIRoot string + Name string + Template *x509.Certificate + MaxPathLen int + IsIntermediateCA bool + IsClientCertificate bool +} + +// GenerateCertificate is a function for helping to generate new x509 +// certificates and keys from the GenerationRequest. This function renders the +// content out to the filesystem. +func GenerateCertificate(genReq *GenerationRequest) error { // TODO(jclerc): check that pki has been init var crtPath string - privateKeyPath := filepath.Join(pkiroot, "private", name+".key") - if name == "ca" { - crtPath = filepath.Join(pkiroot, name+".crt") + privateKeyPath := filepath.Join(genReq.PKIRoot, "private", genReq.Name+".key") + if genReq.Name == "ca" { + crtPath = filepath.Join(genReq.PKIRoot, genReq.Name+".crt") } else { - crtPath = filepath.Join(pkiroot, "issued", name+".crt") + crtPath = filepath.Join(genReq.PKIRoot, "issued", genReq.Name+".crt") } var caCrt *x509.Certificate var caKey *rsa.PrivateKey if _, err := os.Stat(privateKeyPath); err == nil { - return fmt.Errorf("a key pair for %v already exists", name) + return fmt.Errorf("a key pair for %v already exists", genReq.Name) } privateKey, err := GeneratePrivateKey(privateKeyPath) @@ -94,39 +108,66 @@ func GenerateCertifcate(pkiroot, name string, template *x509.Certificate) error return fmt.Errorf("marshal public key: %v", err) } subjectKeyId := sha1.Sum(publicKeyBytes) - template.SubjectKeyId = subjectKeyId[:] + genReq.Template.SubjectKeyId = subjectKeyId[:] + + genReq.Template.NotBefore = time.Now() + genReq.Template.SignatureAlgorithm = x509.SHA256WithRSA - template.NotBefore = time.Now() - template.SignatureAlgorithm = x509.SHA256WithRSA - if template.IsCA { + if genReq.Template.IsCA { serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return fmt.Errorf("failed to generate ca serial number: %s", err) } - template.SerialNumber = serialNumber - template.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign - template.BasicConstraintsValid = true - template.Issuer = template.Subject - template.AuthorityKeyId = template.SubjectKeyId + genReq.Template.SerialNumber = serialNumber + genReq.Template.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign + genReq.Template.BasicConstraintsValid = true + genReq.Template.Issuer = genReq.Template.Subject + genReq.Template.AuthorityKeyId = genReq.Template.SubjectKeyId + + // if the maximum path length was provided be sure to enforce it + if genReq.MaxPathLen >= 0 { + genReq.Template.MaxPathLen = genReq.MaxPathLen + genReq.Template.MaxPathLenZero = true // doesn't force to zero + } + + // Go performs validation not according to spec but according to the Windows + // Crypto API, so we add all usages to CA certs + // - https://github.com/hashicorp/vault/pull/852 + genReq.Template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageAny} - caCrt = template + caCrt = genReq.Template caKey = privateKey - } else { - template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment - serialNumber, err := NextNumber(pkiroot, "serial") + } + + // if this is not a CA certificate... + // or if this is an intermediate certificate... + // we want to sign it with our parent CA's key + if !genReq.Template.IsCA || genReq.IsIntermediateCA { + if !genReq.IsIntermediateCA { + // set the usage for non-CA certificates + genReq.Template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment + genReq.Template.ExtKeyUsage = append(genReq.Template.ExtKeyUsage, x509.ExtKeyUsageClientAuth) + + // set UsageServerAuth only if this isn't a client cert + if !genReq.IsClientCertificate { + genReq.Template.ExtKeyUsage = append(genReq.Template.ExtKeyUsage, x509.ExtKeyUsageServerAuth) + } + } + + serialNumber, err := NextNumber(genReq.PKIRoot, "serial") if err != nil { return fmt.Errorf("get next serial: %v", err) } - template.SerialNumber = serialNumber + genReq.Template.SerialNumber = serialNumber - caCrt, caKey, err = GetCA(pkiroot) + caCrt, caKey, err = GetCA(genReq.PKIRoot) if err != nil { return fmt.Errorf("get ca: %v", err) } } - crt, err := x509.CreateCertificate(rand.Reader, template, caCrt, privateKey.Public(), caKey) + crt, err := x509.CreateCertificate(rand.Reader, genReq.Template, caCrt, privateKey.Public(), caKey) if err != nil { return fmt.Errorf("create certificate: %v", err) } @@ -146,8 +187,8 @@ func GenerateCertifcate(pkiroot, name string, template *x509.Certificate) error } // I do not think we have to write the ca.crt in the index - if !template.IsCA { - WriteIndex(pkiroot, name, template) + if !genReq.Template.IsCA { + WriteIndex(genReq.PKIRoot, genReq.Name, genReq.Template) if err != nil { return fmt.Errorf("write index: %v", err) } -- cgit v1.3 From bc210282ea3e468b9bf79878092d1b6479343c62 Mon Sep 17 00:00:00 2001 From: Tim Heckman Date: Wed, 26 Oct 2016 12:00:08 +1300 Subject: [easyca] for intermediary add eku client/server CA Also add DigitalSignature to certificates generated. --- pkg/easypki/easyca.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pkg/easypki/easyca.go b/pkg/easypki/easyca.go index c38a295..17d0192 100644 --- a/pkg/easypki/easyca.go +++ b/pkg/easypki/easyca.go @@ -67,7 +67,7 @@ func GeneratePrivateKey(path string) (*rsa.PrivateKey, error) { } // GenerationRequest is a struct for providing configuration to -// GenerateCertifcate when actioning a certification generation request. +// GenerateCertificate when actioning a certification generation request. type GenerationRequest struct { PKIRoot string Name string @@ -120,7 +120,7 @@ func GenerateCertificate(genReq *GenerationRequest) error { return fmt.Errorf("failed to generate ca serial number: %s", err) } genReq.Template.SerialNumber = serialNumber - genReq.Template.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign + genReq.Template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign genReq.Template.BasicConstraintsValid = true genReq.Template.Issuer = genReq.Template.Subject genReq.Template.AuthorityKeyId = genReq.Template.SubjectKeyId @@ -131,10 +131,10 @@ func GenerateCertificate(genReq *GenerationRequest) error { genReq.Template.MaxPathLenZero = true // doesn't force to zero } - // Go performs validation not according to spec but according to the Windows - // Crypto API, so we add all usages to CA certs - // - https://github.com/hashicorp/vault/pull/852 - genReq.Template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageAny} + genReq.Template.ExtKeyUsage = []x509.ExtKeyUsage{ + x509.ExtKeyUsageClientAuth, + x509.ExtKeyUsageServerAuth, + } caCrt = genReq.Template caKey = privateKey -- cgit v1.3 From ea1891668a36322802b2c8b34bbe85b25d989f74 Mon Sep 17 00:00:00 2001 From: AJ Christensen Date: Wed, 26 Oct 2016 12:00:08 +1300 Subject: [easyca] handle CAs, Intermediates, and Client * Split certificate type handling up, try to re-use operations where possible. * Set the EKUs for client/serverauth on clients, servers, and Intermediate CAs --- pkg/easypki/easyca.go | 59 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/pkg/easypki/easyca.go b/pkg/easypki/easyca.go index 17d0192..dbb6f0e 100644 --- a/pkg/easypki/easyca.go +++ b/pkg/easypki/easyca.go @@ -113,48 +113,47 @@ func GenerateCertificate(genReq *GenerationRequest) error { genReq.Template.NotBefore = time.Now() genReq.Template.SignatureAlgorithm = x509.SHA256WithRSA - if genReq.Template.IsCA { + // Non-intermediate Certificate Authority + if genReq.Template.IsCA && !genReq.IsIntermediateCA { + // Random Serial serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) if err != nil { return fmt.Errorf("failed to generate ca serial number: %s", err) } genReq.Template.SerialNumber = serialNumber - genReq.Template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign - genReq.Template.BasicConstraintsValid = true + + // Root certificate can self-sign genReq.Template.Issuer = genReq.Template.Subject genReq.Template.AuthorityKeyId = genReq.Template.SubjectKeyId - // if the maximum path length was provided be sure to enforce it - if genReq.MaxPathLen >= 0 { - genReq.Template.MaxPathLen = genReq.MaxPathLen - genReq.Template.MaxPathLenZero = true // doesn't force to zero - } + // Use the generated certificate template and private key (self-signing) + caCrt = genReq.Template + caKey = privateKey + } + // Intermediate-only Certificate Authority + if genReq.Template.IsCA && genReq.IsIntermediateCA { genReq.Template.ExtKeyUsage = []x509.ExtKeyUsage{ x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth, } - - caCrt = genReq.Template - caKey = privateKey } - // if this is not a CA certificate... - // or if this is an intermediate certificate... - // we want to sign it with our parent CA's key - if !genReq.Template.IsCA || genReq.IsIntermediateCA { - if !genReq.IsIntermediateCA { - // set the usage for non-CA certificates - genReq.Template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment - genReq.Template.ExtKeyUsage = append(genReq.Template.ExtKeyUsage, x509.ExtKeyUsageClientAuth) - - // set UsageServerAuth only if this isn't a client cert - if !genReq.IsClientCertificate { - genReq.Template.ExtKeyUsage = append(genReq.Template.ExtKeyUsage, x509.ExtKeyUsageServerAuth) - } + // Either type of Certificate Authority (intermediate, root, etc.) + if genReq.Template.IsCA || genReq.IsIntermediateCA { + genReq.Template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign | x509.KeyUsageCRLSign + genReq.Template.BasicConstraintsValid = true + + // Enforce Maximum Path Length + if genReq.MaxPathLen >= 0 { + genReq.Template.MaxPathLen = genReq.MaxPathLen + genReq.Template.MaxPathLenZero = true // doesn't force to zero } + } + // Any leaf: intermediate CAs, client/server certificates, signed by a root + if !genReq.Template.IsCA || genReq.IsIntermediateCA { serialNumber, err := NextNumber(genReq.PKIRoot, "serial") if err != nil { return fmt.Errorf("get next serial: %v", err) @@ -167,6 +166,18 @@ func GenerateCertificate(genReq *GenerationRequest) error { } } + // Should cover only client/server (All non-CA, i.e. doesn't include intermediates) + if !genReq.Template.IsCA { + if !genReq.IsClientCertificate { + genReq.Template.ExtKeyUsage = append(genReq.Template.ExtKeyUsage, x509.ExtKeyUsageServerAuth) + } + // Clients can only use ClientAuth + genReq.Template.ExtKeyUsage = append(genReq.Template.ExtKeyUsage, x509.ExtKeyUsageClientAuth) + + // set the usage for non-CA certificates + genReq.Template.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment + } + crt, err := x509.CreateCertificate(rand.Reader, genReq.Template, caCrt, privateKey.Public(), caKey) if err != nil { return fmt.Errorf("create certificate: %v", err) -- cgit v1.3