Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions ca/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"fmt"
"io"
Expand Down Expand Up @@ -72,6 +73,9 @@ const (
MinNodeCertExpiration = 1 * time.Hour
)

// BasicConstraintsOID is the ASN1 Object ID indicating a basic constraints extension
var BasicConstraintsOID = asn1.ObjectIdentifier{2, 5, 29, 19}

// A recoverableErr is a non-fatal error encountered signing a certificate,
// which means that the certificate issuance may be retried at a later time.
type recoverableErr struct {
Expand Down Expand Up @@ -305,6 +309,42 @@ func (rca *RootCA) ParseValidateAndSignCSR(csrBytes []byte, cn, ou, org string)
return cert, nil
}

// CrossSignCACertificate takes a CA root certificate and generates an intermediate CA from it signed with the current root signer
func (rca *RootCA) CrossSignCACertificate(otherCAPEM []byte) ([]byte, error) {
if !rca.CanSign() {
return nil, ErrNoValidSigner
}

// create a new cert with exactly the same parameters, including the public key and exact NotBefore and NotAfter
rootCert, err := helpers.ParseCertificatePEM(rca.Cert)
if err != nil {
return nil, errors.Wrap(err, "could not parse old CA certificate")
}
rootSigner, err := helpers.ParsePrivateKeyPEM(rca.Signer.Key)
if err != nil {
return nil, errors.Wrap(err, "could not parse old CA key")
}

newCert, err := helpers.ParseCertificatePEM(otherCAPEM)
if err != nil {
return nil, errors.New("could not parse new CA certificate")
}

if !newCert.IsCA {
return nil, errors.New("certificate not a CA")
}

derBytes, err := x509.CreateCertificate(cryptorand.Reader, newCert, rootCert, newCert.PublicKey, rootSigner)
if err != nil {
return nil, errors.Wrap(err, "could not cross-sign new CA certificate using old CA material")
}

return pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: derBytes,
}), nil
}

// NewRootCA creates a new RootCA object from unparsed PEM cert bundle and key byte
// slices. key may be nil, and in this case NewRootCA will return a RootCA
// without a signer.
Expand Down
63 changes: 63 additions & 0 deletions ca/certificates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -888,3 +888,66 @@ func TestValidateCertificateChain(t *testing.T) {
require.NoError(t, err)
}
}

// Tests cross-signing using a certificate
func TestRootCACrossSignCACertificate(t *testing.T) {
t.Parallel()

cert1, key1, err := testutils.CreateRootCertAndKey("rootCN")
require.NoError(t, err)

rootCA1, err := ca.NewRootCA(cert1, key1, ca.DefaultNodeCertExpiration)
require.NoError(t, err)

cert2, key2, err := testutils.CreateRootCertAndKey("rootCN2")
require.NoError(t, err)

rootCA2, err := ca.NewRootCA(cert2, key2, ca.DefaultNodeCertExpiration)
require.NoError(t, err)

tempdir, err := ioutil.TempDir("", "cross-sign-cert")
require.NoError(t, err)
defer os.RemoveAll(tempdir)
paths := ca.NewConfigPaths(tempdir)
krw := ca.NewKeyReadWriter(paths.Node, nil, nil)

_, err = rootCA2.IssueAndSaveNewCertificates(krw, "cn", "ou", "org")
require.NoError(t, err)
certBytes, _, err := krw.Read()
require.NoError(t, err)
leafCert, err := helpers.ParseCertificatePEM(certBytes)
require.NoError(t, err)

// cross-signing a non-CA fails
_, err = rootCA1.CrossSignCACertificate(certBytes)
require.Error(t, err)

// cross-signing some non-cert PEM bytes fail
_, err = rootCA1.CrossSignCACertificate(key1)
require.Error(t, err)

intermediate, err := rootCA1.CrossSignCACertificate(cert2)
require.NoError(t, err)
parsedIntermediate, err := helpers.ParseCertificatePEM(intermediate)
require.NoError(t, err)
parsedRoot2, err := helpers.ParseCertificatePEM(cert2)
require.NoError(t, err)
require.Equal(t, parsedRoot2.RawSubject, parsedIntermediate.RawSubject)
require.Equal(t, parsedRoot2.RawSubjectPublicKeyInfo, parsedIntermediate.RawSubjectPublicKeyInfo)
require.True(t, parsedIntermediate.IsCA)

intermediatePool := x509.NewCertPool()
intermediatePool.AddCert(parsedIntermediate)

// we can validate a chain from the leaf to the first root through the intermediate,
// or from the leaf cert to the second root with or without the intermediate
_, err = leafCert.Verify(x509.VerifyOptions{Roots: rootCA1.Pool})
require.Error(t, err)
_, err = leafCert.Verify(x509.VerifyOptions{Roots: rootCA1.Pool, Intermediates: intermediatePool})
require.NoError(t, err)

_, err = leafCert.Verify(x509.VerifyOptions{Roots: rootCA2.Pool})
require.NoError(t, err)
_, err = leafCert.Verify(x509.VerifyOptions{Roots: rootCA2.Pool, Intermediates: intermediatePool})
require.NoError(t, err)
}
63 changes: 63 additions & 0 deletions ca/external.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@ package ca

