Skip to content
This repository has been archived by the owner on Aug 31, 2021. It is now read-only.

Commit

Permalink
create intermediate CA certificates + bug fixes
Browse files Browse the repository at this point in the history
**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:

* hashicorp/vault#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
  • Loading branch information
theckman committed Oct 25, 2016
1 parent 5c2d8b7 commit 21b2160
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 30 deletions.
30 changes: 24 additions & 6 deletions cmd/easypki/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)",
Expand Down
89 changes: 65 additions & 24 deletions pkg/easypki/easyca.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
Expand All @@ -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)
}
Expand Down

0 comments on commit 21b2160

Please sign in to comment.