diff --git a/ca/certificates.go b/ca/certificates.go index da60839636..88f05d3da5 100644 --- a/ca/certificates.go +++ b/ca/certificates.go @@ -111,18 +111,25 @@ type LocalSigner struct { // Key will only be used by the original manager to put the private // key-material in raft, no signing operations depend on it. Key []byte + + // Cert is one PEM encoded Certificate used as the signing CA. It must correspond to the key. + Cert []byte + + // just cached parsed values for validation, etc. + parsedCert *x509.Certificate + cryptoSigner crypto.Signer } // 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.Cert: [CA cert1][CA cert2] // RootCA.Intermediates: [intermediate CA1][intermediate CA2][intermediate CA3] -// RootCA.Signer.Key: [signing CA key] +// RootCA.signer.Cert: [signing CA cert] +// 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 +// - [signing CA key] must be the private key for [signing CA cert], and either both or none must be provided // // - [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 @@ -135,10 +142,29 @@ type LocalSigner struct { // 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 // +// Example 1: Simple root rotation +// - Initial state: +// - RootCA.Cert: [Root CA1 self-signed] +// - RootCA.Intermediates: [] +// - RootCA.signer.Cert: [Root CA1 self-signed] +// - Issued TLS cert: [leaf signed by Root CA1] +// +// - Intermediate state (during root rotation): +// - RootCA.Cert: [Root CA1 self-signed] +// - RootCA.Intermediates: [Root CA2 signed by Root CA1] +// - RootCA.signer.Cert: [Root CA2 signed by Root CA1] +// - Issued TLS cert: [leaf signed by Root CA2][Root CA2 signed by Root CA1] +// +// - Final state: +// - RootCA.Cert: [Root CA2 self-signed] +// - RootCA.Intermediates: [] +// - RootCA.signer.Cert: [Root CA2 self-signed] +// - Issued TLS cert: [leaf signed by Root CA2] +// 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 + // Certs contains a bundle of self-signed, PEM encoded certificates for the Root CA to be used + // as the root of trust. + Certs []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 @@ -153,16 +179,16 @@ type RootCA struct { Digest digest.Digest // This signer will be nil if the node doesn't have the appropriate key material - Signer *LocalSigner + signer *LocalSigner } -// CanSign ensures that the signer has all three necessary elements needed to operate -func (rca *RootCA) CanSign() bool { - if rca.Cert == nil || rca.Pool == nil || rca.Signer == nil { - return false +// Signer is an accessor for the local signer that returns an error if this root cannot sign. +func (rca *RootCA) Signer() (*LocalSigner, error) { + if rca.Pool == nil || rca.signer == nil || len(rca.signer.Cert) == 0 || rca.signer.Signer == nil { + return nil, ErrNoValidSigner } - return true + return rca.signer, nil } // IssueAndSaveNewCertificates generates a new key-pair, signs it with the local root-ca, and returns a @@ -173,10 +199,6 @@ func (rca *RootCA) IssueAndSaveNewCertificates(kw KeyWriter, cn, ou, org string) return nil, errors.Wrap(err, "error when generating new node certs") } - if !rca.CanSign() { - return nil, ErrNoValidSigner - } - // Obtain a signed Certificate certChain, err := rca.ParseValidateAndSignCSR(csr, cn, ou, org) if err != nil { @@ -322,13 +344,12 @@ func PrepareCSR(csrBytes []byte, cn, ou, org string) cfsigner.SignRequest { // ParseValidateAndSignCSR returns a signed certificate from a particular rootCA and a CSR. func (rca *RootCA) ParseValidateAndSignCSR(csrBytes []byte, cn, ou, org string) ([]byte, error) { - if !rca.CanSign() { - return nil, ErrNoValidSigner - } - signRequest := PrepareCSR(csrBytes, cn, ou, org) - - cert, err := rca.Signer.Sign(signRequest) + signer, err := rca.Signer() + if err != nil { + return nil, err + } + cert, err := signer.Sign(signRequest) if err != nil { return nil, errors.Wrap(err, "failed to sign node certificate") } @@ -338,20 +359,12 @@ func (rca *RootCA) ParseValidateAndSignCSR(csrBytes []byte, cn, ou, org string) // 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) + signer, err := rca.Signer() 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") + return nil, err } + // create a new cert with exactly the same parameters, including the public key and exact NotBefore and NotAfter newCert, err := helpers.ParseCertificatePEM(otherCAPEM) if err != nil { return nil, errors.New("could not parse new CA certificate") @@ -361,7 +374,7 @@ func (rca *RootCA) CrossSignCACertificate(otherCAPEM []byte) ([]byte, error) { return nil, errors.New("certificate not a CA") } - derBytes, err := x509.CreateCertificate(cryptorand.Reader, newCert, rootCert, newCert.PublicKey, rootSigner) + derBytes, err := x509.CreateCertificate(cryptorand.Reader, newCert, signer.parsedCert, newCert.PublicKey, signer.cryptoSigner) if err != nil { return nil, errors.Wrap(err, "could not cross-sign new CA certificate using old CA material") } @@ -372,28 +385,34 @@ func (rca *RootCA) CrossSignCACertificate(otherCAPEM []byte) ([]byte, error) { }), nil } +func validateSignatureAlgorithm(cert *x509.Certificate) error { + switch cert.SignatureAlgorithm { + case x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA, x509.ECDSAWithSHA256, x509.ECDSAWithSHA384, x509.ECDSAWithSHA512: + return nil + default: + return fmt.Errorf("unsupported signature algorithm: %s", cert.SignatureAlgorithm.String()) + } +} + // 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, intermediates []byte) (RootCA, error) { +func NewRootCA(rootCertBytes, signCertBytes, signKeyBytes []byte, certExpiry time.Duration, intermediates []byte) (RootCA, error) { // Parse all the certificates in the cert bundle - parsedCerts, err := helpers.ParseCertificatesPEM(certBytes) + parsedCerts, err := helpers.ParseCertificatesPEM(rootCertBytes) if err != nil { - return RootCA{}, err + return RootCA{}, errors.Wrap(err, "invalid root certificates") } // Check to see if we have at least one valid cert if len(parsedCerts) < 1 { - return RootCA{}, errors.New("no valid Root CA certificates found") + return RootCA{}, errors.New("no valid root CA certificates found") } // Create a Pool with all of the certificates found pool := x509.NewCertPool() for _, cert := range parsedCerts { - switch cert.SignatureAlgorithm { - case x509.SHA256WithRSA, x509.SHA384WithRSA, x509.SHA512WithRSA, x509.ECDSAWithSHA256, x509.ECDSAWithSHA384, x509.ECDSAWithSHA512: - break - default: - return RootCA{}, fmt.Errorf("unsupported signature algorithm: %s", cert.SignatureAlgorithm.String()) + if err := validateSignatureAlgorithm(cert); err != nil { + return RootCA{}, err } // Check to see if all of the certificates are valid, self-signed root CA certs selfpool := x509.NewCertPool() @@ -405,81 +424,42 @@ func NewRootCA(certBytes, keyBytes []byte, certExpiry time.Duration, intermediat } // 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). + digest := digest.FromBytes(rootCertBytes) + + // The intermediates supplied must be able to chain up to the root certificates, so that when they are appended to + // a leaf certificate, the leaf certificate can be validated through the intermediates to the root certificates. + var intermediatePool *x509.CertPool + var parsedIntermediates []*x509.Certificate if len(intermediates) > 0 { - parsedIntermediates, err := ValidateCertChain(pool, intermediates, false) + 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") + intermediatePool = x509.NewCertPool() + for _, cert := range parsedIntermediates { + intermediatePool.AddCert(cert) } } - if len(keyBytes) == 0 { - // This RootCA does not have a valid signer - return RootCA{Cert: certBytes, Intermediates: intermediates, Digest: digest, Pool: pool}, nil - } - - var ( - passphraseStr string - passphrase, passphrasePrev []byte - priv crypto.Signer - ) - - // Attempt two distinct passphrases, so we can do a hitless passphrase rotation - if passphraseStr = os.Getenv(PassphraseENVVar); passphraseStr != "" { - passphrase = []byte(passphraseStr) - } - - if p := os.Getenv(PassphraseENVVarPrev); p != "" { - passphrasePrev = []byte(p) - } - - // Attempt to decrypt the current private-key with the passphrases provided - priv, err = helpers.ParsePrivateKeyPEMWithPassword(keyBytes, passphrase) - if err != nil { - priv, err = helpers.ParsePrivateKeyPEMWithPassword(keyBytes, passphrasePrev) + var localSigner *LocalSigner + if len(signKeyBytes) != 0 || len(signCertBytes) != 0 { + localSigner, err = newLocalSigner(signKeyBytes, signCertBytes, certExpiry, pool, intermediatePool) if err != nil { - return RootCA{}, errors.Wrap(err, "malformed private key") + return RootCA{}, err } - } - // We will always use the first certificate inside of the root bundle as the active one - if err := ensureCertKeyMatch(parsedCerts[0], priv.Public()); err != nil { - return RootCA{}, err - } - - signer, err := local.NewSigner(priv, parsedCerts[0], cfsigner.DefaultSigAlgo(priv), SigningPolicy(certExpiry)) - if err != nil { - return RootCA{}, err - } - - // If the key was loaded from disk unencrypted, but there is a passphrase set, - // ensure it is encrypted, so it doesn't hit raft in plain-text - keyBlock, _ := pem.Decode(keyBytes) - if keyBlock == nil { - // This RootCA does not have a valid signer. - return RootCA{Cert: certBytes, Digest: digest, Pool: pool}, nil - } - if passphraseStr != "" && !x509.IsEncryptedPEMBlock(keyBlock) { - keyBytes, err = EncryptECPrivateKey(keyBytes, passphraseStr) - if err != nil { - return RootCA{}, err + // If a signer is provided and there are intermediates, then either the first intermediate would be the signer CA + // certificate (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 CA certificate (which could be either an + // intermediate cert or a self-signed root cert). + if len(parsedIntermediates) > 0 && (!bytes.Equal(parsedIntermediates[0].RawSubject, localSigner.parsedCert.RawSubject) || + !bytes.Equal(parsedIntermediates[0].RawSubjectPublicKeyInfo, localSigner.parsedCert.RawSubjectPublicKeyInfo)) { + return RootCA{}, errors.New( + "invalid intermediate chain - the first intermediate must have the same subject and public key as the signing cert") } } - return RootCA{Signer: &LocalSigner{Signer: signer, Key: keyBytes}, Intermediates: intermediates, Digest: digest, Cert: certBytes, Pool: pool}, nil + return RootCA{signer: localSigner, Intermediates: intermediates, Digest: digest, Certs: rootCertBytes, Pool: pool}, nil } // ValidateCertChain checks checks that the certificates provided chain up to the root pool provided. In addition @@ -579,6 +559,78 @@ func ValidateCertChain(rootPool *x509.CertPool, certs []byte, allowExpired bool) return parsedCerts, nil } +// newLocalSigner validates the signing cert and signing key to create a local signer, which accepts a crypto signer and a cert +func newLocalSigner(keyBytes, certBytes []byte, certExpiry time.Duration, rootPool, intermediatePool *x509.CertPool) (*LocalSigner, error) { + if len(keyBytes) == 0 || len(certBytes) == 0 { + return nil, errors.New("must provide both a signing key and a signing cert, or neither") + } + + parsedCerts, err := helpers.ParseCertificatesPEM(certBytes) + if err != nil { + return nil, errors.Wrap(err, "invalid signing CA cert") + } + if len(parsedCerts) == 0 { + return nil, errors.New("no valid signing CA certificates found") + } + if err := validateSignatureAlgorithm(parsedCerts[0]); err != nil { + return nil, err + } + opts := x509.VerifyOptions{ + Roots: rootPool, + Intermediates: intermediatePool, + } + if _, err := parsedCerts[0].Verify(opts); err != nil { + return nil, errors.Wrap(err, "error while validating signing CA certificate against roots and intermediates") + } + + var ( + passphraseStr string + passphrase, passphrasePrev []byte + priv crypto.Signer + ) + + // Attempt two distinct passphrases, so we can do a hitless passphrase rotation + if passphraseStr = os.Getenv(PassphraseENVVar); passphraseStr != "" { + passphrase = []byte(passphraseStr) + } + + if p := os.Getenv(PassphraseENVVarPrev); p != "" { + passphrasePrev = []byte(p) + } + + // Attempt to decrypt the current private-key with the passphrases provided + priv, err = helpers.ParsePrivateKeyPEMWithPassword(keyBytes, passphrase) + if err != nil { + priv, err = helpers.ParsePrivateKeyPEMWithPassword(keyBytes, passphrasePrev) + if err != nil { + return nil, errors.Wrap(err, "malformed private key") + } + } + + // We will always use the first certificate inside of the root bundle as the active one + if err := ensureCertKeyMatch(parsedCerts[0], priv.Public()); err != nil { + return nil, err + } + + signer, err := local.NewSigner(priv, parsedCerts[0], cfsigner.DefaultSigAlgo(priv), SigningPolicy(certExpiry)) + if err != nil { + return nil, err + } + + // If the key was loaded from disk unencrypted, but there is a passphrase set, + // ensure it is encrypted, so it doesn't hit raft in plain-text + // we don't have to check for nil, because if we couldn't pem-decode the bytes, then parsing above would have failed + keyBlock, _ := pem.Decode(keyBytes) + if passphraseStr != "" && !x509.IsEncryptedPEMBlock(keyBlock) { + keyBytes, err = EncryptECPrivateKey(keyBytes, passphraseStr) + if err != nil { + return nil, errors.Wrap(err, "unable to encrypt signing CA key material") + } + } + + return &LocalSigner{Cert: certBytes, Key: keyBytes, Signer: signer, parsedCert: parsedCerts[0], cryptoSigner: priv}, nil +} + func ensureCertKeyMatch(cert *x509.Certificate, key crypto.PublicKey) error { switch certPub := cert.PublicKey.(type) { case *rsa.PublicKey: @@ -620,6 +672,7 @@ func GetLocalRootCA(paths CertPaths) (RootCA, error) { return RootCA{}, err } + signingCert := cert key, err := ioutil.ReadFile(paths.Key) if err != nil { @@ -629,9 +682,10 @@ func GetLocalRootCA(paths CertPaths) (RootCA, error) { // There may not be a local key. It's okay to pass in a nil // key. We'll get a root CA without a signer. key = nil + signingCert = nil } - return NewRootCA(cert, key, DefaultNodeCertExpiration, nil) + return NewRootCA(cert, signingCert, key, DefaultNodeCertExpiration, nil) } func getGRPCConnection(creds credentials.TransportCredentials, connBroker *connectionbroker.Broker, forceRemote bool) (*connectionbroker.Conn, error) { @@ -686,7 +740,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, nil) + return NewRootCA(response.Certificate, nil, nil, DefaultNodeCertExpiration, nil) } // CreateRootCA creates a Certificate authority for a new Swarm Cluster, potentially @@ -705,7 +759,7 @@ func CreateRootCA(rootCN string, paths CertPaths) (RootCA, error) { return RootCA{}, err } - rootCA, err := NewRootCA(cert, key, DefaultNodeCertExpiration, nil) + rootCA, err := NewRootCA(cert, cert, key, DefaultNodeCertExpiration, nil) if err != nil { return RootCA{}, err } @@ -826,7 +880,7 @@ func saveRootCA(rootCA RootCA, paths CertPaths) error { } // If the root certificate got returned successfully, save the rootCA to disk. - return ioutils.AtomicWriteFile(paths.Cert, rootCA.Cert, 0644) + return ioutils.AtomicWriteFile(paths.Cert, rootCA.Certs, 0644) } // GenerateNewCSR returns a newly generated key and CSR signed with said key diff --git a/ca/certificates_test.go b/ca/certificates_test.go index aee5e4d188..a8f14fcc1a 100644 --- a/ca/certificates_test.go +++ b/ca/certificates_test.go @@ -90,7 +90,7 @@ func TestCreateRootCAExpiry(t *testing.T) { assert.NoError(t, err) // Convert the certificate into an object to create a RootCA - parsedCert, err := helpers.ParseCertificatePEM(rootCA.Cert) + parsedCert, err := helpers.ParseCertificatePEM(rootCA.Certs) assert.NoError(t, err) duration, err := time.ParseDuration(ca.RootCAExpiration) assert.NoError(t, err) @@ -111,21 +111,24 @@ func TestGetLocalRootCA(t *testing.T) { // Create the local Root CA to ensure that we can reload it correctly. rootCA, err := ca.CreateRootCA("rootCN", paths.RootCA) - assert.True(t, rootCA.CanSign()) + assert.NoError(t, err) + s, err := rootCA.Signer() assert.NoError(t, err) // No private key here rootCA2, err := ca.GetLocalRootCA(paths.RootCA) assert.NoError(t, err) - assert.Equal(t, rootCA.Cert, rootCA2.Cert) - assert.False(t, rootCA2.CanSign()) + assert.Equal(t, rootCA.Certs, rootCA2.Certs) + _, err = rootCA2.Signer() + assert.Equal(t, err, ca.ErrNoValidSigner) // write private key and assert we can load it and sign - assert.NoError(t, ioutil.WriteFile(paths.RootCA.Key, rootCA.Signer.Key, os.FileMode(0600))) + assert.NoError(t, ioutil.WriteFile(paths.RootCA.Key, s.Key, os.FileMode(0600))) rootCA3, err := ca.GetLocalRootCA(paths.RootCA) assert.NoError(t, err) - assert.Equal(t, rootCA.Cert, rootCA3.Cert) - assert.True(t, rootCA3.CanSign()) + assert.Equal(t, rootCA.Certs, rootCA3.Certs) + _, err = rootCA3.Signer() + assert.NoError(t, err) // Try with a private key that does not match the CA cert public key. privKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader) @@ -251,7 +254,7 @@ func TestGetRemoteCA(t *testing.T) { defer tc.Stop() shaHash := sha256.New() - shaHash.Write(tc.RootCA.Cert) + shaHash.Write(tc.RootCA.Certs) md := shaHash.Sum(nil) mdStr := hex.EncodeToString(md) @@ -260,7 +263,7 @@ func TestGetRemoteCA(t *testing.T) { downloadedRootCA, err := ca.GetRemoteCA(tc.Context, d, tc.ConnBroker) require.NoError(t, err) - require.Equal(t, downloadedRootCA.Cert, tc.RootCA.Cert) + require.Equal(t, downloadedRootCA.Certs, tc.RootCA.Certs) // update the test CA to include a multi-certificate bundle as the root - the digest // we use to verify with must be the digest of the whole bundle @@ -272,11 +275,13 @@ func TestGetRemoteCA(t *testing.T) { otherRootCA, err := ca.CreateRootCA("other", paths.RootCA) require.NoError(t, err) - comboCertBundle := append(tc.RootCA.Cert, otherRootCA.Cert...) + comboCertBundle := append(tc.RootCA.Certs, otherRootCA.Certs...) + s, err := tc.RootCA.Signer() + require.NoError(t, err) require.NoError(t, tc.MemoryStore.Update(func(tx store.Tx) error { cluster := store.GetCluster(tx, tc.Organization) cluster.RootCA.CACert = comboCertBundle - cluster.RootCA.CAKey = tc.RootCA.Signer.Key + cluster.RootCA.CAKey = s.Key return store.UpdateCluster(tx, cluster) })) require.NoError(t, raftutils.PollFunc(nil, func() error { @@ -293,7 +298,7 @@ func TestGetRemoteCA(t *testing.T) { d = digest.FromBytes(comboCertBundle) downloadedRootCA, err = ca.GetRemoteCA(tc.Context, d, tc.ConnBroker) require.NoError(t, err) - require.Equal(t, comboCertBundle, downloadedRootCA.Cert) + require.Equal(t, comboCertBundle, downloadedRootCA.Certs) require.Equal(t, 2, len(downloadedRootCA.Pool.Subjects())) for _, rootCA := range []ca.RootCA{tc.RootCA, otherRootCA} { @@ -315,15 +320,6 @@ func TestGetRemoteCA(t *testing.T) { } } -func TestCanSign(t *testing.T) { - tc := testutils.NewTestCA(t) - defer tc.Stop() - - assert.True(t, tc.RootCA.CanSign()) - tc.RootCA.Signer = nil - assert.False(t, tc.RootCA.CanSign()) -} - func TestGetRemoteCAInvalidHash(t *testing.T) { tc := testutils.NewTestCA(t) defer tc.Stop() @@ -337,7 +333,7 @@ func TestRequestAndSaveNewCertificates(t *testing.T) { defer tc.Stop() // Copy the current RootCA without the signer - rca := ca.RootCA{Cert: tc.RootCA.Cert, Pool: tc.RootCA.Pool} + rca := ca.RootCA{Certs: tc.RootCA.Certs, Pool: tc.RootCA.Pool} cert, err := rca.RequestAndSaveNewCertificates(tc.Context, tc.KeyReadWriter, ca.CertificateRequestConfig{ Token: tc.ManagerToken, @@ -540,13 +536,12 @@ 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, nil) + rootCA, err := ca.NewRootCA(pair.cert, 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) - // require.Equal(t, bytes.TrimSpace(pair.cert), bytes.TrimSpace(rootCA.Cert)) - // require.Equal(t, bytes.TrimSpace(pair.key), bytes.TrimSpace(rootCA.Signer.Key)) - require.NotNil(t, rootCA.Signer) + require.Equal(t, pair.cert, rootCA.Certs) + s, err := rootCA.Signer() + require.NoError(t, err) + require.Equal(t, pair.key, s.Key) _, err = rootCA.Digest.Verifier().Write(pair.cert) require.NoError(t, err) } @@ -566,15 +561,17 @@ func TestNewRootCABundle(t *testing.T) { // make a second root CA firstRootCA, err := ca.CreateRootCA("rootCN1", paths.RootCA) assert.NoError(t, err) + s, err := firstRootCA.Signer() + require.NoError(t, err) // Overwrite the bytes of the second Root CA with the bundle, creating a valid 2 cert bundle - bundle := append(firstRootCA.Cert, secondRootCA.Cert...) + bundle := append(firstRootCA.Certs, secondRootCA.Certs...) err = ioutil.WriteFile(paths.RootCA.Cert, bundle, 0644) assert.NoError(t, err) - newRootCA, err := ca.NewRootCA(bundle, firstRootCA.Signer.Key, ca.DefaultNodeCertExpiration, nil) + newRootCA, err := ca.NewRootCA(bundle, firstRootCA.Certs, s.Key, ca.DefaultNodeCertExpiration, nil) assert.NoError(t, err) - assert.Equal(t, bundle, newRootCA.Cert) + assert.Equal(t, bundle, newRootCA.Certs) assert.Equal(t, 2, len(newRootCA.Pool.Subjects())) // If I use newRootCA's IssueAndSaveNewCertificates to sign certs, I'll get the correct CA in the chain @@ -596,8 +593,10 @@ func TestNewRootCANonDefaultExpiry(t *testing.T) { rootCA, err := ca.CreateRootCA("rootCN", paths.RootCA) assert.NoError(t, err) + s, err := rootCA.Signer() + require.NoError(t, err) - newRootCA, err := ca.NewRootCA(rootCA.Cert, rootCA.Signer.Key, 1*time.Hour, nil) + newRootCA, err := ca.NewRootCA(rootCA.Certs, rootCA.Certs, s.Key, 1*time.Hour, nil) assert.NoError(t, err) // Create and sign a new CSR @@ -614,7 +613,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, nil) + newRootCA, err = ca.NewRootCA(rootCA.Certs, rootCA.Certs, s.Key, 59*time.Minute, nil) assert.NoError(t, err) cert, err = newRootCA.ParseValidateAndSignCSR(csr, "CN", ca.ManagerRole, "ORG") @@ -628,70 +627,177 @@ func TestNewRootCANonDefaultExpiry(t *testing.T) { } type invalidNewRootCATestCase struct { - cert []byte - key []byte - intermediates []byte - errorStr string + roots, cert, key, intermediates []byte + errorStr string } func TestNewRootCAInvalidCertAndKeys(t *testing.T) { + 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)) + invalids := []invalidNewRootCATestCase{ + // invalid root or signer cert { + roots: []byte("malformed"), + cert: testutils.ECDSA256SHA256Cert, + key: testutils.ECDSA256Key, + errorStr: "Failed to decode certificate", + }, + { + roots: testutils.ECDSA256SHA256Cert, cert: []byte("malformed"), key: testutils.ECDSA256Key, errorStr: "Failed to decode certificate", }, { + roots: []byte(" "), + cert: testutils.ECDSA256SHA256Cert, + key: testutils.ECDSA256Key, + errorStr: "no valid root CA certificates found", + }, + { + roots: testutils.ECDSA256SHA256Cert, + cert: []byte(" "), + key: testutils.ECDSA256Key, + errorStr: "no valid signing CA certificates found", + }, + { + roots: testutils.NotYetValidCert, + cert: testutils.ECDSA256SHA256Cert, + key: testutils.ECDSA256Key, + errorStr: "not yet valid", + }, + { + roots: testutils.ECDSA256SHA256Cert, cert: testutils.NotYetValidCert, key: testutils.NotYetValidKey, errorStr: "not yet valid", }, { - cert: testutils.ExpiredCert, - key: testutils.ExpiredKey, + roots: testutils.ExpiredCert, + cert: testutils.ECDSA256SHA256Cert, + key: testutils.ECDSA256Key, + errorStr: "expired", + }, + { + roots: testutils.ExpiredCert, + cert: testutils.ECDSA256SHA256Cert, + key: testutils.ECDSA256Key, errorStr: "expired", }, { + roots: testutils.RSA2048SHA1Cert, + cert: testutils.ECDSA256SHA256Cert, + key: testutils.ECDSA256Key, + errorStr: "unsupported signature algorithm", + }, + { + roots: testutils.ECDSA256SHA256Cert, cert: testutils.RSA2048SHA1Cert, key: testutils.RSA2048Key, errorStr: "unsupported signature algorithm", }, { + roots: testutils.ECDSA256SHA256Cert, cert: testutils.ECDSA256SHA1Cert, key: testutils.ECDSA256Key, errorStr: "unsupported signature algorithm", }, { + roots: testutils.ECDSA256SHA1Cert, + cert: testutils.ECDSA256SHA256Cert, + key: testutils.ECDSA256Key, + errorStr: "unsupported signature algorithm", + }, + { + roots: testutils.ECDSA256SHA256Cert, + cert: testutils.DSA2048Cert, + key: testutils.DSA2048Key, + errorStr: "unsupported signature algorithm", + }, + { + roots: testutils.DSA2048Cert, + cert: testutils.ECDSA256SHA256Cert, + key: testutils.ECDSA256Key, + errorStr: "unsupported signature algorithm", + }, + // invalid signer + { + roots: testutils.ECDSA256SHA256Cert, cert: testutils.ECDSA256SHA256Cert, key: []byte("malformed"), errorStr: "malformed private key", }, { + roots: testutils.RSA1024Cert, cert: testutils.RSA1024Cert, key: testutils.RSA1024Key, errorStr: "unsupported RSA key parameters", }, { + roots: testutils.ECDSA224Cert, cert: testutils.ECDSA224Cert, key: testutils.ECDSA224Key, errorStr: "unsupported ECDSA key parameters", }, { + roots: testutils.ECDSA256SHA256Cert, cert: testutils.ECDSA256SHA256Cert, key: testutils.ECDSA224Key, errorStr: "certificate key mismatch", }, { - cert: testutils.DSA2048Cert, - key: testutils.DSA2048Key, - errorStr: "unsupported signature algorithm", + roots: testutils.ECDSA256SHA256Cert, + cert: testutils.ECDSACertChain[1], + key: testutils.ECDSACertChainKeys[1], + errorStr: "unknown authority", // signer cert doesn't chain up to the root + }, + // invalid intermediates + { + roots: testutils.ECDSACertChain[2], + cert: testutils.ECDSACertChain[1], + key: testutils.ECDSACertChainKeys[1], + intermediates: []byte("malformed"), + errorStr: "Failed to decode certificate", + }, + { + roots: testutils.ECDSACertChain[2], + cert: testutils.ECDSACertChain[1], + key: testutils.ECDSACertChainKeys[1], + intermediates: expiredIntermediate, + errorStr: "expired", + }, + { + roots: testutils.ECDSACertChain[2], + cert: testutils.ECDSACertChain[1], + key: testutils.ECDSACertChainKeys[1], + intermediates: notYetValidIntermediate, + errorStr: "expired", + }, + { + roots: testutils.ECDSACertChain[2], + cert: testutils.ECDSACertChain[1], + key: testutils.ECDSACertChainKeys[1], + intermediates: append(testutils.ECDSACertChain[1], testutils.ECDSA256SHA256Cert...), + errorStr: "do not form a chain", + }, + { + roots: testutils.ECDSACertChain[2], + cert: testutils.ECDSACertChain[1], + key: testutils.ECDSACertChainKeys[1], + intermediates: testutils.ECDSA256SHA256Cert, + errorStr: "unknown authority", // intermediates don't chain up to root }, } - for _, invalid := range invalids { - _, err := ca.NewRootCA(invalid.cert, invalid.key, ca.DefaultNodeCertExpiration, nil) - require.Error(t, err, invalid.errorStr) - require.Contains(t, err.Error(), invalid.errorStr) + for i, invalid := range invalids { + _, err := ca.NewRootCA(invalid.roots, invalid.cert, invalid.key, ca.DefaultNodeCertExpiration, invalid.intermediates) + require.Error(t, err, fmt.Sprintf("expected error containing: \"%s\", test case (%d)", invalid.errorStr, i)) + require.Contains(t, err.Error(), invalid.errorStr, fmt.Sprintf("%d", i)) } } @@ -700,13 +806,6 @@ func TestRootCAWithCrossSignedIntermediates(t *testing.T) { 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) @@ -719,67 +818,55 @@ func TestRootCAWithCrossSignedIntermediates(t *testing.T) { 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]...)) + signWithIntermediate, err := ca.NewRootCA(testutils.ECDSACertChain[2], testutils.ECDSACertChain[1], 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]) + signWithIntermediate, err = ca.NewRootCA(testutils.ECDSACertChain[2], testutils.ECDSACertChain[1], 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") + _, err = signWithIntermediate.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) + parsedCerts, err := ca.ValidateCertChain(signWithIntermediate.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) + oldRoot, err := ca.NewRootCA(testutils.ECDSACertChain[2], testutils.ECDSACertChain[2], testutils.ECDSACertChainKeys[2], ca.DefaultNodeCertExpiration, nil) require.NoError(t, err) - parsedCerts, err = ca.ValidateCertChain(oldRoot.Pool, tlsCert, false) + newRoot, err := ca.NewRootCA(fauxRootCert, fauxRootCert, testutils.ECDSACertChainKeys[1], ca.DefaultNodeCertExpiration, nil) require.NoError(t, err) - require.Len(t, parsedCerts, 2) - require.Equal(t, parsedIntermediate.Raw, parsedCerts[1].Raw) + + for _, root := range []ca.RootCA{signWithIntermediate, oldRoot, newRoot} { + parsedCerts, err = ca.ValidateCertChain(root.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) + tc := testutils.NewTestCAFromRootCA(t, tempdir, newRoot, nil) defer tc.Stop() - secConfig, err := newRoot.CreateSecurityConfig(context.Background(), krw, ca.CertificateRequestConfig{}) + // we need creds that trust both the old and new root in order to connect to the test CA, and we want this root CA to + // append certificates + connectToExternalRootCA, err := ca.NewRootCA(append(testutils.ECDSACertChain[2], fauxRootCert...), testutils.ECDSACertChain[1], + testutils.ECDSACertChainKeys[1], ca.DefaultNodeCertExpiration, testutils.ECDSACertChain[1]) + require.NoError(t, err) + secConfig, err := connectToExternalRootCA.CreateSecurityConfig(context.Background(), krw, ca.CertificateRequestConfig{}) require.NoError(t, err) externalCA := secConfig.ExternalCA() @@ -791,10 +878,12 @@ func TestRootCAWithCrossSignedIntermediates(t *testing.T) { 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) + for _, root := range []ca.RootCA{signWithIntermediate, oldRoot, newRoot} { + parsedCerts, err = ca.ValidateCertChain(root.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) { @@ -808,45 +897,51 @@ func TestNewRootCAWithPassphrase(t *testing.T) { rootCA, err := ca.CreateRootCA("rootCN", paths.RootCA) assert.NoError(t, err) + rcaSigner, err := rootCA.Signer() + assert.NoError(t, err) // 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, nil) + newRootCA, err := ca.NewRootCA(rootCA.Certs, rcaSigner.Cert, rcaSigner.Key, ca.DefaultNodeCertExpiration, nil) + assert.NoError(t, err) + nrcaSigner, err := newRootCA.Signer() assert.NoError(t, err) - assert.NotEqual(t, rootCA.Signer.Key, newRootCA.Signer.Key) - assert.Equal(t, rootCA.Cert, newRootCA.Cert) - assert.NotContains(t, string(rootCA.Signer.Key), string(newRootCA.Signer.Key)) - assert.Contains(t, string(newRootCA.Signer.Key), "Proc-Type: 4,ENCRYPTED") + assert.NotEqual(t, rcaSigner.Key, nrcaSigner.Key) + assert.Equal(t, rootCA.Certs, newRootCA.Certs) + assert.NotContains(t, string(rcaSigner.Key), string(nrcaSigner.Key)) + assert.Contains(t, string(nrcaSigner.Key), "Proc-Type: 4,ENCRYPTED") // 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, nil) + anotherNewRootCA, err := ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.Key, ca.DefaultNodeCertExpiration, nil) + assert.NoError(t, err) + anrcaSigner, err := anotherNewRootCA.Signer() assert.NoError(t, err) assert.Equal(t, newRootCA, anotherNewRootCA) - assert.NotContains(t, string(rootCA.Signer.Key), string(anotherNewRootCA.Signer.Key)) - assert.Contains(t, string(anotherNewRootCA.Signer.Key), "Proc-Type: 4,ENCRYPTED") + assert.NotContains(t, string(rcaSigner.Key), string(anrcaSigner.Key)) + assert.Contains(t, string(anrcaSigner.Key), "Proc-Type: 4,ENCRYPTED") // 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, nil) + anotherNewRootCA, err = ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.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, nil) + anotherNewRootCA, err = ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.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, nil) + anotherNewRootCA, err = ca.NewRootCA(newRootCA.Certs, nrcaSigner.Cert, nrcaSigner.Key, ca.DefaultNodeCertExpiration, nil) assert.NoError(t, err) assert.Equal(t, newRootCA, anotherNewRootCA) - assert.NotContains(t, string(rootCA.Signer.Key), string(anotherNewRootCA.Signer.Key)) - assert.Contains(t, string(anotherNewRootCA.Signer.Key), "Proc-Type: 4,ENCRYPTED") + assert.NotContains(t, string(rcaSigner.Key), string(anrcaSigner.Key)) + assert.Contains(t, string(anrcaSigner.Key), "Proc-Type: 4,ENCRYPTED") } type certTestCase struct { @@ -999,13 +1094,13 @@ func TestRootCACrossSignCACertificate(t *testing.T) { cert1, key1, err := testutils.CreateRootCertAndKey("rootCN") require.NoError(t, err) - rootCA1, err := ca.NewRootCA(cert1, key1, ca.DefaultNodeCertExpiration, nil) + rootCA1, err := ca.NewRootCA(cert1, 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, nil) + rootCA2, err := ca.NewRootCA(cert2, 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 8a10ebadfb..ca08950580 100644 --- a/ca/config.go +++ b/ca/config.go @@ -132,7 +132,13 @@ func (s *SecurityConfig) UpdateRootCA(cert, key []byte, certExpiry time.Duration s.mu.Lock() defer s.mu.Unlock() - rootCA, err := NewRootCA(cert, key, certExpiry, nil) + // If we have no signing key, then we shouldn't pass a signing cert either (because we don't want a local + // signer at all) + signingCert := cert + if len(key) == 0 { + signingCert = nil + } + rootCA, err := NewRootCA(cert, signingCert, key, certExpiry, nil) if err != nil { return err } @@ -359,31 +365,14 @@ type CertificateRequestConfig struct { func (rootCA RootCA) CreateSecurityConfig(ctx context.Context, krw *KeyReadWriter, config CertificateRequestConfig) (*SecurityConfig, error) { ctx = log.WithModule(ctx, "tls") - var ( - tlsKeyPair *tls.Certificate - err error - ) - - if rootCA.CanSign() { - // Create a new random ID for this certificate - cn := identity.NewID() - org := identity.NewID() - - proposedRole := ManagerRole - tlsKeyPair, err = rootCA.IssueAndSaveNewCertificates(krw, cn, proposedRole, org) - if err != nil { - log.G(ctx).WithFields(logrus.Fields{ - "node.id": cn, - "node.role": proposedRole, - }).WithError(err).Errorf("failed to issue and save new certificate") - return nil, err - } + // Create a new random ID for this certificate + cn := identity.NewID() + org := identity.NewID() - log.G(ctx).WithFields(logrus.Fields{ - "node.id": cn, - "node.role": proposedRole, - }).Debug("issued new TLS certificate") - } else { + proposedRole := ManagerRole + tlsKeyPair, err := rootCA.IssueAndSaveNewCertificates(krw, cn, proposedRole, org) + switch errors.Cause(err) { + case ErrNoValidSigner: // Request certificate issuance from a remote CA. // Last argument is nil because at this point we don't have any valid TLS creds tlsKeyPair, err = rootCA.RequestAndSaveNewCertificates(ctx, krw, config) @@ -391,7 +380,19 @@ func (rootCA RootCA) CreateSecurityConfig(ctx context.Context, krw *KeyReadWrite log.G(ctx).WithError(err).Error("failed to request save new certificate") return nil, err } + case nil: + log.G(ctx).WithFields(logrus.Fields{ + "node.id": cn, + "node.role": proposedRole, + }).Debug("issued new TLS certificate") + default: + log.G(ctx).WithFields(logrus.Fields{ + "node.id": cn, + "node.role": proposedRole, + }).WithError(err).Errorf("failed to issue and save new certificate") + return nil, err } + // Create the Server TLS Credentials for this node. These will not be used by workers. serverTLSCreds, err := rootCA.NewServerTLSCredentials(tlsKeyPair) if err != nil { diff --git a/ca/config_test.go b/ca/config_test.go index f05d0e70ec..7d937566d7 100644 --- a/ca/config_test.go +++ b/ca/config_test.go @@ -35,10 +35,10 @@ func TestDownloadRootCASuccess(t *testing.T) { rootCA, err := ca.DownloadRootCA(tc.Context, tc.Paths.RootCA, tc.WorkerToken, tc.ConnBroker) require.NoError(t, err) require.NotNil(t, rootCA.Pool) - require.NotNil(t, rootCA.Cert) - require.Nil(t, rootCA.Signer) - require.False(t, rootCA.CanSign()) - require.Equal(t, tc.RootCA.Cert, rootCA.Cert) + require.NotNil(t, rootCA.Certs) + _, err = rootCA.Signer() + require.Equal(t, err, ca.ErrNoValidSigner) + require.Equal(t, tc.RootCA.Certs, rootCA.Certs) // Remove the CA cert os.RemoveAll(tc.Paths.RootCA.Cert) @@ -47,10 +47,10 @@ func TestDownloadRootCASuccess(t *testing.T) { rootCA, err = ca.DownloadRootCA(tc.Context, tc.Paths.RootCA, "", tc.ConnBroker) require.NoError(t, err) require.NotNil(t, rootCA.Pool) - require.NotNil(t, rootCA.Cert) - require.Nil(t, rootCA.Signer) - require.False(t, rootCA.CanSign()) - require.Equal(t, tc.RootCA.Cert, rootCA.Cert) + require.NotNil(t, rootCA.Certs) + _, err = rootCA.Signer() + require.Equal(t, err, ca.ErrNoValidSigner) + require.Equal(t, tc.RootCA.Certs, rootCA.Certs) } func TestDownloadRootCAWrongCAHash(t *testing.T) { @@ -140,17 +140,19 @@ func TestCreateSecurityConfigNoCerts(t *testing.T) { func TestLoadSecurityConfigExpiredCert(t *testing.T) { tc := testutils.NewTestCA(t) defer tc.Stop() + s, err := tc.RootCA.Signer() + require.NoError(t, err) krw := ca.NewKeyReadWriter(tc.Paths.Node, nil, nil) now := time.Now() - _, err := tc.RootCA.IssueAndSaveNewCertificates(krw, "cn", "ou", "org") + _, err = tc.RootCA.IssueAndSaveNewCertificates(krw, "cn", "ou", "org") require.NoError(t, err) certBytes, _, err := krw.Read() require.NoError(t, err) // A cert that is not yet valid is not valid even if expiry is allowed - invalidCert := testutils.ReDateCert(t, certBytes, tc.RootCA.Cert, tc.RootCA.Signer.Key, now.Add(time.Hour), now.Add(time.Hour*2)) + invalidCert := testutils.ReDateCert(t, certBytes, tc.RootCA.Certs, s.Key, now.Add(time.Hour), now.Add(time.Hour*2)) require.NoError(t, ioutil.WriteFile(tc.Paths.Node.Cert, invalidCert, 0700)) _, err = ca.LoadSecurityConfig(tc.Context, tc.RootCA, krw, false) @@ -162,7 +164,7 @@ func TestLoadSecurityConfigExpiredCert(t *testing.T) { require.IsType(t, x509.CertificateInvalidError{}, errors.Cause(err)) // a cert that is expired is not valid if expiry is not allowed - invalidCert = testutils.ReDateCert(t, certBytes, tc.RootCA.Cert, tc.RootCA.Signer.Key, now.Add(-2*time.Minute), now.Add(-1*time.Minute)) + invalidCert = testutils.ReDateCert(t, certBytes, tc.RootCA.Certs, s.Key, now.Add(-2*time.Minute), now.Add(-1*time.Minute)) require.NoError(t, ioutil.WriteFile(tc.Paths.Node.Cert, invalidCert, 0700)) _, err = ca.LoadSecurityConfig(tc.Context, tc.RootCA, krw, false) @@ -245,7 +247,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, nil) + rootCA, err := ca.NewRootCA(testutils.ECDSACertChain[2], nil, nil, ca.DefaultNodeCertExpiration, nil) require.NoError(t, err) // loading the incomplete chain fails @@ -288,7 +290,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, nil) + rootCA, err := ca.NewRootCA(cert, cert, key, ca.DefaultNodeCertExpiration, nil) require.NoError(t, err) tempdir, err := ioutil.TempDir("", "test-security-config-update") @@ -333,9 +335,11 @@ func TestSecurityConfigUpdateRootCA(t *testing.T) { req := ca.PrepareCSR(csr, "cn", ca.ManagerRole, secConfig.ClientTLSCreds.Organization()) externalServer := tc.ExternalSigningServer + tcSigner, err := tc.RootCA.Signer() + require.NoError(t, err) 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, nil) + updatedRoot, err := ca.NewRootCA(append(tc.RootCA.Certs, cert...), tcSigner.Cert, tcSigner.Key, ca.DefaultNodeCertExpiration, nil) require.NoError(t, err) externalServer, err = testutils.NewExternalSigningServer(updatedRoot, tc.TempDir) require.NoError(t, err) @@ -350,7 +354,9 @@ func TestSecurityConfigUpdateRootCA(t *testing.T) { // update the root CA on the "original"" security config to support both the old root // and the "new root" (the testing CA root) - err = secConfig.UpdateRootCA(append(rootCA.Cert, tc.RootCA.Cert...), rootCA.Signer.Key, ca.DefaultNodeCertExpiration) + rSigner, err := rootCA.Signer() + require.NoError(t, err) + err = secConfig.UpdateRootCA(append(rootCA.Certs, tc.RootCA.Certs...), rSigner.Key, ca.DefaultNodeCertExpiration) require.NoError(t, err) // can now connect to the test CA using our modified security config, and can cannect to our server using @@ -397,10 +403,10 @@ func TestRenewTLSConfigUpdateRootCARace(t *testing.T) { go func() { defer close(done1) var key []byte - if rootCA.Signer != nil { - key = rootCA.Signer.Key + if signer, err := rootCA.Signer(); err == nil { + key = signer.Key } - require.NoError(t, secConfig.UpdateRootCA(append(rootCA.Cert, cert...), key, ca.DefaultNodeCertExpiration)) + require.NoError(t, secConfig.UpdateRootCA(append(rootCA.Certs, cert...), key, ca.DefaultNodeCertExpiration)) }() go func() { @@ -428,6 +434,8 @@ func TestRenewTLSConfigWorker(t *testing.T) { tc := testutils.NewTestCA(t) defer tc.Stop() + s, err := tc.RootCA.Signer() + require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -439,9 +447,11 @@ 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, nil) + newRootCA, err := ca.NewRootCA(tc.RootCA.Certs, s.Cert, s.Key, ca.DefaultNodeCertExpiration, nil) assert.NoError(t, err) - newRootCA.Signer.SetPolicy(&cfconfig.Signing{ + newSigner, err := newRootCA.Signer() + require.NoError(t, err) + newSigner.SetPolicy(&cfconfig.Signing{ Default: &cfconfig.SigningProfile{ Usage: []string{"signing", "key encipherment", "server auth", "client auth"}, Expiry: 6 * time.Minute, @@ -482,6 +492,8 @@ func TestRenewTLSConfigManager(t *testing.T) { tc := testutils.NewTestCA(t) defer tc.Stop() + s, err := tc.RootCA.Signer() + assert.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -493,9 +505,11 @@ 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, nil) + newRootCA, err := ca.NewRootCA(tc.RootCA.Certs, s.Cert, s.Key, ca.DefaultNodeCertExpiration, nil) + assert.NoError(t, err) + newSigner, err := newRootCA.Signer() assert.NoError(t, err) - newRootCA.Signer.SetPolicy(&cfconfig.Signing{ + newSigner.SetPolicy(&cfconfig.Signing{ Default: &cfconfig.SigningProfile{ Usage: []string{"signing", "key encipherment", "server auth", "client auth"}, Expiry: 6 * time.Minute, @@ -538,6 +552,8 @@ func TestRenewTLSConfigWithNoNode(t *testing.T) { tc := testutils.NewTestCA(t) defer tc.Stop() + s, err := tc.RootCA.Signer() + require.NoError(t, err) ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -549,9 +565,11 @@ 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, nil) + newRootCA, err := ca.NewRootCA(tc.RootCA.Certs, s.Cert, s.Key, ca.DefaultNodeCertExpiration, nil) + assert.NoError(t, err) + newSigner, err := newRootCA.Signer() assert.NoError(t, err) - newRootCA.Signer.SetPolicy(&cfconfig.Signing{ + newSigner.SetPolicy(&cfconfig.Signing{ Default: &cfconfig.SigningProfile{ Usage: []string{"signing", "key encipherment", "server auth", "client auth"}, Expiry: 6 * time.Minute, diff --git a/ca/external.go b/ca/external.go index 7dd8a7c8be..e4edeac2ee 100644 --- a/ca/external.go +++ b/ca/external.go @@ -16,7 +16,6 @@ import ( "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" @@ -107,20 +106,14 @@ func (eca *ExternalCA) Sign(ctx context.Context, req signer.SignRequest) (cert [ // 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 + rcaSigner, err := rca.Signer() + if err != nil { + return nil, err + } + rootCert := rcaSigner.parsedCert cfCSRObj := csr.ExtractCertificateRequest(rootCert) der, err := x509.CreateCertificateRequest(cryptorand.Reader, &x509.CertificateRequest{ @@ -132,7 +125,7 @@ func (eca *ExternalCA) CrossSignRootCA(ctx context.Context, rca RootCA) ([]byte, DNSNames: rootCert.DNSNames, EmailAddresses: rootCert.EmailAddresses, IPAddresses: rootCert.IPAddresses, - }, rootSigner) + }, rcaSigner.cryptoSigner) if err != nil { return nil, err } diff --git a/ca/external_test.go b/ca/external_test.go index 5e34767eec..a8fbf9f1c0 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, nil) + rootCA2, err := ca.NewRootCA(cert2, cert2, key2, ca.DefaultNodeCertExpiration, nil) require.NoError(t, err) krw := ca.NewKeyReadWriter(paths.Node, nil, nil) diff --git a/ca/server.go b/ca/server.go index 562fcdd0e4..b89011fd93 100644 --- a/ca/server.go +++ b/ca/server.go @@ -361,7 +361,7 @@ func (s *Server) GetRootCACertificate(ctx context.Context, request *api.GetRootC }) return &api.GetRootCACertificateResponse{ - Certificate: s.securityConfig.RootCA().Cert, + Certificate: s.securityConfig.RootCA().Certs, }, nil } diff --git a/ca/testutils/cautils.go b/ca/testutils/cautils.go index b6bb6895fb..961f114cf2 100644 --- a/ca/testutils/cautils.go +++ b/ca/testutils/cautils.go @@ -15,9 +15,7 @@ import ( cfcsr "github.com/cloudflare/cfssl/csr" "github.com/cloudflare/cfssl/helpers" "github.com/cloudflare/cfssl/initca" - "github.com/cloudflare/cfssl/log" cfsigner "github.com/cloudflare/cfssl/signer" - "github.com/cloudflare/cfssl/signer/local" "github.com/docker/swarmkit/api" "github.com/docker/swarmkit/ca" "github.com/docker/swarmkit/connectionbroker" @@ -26,8 +24,6 @@ import ( "github.com/docker/swarmkit/manager/state" "github.com/docker/swarmkit/manager/state/store" "github.com/docker/swarmkit/remotes" - "github.com/opencontainers/go-digest" - "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/net/context" @@ -292,7 +288,11 @@ func genSecurityConfig(s *store.MemoryStore, rootCA ca.RootCA, krw *ca.KeyReadWr hosts = append(hosts, ca.CARole) } - cert, err := rootCA.Signer.Sign(cfsigner.SignRequest{ + signer, err := rootCA.Signer() + if err != nil { + return nil, err + } + cert, err := signer.Sign(cfsigner.SignRequest{ Request: string(csr), // OU is used for Authentication of the node type. The CN has the random // node ID. @@ -305,7 +305,7 @@ func genSecurityConfig(s *store.MemoryStore, rootCA ca.RootCA, krw *ca.KeyReadWr } // Append the root CA Key to the certificate, to create a valid chain - certChain := append(cert, rootCA.Cert...) + certChain := append(cert, rootCA.Certs...) // If we were instructed to persist the files if tmpDir != "" { @@ -341,7 +341,7 @@ func genSecurityConfig(s *store.MemoryStore, rootCA ca.RootCA, krw *ca.KeyReadWr if nonSigningRoot { rootCA = ca.RootCA{ - Cert: rootCA.Cert, + Certs: rootCA.Certs, Digest: rootCA.Digest, Pool: rootCA.Pool, } @@ -395,26 +395,11 @@ func createAndWriteRootCA(rootCN string, paths ca.CertPaths, expiry time.Duratio return ca.RootCA{}, err } - // Convert the key given by initca to an object to create a ca.RootCA - parsedKey, err := helpers.ParsePrivateKeyPEM(key) - if err != nil { - log.Errorf("failed to parse private key: %v", err) - return ca.RootCA{}, err - } - - // Convert the certificate into an object to create a ca.RootCA - parsedCert, err := helpers.ParseCertificatePEM(cert) + rootCA, err := ca.NewRootCA(cert, cert, key, ca.DefaultNodeCertExpiration, nil) if err != nil { return ca.RootCA{}, err } - // Create a Signer out of the private key - signer, err := local.NewSigner(parsedKey, parsedCert, cfsigner.DefaultSigAlgo(parsedKey), ca.SigningPolicy(expiry)) - if err != nil { - log.Errorf("failed to create signer: %v", err) - return ca.RootCA{}, err - } - // Ensure directory exists err = os.MkdirAll(filepath.Dir(paths.Cert), 0755) if err != nil { @@ -428,22 +413,7 @@ func createAndWriteRootCA(rootCN string, paths ca.CertPaths, expiry time.Duratio if err := ioutils.AtomicWriteFile(paths.Key, key, 0600); err != nil { return ca.RootCA{}, err } - - // Create a Pool with our Root CA Certificate - pool := x509.NewCertPool() - if !pool.AppendCertsFromPEM(cert) { - return ca.RootCA{}, errors.New("failed to append certificate to cert pool") - } - - return ca.RootCA{ - Signer: &ca.LocalSigner{ - Signer: signer, - Key: key, - }, - Cert: cert, - Pool: pool, - Digest: digest.FromBytes(cert), - }, nil + return rootCA, nil } // ReDateCert takes an existing cert and changes the not before and not after date, to make it easier diff --git a/ca/testutils/externalutils.go b/ca/testutils/externalutils.go index d8f57a4530..f1605d4031 100644 --- a/ca/testutils/externalutils.go +++ b/ca/testutils/externalutils.go @@ -36,6 +36,11 @@ func NewExternalSigningServer(rootCA ca.RootCA, basedir string) (*ExternalSignin serverCN := "external-ca-example-server" serverOU := "localhost" // Make a valid server cert for localhost. + s, err := rootCA.Signer() + if err != nil { + return nil, err + } + // Create TLS credentials for the external CA server which we will run. serverPaths := ca.CertPaths{ Cert: filepath.Join(basedir, "server.crt"), @@ -72,9 +77,9 @@ func NewExternalSigningServer(rootCA ca.RootCA, basedir string) (*ExternalSignin mux := http.NewServeMux() handler := &signHandler{ - numIssued: &ess.NumIssued, - rootCA: rootCA, - flaky: &ess.flaky, + numIssued: &ess.NumIssued, + leafSigner: s, + flaky: &ess.flaky, } mux.Handle(signURL.Path, handler) ess.handler = handler @@ -118,13 +123,11 @@ func (ess *ExternalSigningServer) EnableCASigning() error { ess.handler.mu.Lock() defer ess.handler.mu.Unlock() - rca := ess.handler.rootCA - - rootCert, err := helpers.ParseCertificatePEM(rca.Cert) + rootCert, err := helpers.ParseCertificatePEM(ess.handler.leafSigner.Cert) if err != nil { return errors.Wrap(err, "could not parse old CA certificate") } - rootSigner, err := helpers.ParsePrivateKeyPEM(rca.Signer.Key) + rootSigner, err := helpers.ParsePrivateKeyPEM(ess.handler.leafSigner.Key) if err != nil { return errors.Wrap(err, "could not parse old CA key") } @@ -154,11 +157,11 @@ func (ess *ExternalSigningServer) DisableCASigning() { } type signHandler struct { - mu sync.Mutex - numIssued *uint64 - rootCA ca.RootCA - flaky *uint32 - caSigner signer.Signer + mu sync.Mutex + numIssued *uint64 + flaky *uint32 + leafSigner *ca.LocalSigner + caSigner signer.Signer } func (h *signHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -255,7 +258,7 @@ func (h *signHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } // Sign the requested leaf certificate. - certPEM, err = h.rootCA.Signer.Sign(signReq) + certPEM, err = h.leafSigner.Sign(signReq) } if err != nil { cfsslErr := cfsslerrors.New(cfsslerrors.APIClientError, cfsslerrors.ServerRequestFailed) diff --git a/cmd/external-ca-example/main.go b/cmd/external-ca-example/main.go index 1942da8dfa..0d972b737f 100644 --- a/cmd/external-ca-example/main.go +++ b/cmd/external-ca-example/main.go @@ -39,7 +39,7 @@ func main() { // And copy the Root CA certificate into the node config path for its // CA. - ioutil.WriteFile(nodeConfigPaths.RootCA.Cert, rootCA.Cert, os.FileMode(0644)) + ioutil.WriteFile(nodeConfigPaths.RootCA.Cert, rootCA.Certs, os.FileMode(0644)) server, err := testutils.NewExternalSigningServer(rootCA, "ca") if err != nil { diff --git a/integration/integration_test.go b/integration/integration_test.go index d0f442b55f..7735844674 100644 --- a/integration/integration_test.go +++ b/integration/integration_test.go @@ -517,7 +517,9 @@ func TestForceNewCluster(t *testing.T) { require.NoError(t, err) now := time.Now() // we don't want it too expired, because it can't have expired before the root CA cert is valid - expiredCertPEM := testutils.ReDateCert(t, certBytes, rootCA.Cert, rootCA.Signer.Key, now.Add(-1*time.Hour), now.Add(-1*time.Second)) + rootSigner, err := rootCA.Signer() + require.NoError(t, err) + expiredCertPEM := testutils.ReDateCert(t, certBytes, rootSigner.Cert, rootSigner.Key, now.Add(-1*time.Hour), now.Add(-1*time.Second)) // restart node with an expired certificate while forcing a new cluster - it should start without error and the certificate should be renewed nodeID := leader.node.NodeID() diff --git a/integration/node.go b/integration/node.go index d7afcf7405..1d8f787a45 100644 --- a/integration/node.go +++ b/integration/node.go @@ -47,19 +47,24 @@ func newTestNode(joinAddr, joinToken string, lateBind bool, rootCA *ca.RootCA) ( cfg.ListenRemoteAPI = "127.0.0.1:0" } if rootCA != nil { + signer, err := rootCA.Signer() + if err != nil { + return nil, err + } certDir := filepath.Join(tmpDir, "certificates") if err := os.MkdirAll(certDir, 0700); err != nil { return nil, err } certPaths := ca.NewConfigPaths(certDir) - if err := ioutil.WriteFile(certPaths.RootCA.Cert, rootCA.Cert, 0644); err != nil { + if err := ioutil.WriteFile(certPaths.RootCA.Cert, signer.Cert, 0644); err != nil { return nil, err } - if err := ioutil.WriteFile(certPaths.RootCA.Key, rootCA.Signer.Key, 0600); err != nil { + if err := ioutil.WriteFile(certPaths.RootCA.Key, signer.Key, 0600); err != nil { return nil, err } // generate TLS certs for this manager for bootstrapping, else the node will generate its own CA - _, err := rootCA.IssueAndSaveNewCertificates(ca.NewKeyReadWriter(certPaths.Node, nil, nil), + _, err = rootCA.IssueAndSaveNewCertificates( + ca.NewKeyReadWriter(certPaths.Node, nil, nil), identity.NewID(), ca.ManagerRole, identity.NewID()) if err != nil { return nil, err diff --git a/manager/manager.go b/manager/manager.go index 1f2cdf93f3..b02dd5f594 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -1033,8 +1033,8 @@ func defaultClusterObject( initialUnlockKeys []*api.EncryptionKey, rootCA *ca.RootCA) *api.Cluster { var caKey []byte - if rootCA.Signer != nil { - caKey = rootCA.Signer.Key + if rcaSigner, err := rootCA.Signer(); err == nil { + caKey = rcaSigner.Key } return &api.Cluster{ @@ -1055,7 +1055,7 @@ func defaultClusterObject( }, RootCA: api.RootCA{ CAKey: caKey, - CACert: rootCA.Cert, + CACert: rootCA.Certs, CACertHash: rootCA.Digest.String(), JoinTokens: api.JoinTokens{ Worker: ca.GenerateJoinToken(rootCA), diff --git a/manager/manager_test.go b/manager/manager_test.go index c43b219213..561fd7cba0 100644 --- a/manager/manager_test.go +++ b/manager/manager_test.go @@ -485,7 +485,7 @@ func TestManagerUpdatesSecurityConfig(t *testing.T) { newRootCert, _, err := testutils.CreateRootCertAndKey("rootOther") require.NoError(t, err) - updatedCA := append(tc.RootCA.Cert, newRootCert...) + updatedCA := append(tc.RootCA.Certs, newRootCert...) // Update the RootCA to have a bundle require.NoError(t, m.raftNode.MemoryStore().Update(func(tx store.Tx) error { @@ -496,7 +496,7 @@ func TestManagerUpdatesSecurityConfig(t *testing.T) { // wait for the manager's security config to be updated require.NoError(t, raftutils.PollFuncWithTimeout(nil, func() error { - if !bytes.Equal(managerSecurityConfig.RootCA().Cert, updatedCA) { + if !bytes.Equal(managerSecurityConfig.RootCA().Certs, updatedCA) { return fmt.Errorf("root CA not updated yet") } return nil diff --git a/node/node_test.go b/node/node_test.go index b5741d1e05..cf18208218 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -70,7 +70,7 @@ func TestLoadSecurityConfigPartialCertsOnDisk(t *testing.T) { require.NoError(t, err) // a new CA was generated because no existing TLS certs were present - require.NotEqual(t, rootCA.Cert, securityConfig.RootCA().Cert) + require.NotEqual(t, rootCA.Certs, securityConfig.RootCA().Certs) // if the TLS key and cert are on disk, but there's no CA, a new CA and TLS // key+cert are generated