import (
"bytes"
cryptorand "crypto/rand"
"crypto/tls"
"crypto/x509"
"encoding/hex"
"encoding/json"
"encoding/pem"
"io/ioutil"
"net/http"
"sync"

"github.com/Sirupsen/logrus"
"github.com/cloudflare/cfssl/api"
"github.com/cloudflare/cfssl/config"
"github.com/cloudflare/cfssl/csr"
"github.com/cloudflare/cfssl/helpers"
"github.com/cloudflare/cfssl/signer"
"github.com/pkg/errors"
"golang.org/x/net/context"
Expand Down Expand Up @@ -97,6 +104,62 @@ func (eca *ExternalCA) Sign(ctx context.Context, req signer.SignRequest) (cert [
return nil, err
}

// CrossSignRootCA takes a RootCA object, generates a CA CSR, sends a signing request with the CA CSR to the external
// CFSSL API server in order to obtain a cross-signed root
func (eca *ExternalCA) CrossSignRootCA(ctx context.Context, rca RootCA) ([]byte, error) {
if !rca.CanSign() {
return nil, errors.Wrap(ErrNoValidSigner, "cannot generate CSR for a cross-signed root")
}
rootCert, err := helpers.ParseCertificatePEM(rca.Cert)
if err != nil {
return nil, errors.Wrap(err, "could not parse CA certificate")
}
rootSigner, err := helpers.ParsePrivateKeyPEM(rca.Signer.Key)
if err != nil {
return nil, errors.Wrap(err, "could not parse old CA key")
}
// ExtractCertificateRequest generates a new key request, and we want to continue to use the old
// key. However, ExtractCertificateRequest will also convert the pkix.Name to csr.Name, which we
// need in order to generate a signing request
cfCSRObj := csr.ExtractCertificateRequest(rootCert)

der, err := x509.CreateCertificateRequest(cryptorand.Reader, &x509.CertificateRequest{
RawSubjectPublicKeyInfo: rootCert.RawSubjectPublicKeyInfo,
RawSubject: rootCert.RawSubject,
PublicKeyAlgorithm: rootCert.PublicKeyAlgorithm,
Subject: rootCert.Subject,
Extensions: rootCert.Extensions,
DNSNames: rootCert.DNSNames,
EmailAddresses: rootCert.EmailAddresses,
IPAddresses: rootCert.IPAddresses,
}, rootSigner)
if err != nil {
return nil, err
}
req := signer.SignRequest{
Request: string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: der,
})),
Subject: &signer.Subject{
CN: rootCert.Subject.CommonName,
Names: cfCSRObj.Names,
},
}
// cfssl actually ignores non subject alt name extensions in the CSR, so we have to add the CA extension in the signing
// request as well
for _, ext := range rootCert.Extensions {
if ext.Id.Equal(BasicConstraintsOID) {
req.Extensions = append(req.Extensions, signer.Extension{
ID: config.OID(ext.Id),
Critical: ext.Critical,
Value: hex.EncodeToString(ext.Value),
})
}
}
return eca.Sign(ctx, req)
}

