diff --git a/ca/certificates.go b/ca/certificates.go index 7a7b9abc73..da60839636 100644 --- a/ca/certificates.go +++ b/ca/certificates.go @@ -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 @@ -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 @@ -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 { @@ -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) @@ -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 ( @@ -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 @@ -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) { @@ -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 @@ -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 } diff --git a/ca/certificates_test.go b/ca/certificates_test.go index 0828cd240c..aee5e4d188 100644 --- a/ca/certificates_test.go +++ b/ca/certificates_test.go @@ -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) @@ -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())) @@ -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 @@ -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") @@ -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, @@ -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) @@ -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) @@ -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)) @@ -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)) @@ -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") diff --git a/ca/config.go b/ca/config.go index 75b0c95863..8a10ebadfb 100644 --- a/ca/config.go +++ b/ca/config.go @@ -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 } diff --git a/ca/config_test.go b/ca/config_test.go index a4f7585ed5..f05d0e70ec 100644 --- a/ca/config_test.go +++ b/ca/config_test.go @@ -245,7 +245,7 @@ func TestLoadSecurityConfigIntermediates(t *testing.T) { paths := ca.NewConfigPaths(tempdir) krw := ca.NewKeyReadWriter(paths.Node, nil, nil) - rootCA, err := ca.NewRootCA(testutils.ECDSACertChain[2], nil, ca.DefaultNodeCertExpiration) + rootCA, err := ca.NewRootCA(testutils.ECDSACertChain[2], nil, ca.DefaultNodeCertExpiration, nil) require.NoError(t, err) // loading the incomplete chain fails @@ -288,7 +288,7 @@ func TestSecurityConfigUpdateRootCA(t *testing.T) { // create the "original" security config, and we'll update it to trust the test server's cert, key, err := testutils.CreateRootCertAndKey("root1") require.NoError(t, err) - rootCA, err := ca.NewRootCA(cert, key, ca.DefaultNodeCertExpiration) + rootCA, err := ca.NewRootCA(cert, key, ca.DefaultNodeCertExpiration, nil) require.NoError(t, err) tempdir, err := ioutil.TempDir("", "test-security-config-update") @@ -335,7 +335,7 @@ func TestSecurityConfigUpdateRootCA(t *testing.T) { externalServer := tc.ExternalSigningServer if testutils.External { // stop the external server and create a new one because the external server actually has to trust our client certs as well. - updatedRoot, err := ca.NewRootCA(append(tc.RootCA.Cert, cert...), tc.RootCA.Signer.Key, ca.DefaultNodeCertExpiration) + updatedRoot, err := ca.NewRootCA(append(tc.RootCA.Cert, cert...), tc.RootCA.Signer.Key, ca.DefaultNodeCertExpiration, nil) require.NoError(t, err) externalServer, err = testutils.NewExternalSigningServer(updatedRoot, tc.TempDir) require.NoError(t, err) @@ -439,7 +439,7 @@ func TestRenewTLSConfigWorker(t *testing.T) { // Create a new RootCA, and change the policy to issue 6 minute certificates // Because of the default backdate of 5 minutes, this issues certificates // valid for 1 minute. - newRootCA, err := ca.NewRootCA(tc.RootCA.Cert, tc.RootCA.Signer.Key, ca.DefaultNodeCertExpiration) + newRootCA, err := ca.NewRootCA(tc.RootCA.Cert, tc.RootCA.Signer.Key, ca.DefaultNodeCertExpiration, nil) assert.NoError(t, err) newRootCA.Signer.SetPolicy(&cfconfig.Signing{ Default: &cfconfig.SigningProfile{ @@ -493,7 +493,7 @@ func TestRenewTLSConfigManager(t *testing.T) { // Create a new RootCA, and change the policy to issue 6 minute certificates // Because of the default backdate of 5 minutes, this issues certificates // valid for 1 minute. - newRootCA, err := ca.NewRootCA(tc.RootCA.Cert, tc.RootCA.Signer.Key, ca.DefaultNodeCertExpiration) + newRootCA, err := ca.NewRootCA(tc.RootCA.Cert, tc.RootCA.Signer.Key, ca.DefaultNodeCertExpiration, nil) assert.NoError(t, err) newRootCA.Signer.SetPolicy(&cfconfig.Signing{ Default: &cfconfig.SigningProfile{ @@ -549,7 +549,7 @@ func TestRenewTLSConfigWithNoNode(t *testing.T) { // Create a new RootCA, and change the policy to issue 6 minute certificates. // Because of the default backdate of 5 minutes, this issues certificates // valid for 1 minute. - newRootCA, err := ca.NewRootCA(tc.RootCA.Cert, tc.RootCA.Signer.Key, ca.DefaultNodeCertExpiration) + newRootCA, err := ca.NewRootCA(tc.RootCA.Cert, tc.RootCA.Signer.Key, ca.DefaultNodeCertExpiration, nil) assert.NoError(t, err) newRootCA.Signer.SetPolicy(&cfconfig.Signing{ Default: &cfconfig.SigningProfile{ diff --git a/ca/external.go b/ca/external.go index f53078f8f4..7dd8a7c8be 100644 --- a/ca/external.go +++ b/ca/external.go @@ -96,7 +96,7 @@ func (eca *ExternalCA) Sign(ctx context.Context, req signer.SignRequest) (cert [ for _, url := range urls { cert, err = makeExternalSignRequest(ctx, client, url, csrJSON) if err == nil { - return cert, err + return append(cert, eca.rootCA.Intermediates...), err } logrus.Debugf("unable to proxy certificate signing request to %s: %s", url, err) } diff --git a/ca/external_test.go b/ca/external_test.go index b064746f3c..5e34767eec 100644 --- a/ca/external_test.go +++ b/ca/external_test.go @@ -33,7 +33,7 @@ func TestExternalCACrossSign(t *testing.T) { 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) krw := ca.NewKeyReadWriter(paths.Node, nil, nil) diff --git a/ca/testutils/cautils.go b/ca/testutils/cautils.go index 492cebf2f8..b6bb6895fb 100644 --- a/ca/testutils/cautils.go +++ b/ca/testutils/cautils.go @@ -103,20 +103,28 @@ var External bool // NewTestCA is a helper method that creates a TestCA and a bunch of default // connections and security configs. func NewTestCA(t *testing.T, krwGenerators ...func(ca.CertPaths) *ca.KeyReadWriter) *TestCA { - tempBaseDir, err := ioutil.TempDir("", "swarm-ca-test-") - assert.NoError(t, err) + tempdir, err := ioutil.TempDir("", "swarm-ca-test-") + require.NoError(t, err) + paths := ca.NewConfigPaths(tempdir) + + rootCA, err := createAndWriteRootCA("swarm-test-CA", paths.RootCA, ca.DefaultNodeCertExpiration) + require.NoError(t, err) + return NewTestCAFromRootCA(t, tempdir, rootCA, krwGenerators) +} + +// NewTestCAFromRootCA is a helper method that creates a TestCA and a bunch of default +// connections and security configs, given a temp directory and a RootCA to use for signing. +func NewTestCAFromRootCA(t *testing.T, tempBaseDir string, rootCA ca.RootCA, krwGenerators []func(ca.CertPaths) *ca.KeyReadWriter) *TestCA { s := store.NewMemoryStore(nil) paths := ca.NewConfigPaths(tempBaseDir) organization := identity.NewID() - rootCA, err := createAndWriteRootCA("swarm-test-CA", paths.RootCA, ca.DefaultNodeCertExpiration) - assert.NoError(t, err) - var ( externalSigningServer *ExternalSigningServer externalCAs []*api.ExternalCA + err error ) if External { diff --git a/ca/testutils/staticcerts.go b/ca/testutils/staticcerts.go index 0b70abfa26..52268c8e00 100644 --- a/ca/testutils/staticcerts.go +++ b/ca/testutils/staticcerts.go @@ -1,7 +1,7 @@ package testutils var ( - // NotYetValidCert is an ECDSA certificate that becomes valid in 2117, and expires in 2316 + // NotYetValidCert is an ECDSA CA certificate that becomes valid in 2117, and expires in 2316 NotYetValidCert = []byte(` -----BEGIN CERTIFICATE----- MIIBajCCARCgAwIBAgIUWYyg+FvrTJ/wtJd4pZF/GfO5uC0wCgYIKoZIzj0EAwIw @@ -23,17 +23,17 @@ AwEHoUQDQgAEMLSpaZt8CRkfTXFRWPDBH7ai+i1TWJUhQPZ84Wb3dZR+DfVzKBr1 -----END EC PRIVATE KEY----- `) - // ExpiredCert is an ECDSA certificate that expired in 2007 + // ExpiredCert is an ECDSA CA certificate that expired in 2007 (1967-2007) ExpiredCert = []byte(` -----BEGIN CERTIFICATE----- -MIIBZjCCAQygAwIBAgIUNwwbocQMXzakEpwZoGkk7yOleRgwCgYIKoZIzj0EAwIw -ETEPMA0GA1UEAxMGcm9vdENOMB4XDTg3MDIwMzE4MTg1MloXDTc3MDIwNTE4MTg1 -MlowETEPMA0GA1UEAxMGcm9vdENOMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +MIIBZzCCAQygAwIBAgIUNwwbocQMXzakEpwZoGkk7yOleRgwCgYIKoZIzj0EAwIw +ETEPMA0GA1UEAxMGcm9vdENOMB4XDTY3MDIyNDIzMDc0MFoXDTA3MDIyNDIzMDc0 +MFowETEPMA0GA1UEAxMGcm9vdENOMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE MLSpaZt8CRkfTXFRWPDBH7ai+i1TWJUhQPZ84Wb3dZR+DfVzKBr108OyDtEZcmVE GdT0DjxNZkzMhyppr1mnDKNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF MAMBAf8wHQYDVR0OBBYEFLXvPNd7VDa0FkbDzEYvSykTzzJBMAoGCCqGSM49BAMC -A0gAMEUCIQDftvYe1qX/paQ+VqNHa56pOKsZ0vp7IT8Ga1mCeWB9ygIgIj+yV7Xr -2IuYMcW9DrKSP8fJXK0vqYzCRp4W61m/IA0= +A0kAMEYCIQCx5Lhl4b3YsjQuqHT/+vL5rnc0GV/OwJ8l2GFS2IB7EgIhAKrHZrcr +5+MmM1YUiykjweok2j5rj0/+9sR7waa69dkW -----END CERTIFICATE----- `) // ExpiredKey is the key corresponding to the ExpiredCert @@ -45,7 +45,7 @@ AwEHoUQDQgAEMLSpaZt8CRkfTXFRWPDBH7ai+i1TWJUhQPZ84Wb3dZR+DfVzKBr1 -----END EC PRIVATE KEY----- `) - // RSA2048SHA256Cert is an RSA cert with a 2048-bit key, SHA256 signature algorithm, that is currently valid and expires in 2117. + // RSA2048SHA256Cert is an RSA CA cert with a 2048-bit key, SHA256 signature algorithm, that is currently valid and expires in 2117. // This should be valid because the key length is at least 2048 and the signature algorithm is SHA256. RSA2048SHA256Cert = []byte(` -----BEGIN CERTIFICATE----- @@ -70,7 +70,7 @@ fpg1gDGYtAcxpE+qZBI+YCh0r9ae/Wtg3lzw+I7/usmfO2Pm56Hb/O7ulRuLEOFu XL2VZMKBpOTyDpe3YXMcvp3HT4qO5PmNs1b/N3Q8GwYRwfg6DZX2fPHT9vJGEdyq -----END CERTIFICATE----- `) - // RSA2048SHA1Cert is an RSA cert with a 2048-bit key, SHA1 signature algorithm, that is currently valid and expires in 2117. + // RSA2048SHA1Cert is an RSA CA cert with a 2048-bit key, SHA1 signature algorithm, that is currently valid and expires in 2117. // This should be not valid because the signature algorithm is SHA1. RSA2048SHA1Cert = []byte(` -----BEGIN CERTIFICATE----- @@ -126,7 +126,7 @@ U76uzbRNRlCGtKPKRwQhcSxrc6gNuCd84l1t1goCBvkQk/c0q2J/8YQi743OJLT6 -----END RSA PRIVATE KEY----- `) - // RSA1024Cert is an RSA cert with a 1024-bit key, SHA256 signature algorithm, that is currently valid and expires in 2117. + // RSA1024Cert is an RSA CA cert with a 1024-bit key, SHA256 signature algorithm, that is currently valid and expires in 2117. // This should not be a valid cert because the key is only 1024 bits. RSA1024Cert = []byte(` -----BEGIN CERTIFICATE----- @@ -165,7 +165,7 @@ anuVw1kAAKz5HYioZkBJpnpN5dXCHNC54ooq76cIGFpT -----END RSA PRIVATE KEY----- `) - // ECDSA224Cert is an ECDSA curve-P224 cert with a SHA256 signature algorithm + // ECDSA224Cert is an ECDSA curve-P224 CA cert with a SHA256 signature algorithm // that is current valid and expires in 2117. This should not be a valid cert because we only accept curve-P256, // curve-P385, and curve-P521 (the only keys cfssl will generate). ECDSA224Cert = []byte(` @@ -192,7 +192,7 @@ AARbHhgFiGwhyomtEzerYyW87uBjoupasYrYppMoFp8WkTlaNbCf+tQNJZsu82eZ -----END EC PRIVATE KEY----- `) - // ECDSA256SHA256Cert is an ECDSA curve-P256 cert with a SHA256 signature algorithm + // ECDSA256SHA256Cert is an ECDSA curve-P256 CA cert with a SHA256 signature algorithm // that is current valid and expires in 2117. This is a valid cert because it has an accepted key length // and an accepted signature algorithm. ECDSA256SHA256Cert = []byte(` @@ -210,7 +210,7 @@ BggqhkjOPQQDAgNIADBFAiAdIZG7qzr+vCSt6FnotFKOhRBpLw9vkq8O2kBNbPCy 4wIhANXcKDlG507bv5bOWYo92XDWuHd1FzyZfSLren9uFVfB -----END CERTIFICATE----- `) - // ECDSA256SHA1Cert is an ECDSA curve-P256 cert with a SHA1 signature algorithm + // ECDSA256SHA1Cert is an ECDSA curve-P256 CA cert with a SHA1 signature algorithm // that is current valid and expires in 2117. This should not be a valid cert because a SHA1 signature algorithm. ECDSA256SHA1Cert = []byte(` -----BEGIN CERTIFICATE----- @@ -236,7 +236,7 @@ AwEHoUQDQgAEebJ+AUku73gjI9h5i/+VoNR7pxdxlZkvrZ1be2brVhQNFBvnvMnG -----END EC PRIVATE KEY----- `) - // DSA2048Cert is a DSA cert with a 2048 key, SHA1 hash, that is currently valid and expires in 2117 + // DSA2048Cert is a DSA CA cert with a 2048 key, SHA1 hash, that is currently valid and expires in 2117 // This should not be a valid cert because we do not accept DSA keys. DSA2048Cert = []byte(` -----BEGIN CERTIFICATE-----