diff --git a/agent/connect/ca/provider.go b/agent/connect/ca/provider.go index 6027fc62af..2d92baf80a 100644 --- a/agent/connect/ca/provider.go +++ b/agent/connect/ca/provider.go @@ -32,7 +32,12 @@ type Provider interface { // intemediate and any cross-signed intermediates managed by Consul. Sign(*x509.CertificateRequest) (string, error) - // CrossSignCA must accept a CA certificate signed by another CA's key + // GetCrossSigningCSR returns a CSR that can be signed by another root + // to create an intermediate cert that forms a chain of trust back to + // that root CA. + GetCrossSigningCSR() (*x509.CertificateRequest, error) + + // CrossSignCA must accept a CA CSR signed by another CA's key // and cross sign it exactly as it is such that it forms a chain back the the // CAProvider's current root. Specifically, the Distinguished Name, Subject // Alternative Name, SubjectKeyID and other relevant extensions must be kept. @@ -40,7 +45,7 @@ type Provider interface { // AuthorityKeyID set to the CAProvider's current signing key as well as the // Issuer related fields changed as necessary. The resulting certificate is // returned as a PEM formatted string. - CrossSignCA(*x509.Certificate) (string, error) + CrossSignCA(*x509.CertificateRequest) (string, error) // Cleanup performs any necessary cleanup that should happen when the provider // is shut down permanently, such as removing a temporary PKI backend in Vault diff --git a/agent/connect/ca/provider_vault.go b/agent/connect/ca/provider_vault.go index 0fbfd75ca1..5cd6203b76 100644 --- a/agent/connect/ca/provider_vault.go +++ b/agent/connect/ca/provider_vault.go @@ -181,7 +181,7 @@ func (v *VaultProvider) GenerateIntermediate() (string, error) { if err != nil { return "", err } - if csr == nil || csr.Data["csr"] == nil { + if csr == nil || csr.Data["csr"] == "" { return "", fmt.Errorf("got empty value when generating intermediate CSR") } @@ -193,7 +193,7 @@ func (v *VaultProvider) GenerateIntermediate() (string, error) { if err != nil { return "", err } - if intermediate == nil || intermediate.Data["certificate"] == nil { + if intermediate == nil || intermediate.Data["certificate"] == "" { return "", fmt.Errorf("got empty value when generating intermediate certificate") } @@ -240,14 +240,62 @@ func (v *VaultProvider) Sign(csr *x509.CertificateRequest) (string, error) { return fmt.Sprintf("%s\n%s", cert, ca), nil } -// todo(kyhavlov): decide which vault endpoint to use here -func (v *VaultProvider) CrossSignCA(cert *x509.Certificate) (string, error) { - var pemBuf bytes.Buffer - if err := pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw}); err != nil { +// GetCrossSigningCSR creates a CSR for an intermediate CA certificate to be signed +// by another CA provider. +func (v *VaultProvider) GetCrossSigningCSR() (*x509.CertificateRequest, error) { + // Generate a new intermediate CSR for the root to sign. + spiffeID := connect.SpiffeIDSigning{ClusterID: v.clusterId, Domain: "consul"} + csr, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/generate/internal", map[string]interface{}{ + "common_name": "Vault CA Intermediate Authority", + "uri_sans": spiffeID.URI().String(), + }) + if err != nil { + return nil, err + } + if csr == nil || csr.Data["csr"] == "" { + return nil, fmt.Errorf("got empty value when generating intermediate CSR") + } + + // Return the parsed CSR. + pem, ok := csr.Data["csr"].(string) + if !ok { + return nil, fmt.Errorf("CSR was not a string") + } + result, err := connect.ParseCSR(pem) + if err != nil { + return nil, err + } + + return result, nil +} + +// CrossSignCA creates a CSR from the given CA certificate and uses the +// configured root PKI backend to issue a cert from the request. +func (v *VaultProvider) CrossSignCA(cert *x509.CertificateRequest) (string, error) { + var csrBuf bytes.Buffer + err := pem.Encode(&csrBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: cert.Raw}) + if err != nil { return "", err } - return pemBuf.String(), nil + // Have the root PKI backend sign this CSR. + response, err := v.client.Logical().Write(v.config.RootPKIPath+"root/sign-intermediate", map[string]interface{}{ + "csr": csrBuf.String(), + "use_csr_values": true, + }) + if err != nil { + return "", fmt.Errorf("error having Vault cross-sign cert: %v", err) + } + if response == nil || response.Data["certificate"] == "" { + return "", fmt.Errorf("certificate info returned from Vault was blank") + } + + xcCert, ok := response.Data["certificate"].(string) + if !ok { + return "", fmt.Errorf("certificate was not a string") + } + + return xcCert, nil } // Cleanup unmounts the configured intermediate PKI backend. It's fine to tear diff --git a/agent/connect/ca/provider_vault_test.go b/agent/connect/ca/provider_vault_test.go index fc5552e616..8c48fd2e55 100644 --- a/agent/connect/ca/provider_vault_test.go +++ b/agent/connect/ca/provider_vault_test.go @@ -148,3 +148,43 @@ func TestVaultCAProvider_SignLeaf(t *testing.T) { require.True(parsed.NotBefore.Before(time.Now())) } } + +func TestVaultCAProvider_CrossSignCA(t *testing.T) { + t.Parallel() + + require := require.New(t) + provider1, core1, listener1 := testVaultCluster(t) + defer core1.Shutdown() + defer listener1.Close() + + provider2, core2, listener2 := testVaultCluster(t) + defer core2.Shutdown() + defer listener2.Close() + + // Have provider2 generate a cross-signing CSR + csr, err := provider2.GetCrossSigningCSR() + require.NoError(err) + oldSubject := csr.Subject.CommonName + + // Have the provider cross sign our new CA cert. + xcPEM, err := provider1.CrossSignCA(csr) + require.NoError(err) + xc, err := connect.ParseCert(xcPEM) + require.NoError(err) + + rootPEM, err := provider1.ActiveRoot() + require.NoError(err) + root, err := connect.ParseCert(rootPEM) + require.NoError(err) + + // AuthorityKeyID should be the signing root's, SubjectKeyId should be different. + require.Equal(root.AuthorityKeyId, xc.AuthorityKeyId) + require.NotEqual(root.SubjectKeyId, xc.SubjectKeyId) + + // Subject name should not have changed. + require.NotEqual(root.Subject.CommonName, xc.Subject.CommonName) + require.Equal(oldSubject, xc.Subject.CommonName) + + // Issuer should be the signing root. + require.Equal(root.Issuer.CommonName, xc.Issuer.CommonName) +}