func makeExternalSignRequest(ctx context.Context, client *http.Client, url string, csrJSON []byte) (cert []byte, err error) {
resp, err := ctxhttp.Post(ctx, client, url, "application/json", bytes.NewReader(csrJSON))
if err != nil {
Expand Down
79 changes: 79 additions & 0 deletions ca/external_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package ca_test

import (
"context"
"crypto/x509"
"testing"

"github.com/cloudflare/cfssl/helpers"
"github.com/docker/swarmkit/ca"
"github.com/docker/swarmkit/ca/testutils"
"github.com/stretchr/testify/require"
)

// Tests ExternalCA.CrossSignRootCA can produce an intermediate that can be used to
// validate a leaf certificate
func TestExternalCACrossSign(t *testing.T) {
t.Parallel()

if !testutils.External {
return // this is only tested using the external CA
}

tc := testutils.NewTestCA(t)
defer tc.Stop()
paths := ca.NewConfigPaths(tc.TempDir)

secConfig, err := tc.RootCA.CreateSecurityConfig(context.Background(),
ca.NewKeyReadWriter(paths.Node, nil, nil), ca.CertificateRequestConfig{})
require.NoError(t, err)
externalCA := secConfig.ExternalCA()
externalCA.UpdateURLs(tc.ExternalSigningServer.URL)

cert2, key2, err := testutils.CreateRootCertAndKey("rootCN2")
require.NoError(t, err)

rootCA2, err := ca.NewRootCA(cert2, key2, ca.DefaultNodeCertExpiration)
require.NoError(t, err)

krw := ca.NewKeyReadWriter(paths.Node, nil, nil)

_, err = rootCA2.IssueAndSaveNewCertificates(krw, "cn", "ou", "org")
require.NoError(t, err)
certBytes, _, err := krw.Read()
require.NoError(t, err)
leafCert, err := helpers.ParseCertificatePEM(certBytes)
require.NoError(t, err)

// we have not enabled CA signing on the external server
_, err = externalCA.CrossSignRootCA(context.Background(), rootCA2)
require.Error(t, err)

require.NoError(t, tc.ExternalSigningServer.EnableCASigning())

intermediate, err := externalCA.CrossSignRootCA(context.Background(), rootCA2)
require.NoError(t, err)

parsedIntermediate, err := helpers.ParseCertificatePEM(intermediate)
require.NoError(t, err)
parsedRoot2, err := helpers.ParseCertificatePEM(cert2)
require.NoError(t, err)
require.Equal(t, parsedRoot2.RawSubject, parsedIntermediate.RawSubject)
require.Equal(t, parsedRoot2.RawSubjectPublicKeyInfo, parsedIntermediate.RawSubjectPublicKeyInfo)
require.True(t, parsedIntermediate.IsCA)

intermediatePool := x509.NewCertPool()
intermediatePool.AddCert(parsedIntermediate)

// we can validate a chain from the leaf to the first root through the intermediate,
// or from the leaf cert to the second root with or without the intermediate
_, err = leafCert.Verify(x509.VerifyOptions{Roots: tc.RootCA.Pool})
require.Error(t, err)
_, err = leafCert.Verify(x509.VerifyOptions{Roots: tc.RootCA.Pool, Intermediates: intermediatePool})
require.NoError(t, err)

_, err = leafCert.Verify(x509.VerifyOptions{Roots: rootCA2.Pool})
require.NoError(t, err)
_, err = leafCert.Verify(x509.VerifyOptions{Roots: rootCA2.Pool, Intermediates: intermediatePool})
require.NoError(t, err)
}
Loading