connect/ca: update Vault provider to add cross-signing methods

This commit is contained in:
Kyle Havlovitz 2018-06-15 16:25:53 -07:00 committed by Jack Pearkes
parent a97c44c1ba
commit 675555c4ff
3 changed files with 102 additions and 9 deletions

View File

@ -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

View File

@ -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

View File

@ -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)
}