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
65 changes: 55 additions & 10 deletions ca/certificates.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,39 @@ type LocalSigner struct {
Key []byte
}

// RootCA is the representation of everything we need to sign certificates
// RootCA is the representation of everything we need to sign certificates and/or to verify certificates
//
// RootCA.Cert: [signing CA cert][CA cert1][CA cert2]
// RootCA.Intermediates: [intermediate CA1][intermediate CA2][intermediate CA3]
// RootCA.Signer.Key: [signing CA key]
//
// Requirements:
//
// - [signing CA key] must be the private key for [signing CA cert]
// - [signing CA cert] must be the first cert in RootCA.Cert
//
// - [intermediate CA1] must have the same public key and subject as [signing CA cert], because otherwise when
// appended to a leaf certificate, the intermediates will not form a chain (because [intermediate CA1] won't because
// the signer of the leaf certificate)
// - [intermediate CA1] must be signed by [intermediate CA2], which must be signed by [intermediate CA3]
//
// - When we issue a certificate, the intermediates will be appended so that the certificate looks like:
// [leaf signed by signing CA cert][intermediate CA1][intermediate CA2][intermediate CA3]
// - [leaf signed by signing CA cert][intermediate CA1][intermediate CA2][intermediate CA3] is guaranteed to form a
// valid chain from [leaf signed by signing CA cert] to one of the root certs ([signing CA cert], [CA cert1], [CA cert2])
// using zero or more of the intermediate certs ([intermediate CA1][intermediate CA2][intermediate CA3]) as intermediates
//
type RootCA struct {
// Cert contains a bundle of PEM encoded Certificate for the Root CA, the first one of which
// must correspond to the key in the local signer, if provided
Cert []byte

// Intermediates contains a bundle of PEM encoded intermediate CA certificates to append to any
// issued TLS (leaf) certificates. The first one must have the same public key and subject as the
// signing root certificate, and the rest must form a chain, each one certifying the one above it,
// as per RFC5246 section 7.4.2.
Intermediates []byte

// Pool is the root pool used to validate TLS certificates
Pool *x509.CertPool

Expand Down Expand Up @@ -306,7 +333,7 @@ func (rca *RootCA) ParseValidateAndSignCSR(csrBytes []byte, cn, ou, org string)
return nil, errors.Wrap(err, "failed to sign node certificate")
}

return cert, nil
return append(cert, rca.Intermediates...), nil
}

// CrossSignCACertificate takes a CA root certificate and generates an intermediate CA from it signed with the current root signer
Expand Down Expand Up @@ -348,7 +375,7 @@ func (rca *RootCA) CrossSignCACertificate(otherCAPEM []byte) ([]byte, error) {
// 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.
func NewRootCA(certBytes, keyBytes []byte, certExpiry time.Duration) (RootCA, error) {
func NewRootCA(certBytes, keyBytes []byte, certExpiry time.Duration, intermediates []byte) (RootCA, error) {
// Parse all the certificates in the cert bundle
parsedCerts, err := helpers.ParseCertificatesPEM(certBytes)
if err != nil {
Expand All @@ -368,7 +395,6 @@ func NewRootCA(certBytes, keyBytes []byte, certExpiry time.Duration) (RootCA, er
default:
return RootCA{}, fmt.Errorf("unsupported signature algorithm: %s", cert.SignatureAlgorithm.String())
}

// Check to see if all of the certificates are valid, self-signed root CA certs
selfpool := x509.NewCertPool()
selfpool.AddCert(cert)
Expand All @@ -381,9 +407,28 @@ func NewRootCA(certBytes, keyBytes []byte, certExpiry time.Duration) (RootCA, er
// Calculate the digest for our Root CA bundle
digest := digest.FromBytes(certBytes)

// We do not yet support arbitrary chains of intermediates (e.g. the case of an offline root, and the swarm CA is an
// intermediate CA). We currently only intermediates for which the first intermediate is cross-signed version of the
// CA signing cert (the first cert of the root certs) for the purposes of root rotation. If we wanted to support
// offline roots, we'd have to separate the CA signing cert from the self-signed root certs, but this intermediate
// validation logic should remain the same. Either the first intermediate would BE the intermediate CA we sign with
// (in which case it'd have the same subject and public key), or it would be a cross-signed intermediate with the
// same subject and public key as our signing cert (which could be either an intermediate cert or a self-signed root
// cert).
if len(intermediates) > 0 {
parsedIntermediates, err := ValidateCertChain(pool, intermediates, false)
if err != nil {
return RootCA{}, errors.Wrap(err, "invalid intermediate chain")
}
if !bytes.Equal(parsedIntermediates[0].RawSubject, parsedCerts[0].RawSubject) ||
!bytes.Equal(parsedIntermediates[0].RawSubjectPublicKeyInfo, parsedCerts[0].RawSubjectPublicKeyInfo) {
return RootCA{}, errors.New("invalid intermediate chain - the first intermediate must have the same subject and public key as the root")
}
}

if len(keyBytes) == 0 {
// This RootCA does not have a valid signer.
return RootCA{Cert: certBytes, Digest: digest, Pool: pool}, nil
// This RootCA does not have a valid signer
return RootCA{Cert: certBytes, Intermediates: intermediates, Digest: digest, Pool: pool}, nil
}

var (
Expand Down Expand Up @@ -434,7 +479,7 @@ func NewRootCA(certBytes, keyBytes []byte, certExpiry time.Duration) (RootCA, er
}
}

return RootCA{Signer: &LocalSigner{Signer: signer, Key: keyBytes}, Digest: digest, Cert: certBytes, Pool: pool}, nil
return RootCA{Signer: &LocalSigner{Signer: signer, Key: keyBytes}, Intermediates: intermediates, Digest: digest, Cert: certBytes, Pool: pool}, nil
}

// ValidateCertChain checks checks that the certificates provided chain up to the root pool provided. In addition
Expand Down Expand Up @@ -586,7 +631,7 @@ func GetLocalRootCA(paths CertPaths) (RootCA, error) {
key = nil
}

return NewRootCA(cert, key, DefaultNodeCertExpiration)
return NewRootCA(cert, key, DefaultNodeCertExpiration, nil)
}

func getGRPCConnection(creds credentials.TransportCredentials, connBroker *connectionbroker.Broker, forceRemote bool) (*connectionbroker.Conn, error) {
Expand Down Expand Up @@ -641,7 +686,7 @@ func GetRemoteCA(ctx context.Context, d digest.Digest, connBroker *connectionbro

// NewRootCA will validate that the certificates are otherwise valid and create a RootCA object.
// Since there is no key, the certificate expiry does not matter and will not be used.
return NewRootCA(response.Certificate, nil, DefaultNodeCertExpiration)
return NewRootCA(response.Certificate, nil, DefaultNodeCertExpiration, nil)
}

// CreateRootCA creates a Certificate authority for a new Swarm Cluster, potentially
Expand All @@ -660,7 +705,7 @@ func CreateRootCA(rootCN string, paths CertPaths) (RootCA, error) {
return RootCA{}, err
}

rootCA, err := NewRootCA(cert, key, DefaultNodeCertExpiration)
rootCA, err := NewRootCA(cert, key, DefaultNodeCertExpiration, nil)
if err != nil {
return RootCA{}, err
}
Expand Down
137 changes: 120 additions & 17 deletions ca/certificates_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,7 +540,7 @@ func TestNewRootCA(t *testing.T) {
{cert: testutils.ECDSA256SHA256Cert, key: testutils.ECDSA256Key},
{cert: testutils.RSA2048SHA256Cert, key: testutils.RSA2048Key},
} {
rootCA, err := ca.NewRootCA(pair.cert, pair.key, ca.DefaultNodeCertExpiration)
rootCA, err := ca.NewRootCA(pair.cert, pair.key, ca.DefaultNodeCertExpiration, nil)
require.NoError(t, err, string(pair.key))
require.Equal(t, pair.cert, rootCA.Cert)
require.Equal(t, pair.key, rootCA.Signer.Key)
Expand Down Expand Up @@ -572,7 +572,7 @@ func TestNewRootCABundle(t *testing.T) {
err = ioutil.WriteFile(paths.RootCA.Cert, bundle, 0644)
assert.NoError(t, err)

newRootCA, err := ca.NewRootCA(bundle, firstRootCA.Signer.Key, ca.DefaultNodeCertExpiration)
newRootCA, err := ca.NewRootCA(bundle, firstRootCA.Signer.Key, ca.DefaultNodeCertExpiration, nil)
assert.NoError(t, err)
assert.Equal(t, bundle, newRootCA.Cert)
assert.Equal(t, 2, len(newRootCA.Pool.Subjects()))
Expand All @@ -597,7 +597,7 @@ func TestNewRootCANonDefaultExpiry(t *testing.T) {
rootCA, err := ca.CreateRootCA("rootCN", paths.RootCA)
assert.NoError(t, err)

newRootCA, err := ca.NewRootCA(rootCA.Cert, rootCA.Signer.Key, 1*time.Hour)
newRootCA, err := ca.NewRootCA(rootCA.Cert, rootCA.Signer.Key, 1*time.Hour, nil)
assert.NoError(t, err)

// Create and sign a new CSR
Expand All @@ -614,7 +614,7 @@ func TestNewRootCANonDefaultExpiry(t *testing.T) {

// Sign the same CSR again, this time with a 59 Minute expiration RootCA (under the 60 minute minimum).
// This should use the default of 3 months
newRootCA, err = ca.NewRootCA(rootCA.Cert, rootCA.Signer.Key, 59*time.Minute)
newRootCA, err = ca.NewRootCA(rootCA.Cert, rootCA.Signer.Key, 59*time.Minute, nil)
assert.NoError(t, err)

cert, err = newRootCA.ParseValidateAndSignCSR(csr, "CN", ca.ManagerRole, "ORG")
Expand All @@ -627,14 +627,15 @@ func TestNewRootCANonDefaultExpiry(t *testing.T) {
assert.True(t, time.Now().Add(ca.DefaultNodeCertExpiration).AddDate(0, 0, 1).After(parsedCerts[0].NotAfter))
}

type invalidCertKeyTestCase struct {
cert []byte
key []byte
errorStr string
type invalidNewRootCATestCase struct {
cert []byte
key []byte
intermediates []byte
errorStr string
}

func TestNewRootCAInvalidCertAndKeys(t *testing.T) {
invalids := []invalidCertKeyTestCase{
invalids := []invalidNewRootCATestCase{
{
cert: []byte("malformed"),
key: testutils.ECDSA256Key,
Expand Down Expand Up @@ -688,12 +689,114 @@ func TestNewRootCAInvalidCertAndKeys(t *testing.T) {
}

for _, invalid := range invalids {
_, err := ca.NewRootCA(invalid.cert, invalid.key, ca.DefaultNodeCertExpiration)
_, err := ca.NewRootCA(invalid.cert, invalid.key, ca.DefaultNodeCertExpiration, nil)
require.Error(t, err, invalid.errorStr)
require.Contains(t, err.Error(), invalid.errorStr)
}
}

func TestRootCAWithCrossSignedIntermediates(t *testing.T) {
tempdir, err := ioutil.TempDir("", "swarm-ca-test-")
require.NoError(t, err)
defer os.RemoveAll(tempdir)

now := time.Now()

expiredIntermediate := testutils.ReDateCert(t, testutils.ECDSACertChain[1],
testutils.ECDSACertChain[2], testutils.ECDSACertChainKeys[2], now.Add(-10*time.Hour), now.Add(-1*time.Minute))
notYetValidIntermediate := testutils.ReDateCert(t, testutils.ECDSACertChain[1],
testutils.ECDSACertChain[2], testutils.ECDSACertChainKeys[2], now.Add(time.Hour), now.Add(2*time.Hour))

// re-generate the intermediate to be a self-signed root, and use that as the second root
parsedKey, err := helpers.ParsePrivateKeyPEM(testutils.ECDSACertChainKeys[1])
require.NoError(t, err)
parsedIntermediate, err := helpers.ParseCertificatePEM(testutils.ECDSACertChain[1])
require.NoError(t, err)
fauxRootDER, err := x509.CreateCertificate(cryptorand.Reader, parsedIntermediate, parsedIntermediate, parsedKey.Public(), parsedKey)
require.NoError(t, err)
fauxRootCert := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: fauxRootDER,
})

bothRoots := append(fauxRootCert, testutils.ECDSACertChain[2]...)

var invalids = []struct {
intermediate []byte
root []byte
}{
{intermediate: []byte("malformed"), root: bothRoots},
{intermediate: expiredIntermediate, root: bothRoots},
{intermediate: notYetValidIntermediate, root: bothRoots},
{intermediate: append(testutils.ECDSACertChain[1], testutils.ECDSA256SHA256Cert...), root: bothRoots}, // doesn't form chain
{intermediate: testutils.ECDSACertChain[1], root: fauxRootCert}, // doesn't chain up to the root
}

for _, invalid := range invalids {
_, err := ca.NewRootCA(invalid.root, testutils.ECDSACertChainKeys[2], ca.DefaultNodeCertExpiration, invalid.intermediate)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid intermediate chain")
}

// Trust the new root and the old root, else the intermediate will fail to chain up to the root pool
// It is not required, but not wrong, for the intermediate chain to terminate with a self-signed root
newRoot, err := ca.NewRootCA(bothRoots, testutils.ECDSACertChainKeys[1], ca.DefaultNodeCertExpiration,
append(testutils.ECDSACertChain[1], testutils.ECDSACertChain[2]...))
require.NoError(t, err)

// just the intermediate, without a terminating self-signed root, is also ok
newRoot, err = ca.NewRootCA(bothRoots, testutils.ECDSACertChainKeys[1], ca.DefaultNodeCertExpiration,
testutils.ECDSACertChain[1])
require.NoError(t, err)

paths := ca.NewConfigPaths(tempdir)
krw := ca.NewKeyReadWriter(paths.Node, nil, nil)
_, err = newRoot.IssueAndSaveNewCertificates(krw, "cn", "ou", "org")
require.NoError(t, err)
tlsCert, _, err := krw.Read()
require.NoError(t, err)

parsedCerts, err := ca.ValidateCertChain(newRoot.Pool, tlsCert, false)
require.NoError(t, err)
require.Len(t, parsedCerts, 2)
require.Equal(t, parsedIntermediate.Raw, parsedCerts[1].Raw)

oldRoot, err := ca.NewRootCA(testutils.ECDSACertChain[2], testutils.ECDSACertChainKeys[2], ca.DefaultNodeCertExpiration, nil)
require.NoError(t, err)

parsedCerts, err = ca.ValidateCertChain(oldRoot.Pool, tlsCert, false)
require.NoError(t, err)
require.Len(t, parsedCerts, 2)
require.Equal(t, parsedIntermediate.Raw, parsedCerts[1].Raw)

if !testutils.External {
return
}

// create an external signing server that generates leaf certs with the new root (but does not append the intermediate)
externalCARoot, err := ca.NewRootCA(fauxRootCert, testutils.ECDSACertChainKeys[1], ca.DefaultNodeCertExpiration, nil)
require.NoError(t, err)
tc := testutils.NewTestCAFromRootCA(t, tempdir, externalCARoot, nil)
defer tc.Stop()

secConfig, err := newRoot.CreateSecurityConfig(context.Background(), krw, ca.CertificateRequestConfig{})
require.NoError(t, err)

externalCA := secConfig.ExternalCA()
externalCA.UpdateURLs(tc.ExternalSigningServer.URL)

newCSR, _, err := ca.GenerateNewCSR()
require.NoError(t, err)

tlsCert, err = externalCA.Sign(context.Background(), ca.PrepareCSR(newCSR, "cn", ca.ManagerRole, secConfig.ClientTLSCreds.Organization()))
require.NoError(t, err)

parsedCerts, err = ca.ValidateCertChain(oldRoot.Pool, tlsCert, false)
require.NoError(t, err)
require.Len(t, parsedCerts, 2)
require.Equal(t, parsedIntermediate.Raw, parsedCerts[1].Raw)
}

func TestNewRootCAWithPassphrase(t *testing.T) {
tempBaseDir, err := ioutil.TempDir("", "swarm-ca-test-")
assert.NoError(t, err)
Expand All @@ -709,7 +812,7 @@ func TestNewRootCAWithPassphrase(t *testing.T) {
// Ensure that we're encrypting the Key bytes out of NewRoot if there
// is a passphrase set as an env Var
os.Setenv(ca.PassphraseENVVar, "password1")
newRootCA, err := ca.NewRootCA(rootCA.Cert, rootCA.Signer.Key, ca.DefaultNodeCertExpiration)
newRootCA, err := ca.NewRootCA(rootCA.Cert, rootCA.Signer.Key, ca.DefaultNodeCertExpiration, nil)
assert.NoError(t, err)
assert.NotEqual(t, rootCA.Signer.Key, newRootCA.Signer.Key)
assert.Equal(t, rootCA.Cert, newRootCA.Cert)
Expand All @@ -718,7 +821,7 @@ func TestNewRootCAWithPassphrase(t *testing.T) {

// Ensure that we're decrypting the Key bytes out of NewRoot if there
// is a passphrase set as an env Var
anotherNewRootCA, err := ca.NewRootCA(newRootCA.Cert, newRootCA.Signer.Key, ca.DefaultNodeCertExpiration)
anotherNewRootCA, err := ca.NewRootCA(newRootCA.Cert, newRootCA.Signer.Key, ca.DefaultNodeCertExpiration, nil)
assert.NoError(t, err)
assert.Equal(t, newRootCA, anotherNewRootCA)
assert.NotContains(t, string(rootCA.Signer.Key), string(anotherNewRootCA.Signer.Key))
Expand All @@ -727,19 +830,19 @@ func TestNewRootCAWithPassphrase(t *testing.T) {
// Ensure that we cant decrypt the Key bytes out of NewRoot if there
// is a wrong passphrase set as an env Var
os.Setenv(ca.PassphraseENVVar, "password2")
anotherNewRootCA, err = ca.NewRootCA(newRootCA.Cert, newRootCA.Signer.Key, ca.DefaultNodeCertExpiration)
anotherNewRootCA, err = ca.NewRootCA(newRootCA.Cert, newRootCA.Signer.Key, ca.DefaultNodeCertExpiration, nil)
assert.Error(t, err)

// Ensure that we cant decrypt the Key bytes out of NewRoot if there
// is a wrong passphrase set as an env Var
os.Setenv(ca.PassphraseENVVarPrev, "password2")
anotherNewRootCA, err = ca.NewRootCA(newRootCA.Cert, newRootCA.Signer.Key, ca.DefaultNodeCertExpiration)
anotherNewRootCA, err = ca.NewRootCA(newRootCA.Cert, newRootCA.Signer.Key, ca.DefaultNodeCertExpiration, nil)
assert.Error(t, err)

// Ensure that we can decrypt the Key bytes out of NewRoot if there
// is a wrong passphrase set as an env Var, but a valid as Prev
os.Setenv(ca.PassphraseENVVarPrev, "password1")
anotherNewRootCA, err = ca.NewRootCA(newRootCA.Cert, newRootCA.Signer.Key, ca.DefaultNodeCertExpiration)
anotherNewRootCA, err = ca.NewRootCA(newRootCA.Cert, newRootCA.Signer.Key, ca.DefaultNodeCertExpiration, nil)
assert.NoError(t, err)
assert.Equal(t, newRootCA, anotherNewRootCA)
assert.NotContains(t, string(rootCA.Signer.Key), string(anotherNewRootCA.Signer.Key))
Expand Down Expand Up @@ -896,13 +999,13 @@ func TestRootCACrossSignCACertificate(t *testing.T) {
cert1, key1, err := testutils.CreateRootCertAndKey("rootCN")
require.NoError(t, err)

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

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

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

tempdir, err := ioutil.TempDir("", "cross-sign-cert")
Expand Down
2 changes: 1 addition & 1 deletion ca/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ func (s *SecurityConfig) UpdateRootCA(cert, key []byte, certExpiry time.Duration
s.mu.Lock()
defer s.mu.Unlock()

rootCA, err := NewRootCA(cert, key, certExpiry)
rootCA, err := NewRootCA(cert, key, certExpiry, nil)
if err != nil {
return err
}
Expand Down
Loading