diff --git a/agent/connect/ca/provider.go b/agent/connect/ca/provider.go index a812d64113..0a1df39dec 100644 --- a/agent/connect/ca/provider.go +++ b/agent/connect/ca/provider.go @@ -22,6 +22,16 @@ type Provider interface { // ActiveIntermediate() ActiveRoot() (string, error) + // GenerateIntermediateCSR generates a CSR for an intermediate CA + // certificate, to be signed by the root of another datacenter. If isRoot was + // set to true with Configure(), calling this is an error. + GenerateIntermediateCSR() (string, error) + + // SetIntermediate sets the provider to use the given intermediate certificate + // as well as the root it was signed by. This completes the initialization for + // a provider where isRoot was set to false in Configure(). + SetIntermediate(intermediatePEM, rootPEM string) error + // ActiveIntermediate returns the current signing cert used by this provider // for generating SPIFFE leaf certs. Note that this must not change except // when Consul requests the change via GenerateIntermediate. Changing the @@ -41,6 +51,12 @@ type Provider interface { // intemediate and any cross-signed intermediates managed by Consul. Sign(*x509.CertificateRequest) (string, error) + // SignIntermediate will validate the CSR to ensure the trust domain in the + // URI SAN matches the local one and that basic constraints for a CA certificate + // are met. It should return a signed CA certificate with a path length constraint + // of 0 to ensure that the certificate cannot be used to generate further CA certs. + SignIntermediate(*x509.CertificateRequest) (string, error) + // CrossSignCA must accept a CA certificate from another CA provider // 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 diff --git a/agent/connect/ca/provider_consul.go b/agent/connect/ca/provider_consul.go index 803ce2ac18..8971d5cd99 100644 --- a/agent/connect/ca/provider_consul.go +++ b/agent/connect/ca/provider_consul.go @@ -25,9 +25,12 @@ var ErrNotInitialized = errors.New("provider not initialized") type ConsulProvider struct { Delegate ConsulProviderStateDelegate - config *structs.ConsulCAProviderConfig - id string - isRoot bool + config *structs.ConsulCAProviderConfig + id string + clusterID string + isRoot bool + spiffeID *connect.SpiffeIDSigning + sync.RWMutex } @@ -44,9 +47,11 @@ func (c *ConsulProvider) Configure(clusterID string, isRoot bool, rawConfig map[ return err } c.config = config - c.isRoot = isRoot hash := sha256.Sum256([]byte(fmt.Sprintf("%s,%s,%v", config.PrivateKey, config.RootCert, isRoot))) c.id = strings.Replace(fmt.Sprintf("% x", hash), " ", ":", -1) + c.clusterID = clusterID + c.isRoot = isRoot + c.spiffeID = connect.SpiffeIDSigningForCluster(&structs.CAConfiguration{ClusterID: clusterID}) // Exit early if the state store has an entry for this provider's config. _, providerState, err := c.Delegate.State().CAProviderState(c.id) @@ -107,8 +112,7 @@ func (c *ConsulProvider) Configure(clusterID string, isRoot bool, rawConfig map[ // ActiveRoot returns the active root CA certificate. func (c *ConsulProvider) ActiveRoot() (string, error) { - state := c.Delegate.State() - _, providerState, err := state.CAProviderState(c.id) + _, providerState, err := c.getState() if err != nil { return "", err } @@ -119,15 +123,11 @@ func (c *ConsulProvider) ActiveRoot() (string, error) { // GenerateRoot initializes a new root certificate and private key // if needed. func (c *ConsulProvider) GenerateRoot() error { - state := c.Delegate.State() - idx, providerState, err := state.CAProviderState(c.id) + idx, providerState, err := c.getState() if err != nil { return err } - if providerState == nil { - return ErrNotInitialized - } if !c.isRoot { return fmt.Errorf("provider is not the root certificate authority") } @@ -170,10 +170,129 @@ func (c *ConsulProvider) GenerateRoot() error { return nil } +// GenerateIntermediateCSR creates a private key and generates a CSR +// for another datacenter's root to sign. +func (c *ConsulProvider) GenerateIntermediateCSR() (string, error) { + _, providerState, err := c.getState() + if err != nil { + return "", err + } + + if c.isRoot { + return "", fmt.Errorf("provider is the root certificate authority, " + + "cannot generate an intermediate CSR") + } + + // Create a new private key and CSR. + signer, pk, err := connect.GeneratePrivateKey() + if err != nil { + return "", err + } + + csr, err := connect.CreateCACSR(c.spiffeID, signer) + if err != nil { + return "", err + } + + // Write the new provider state to the store. + newState := *providerState + newState.PrivateKey = pk + args := &structs.CARequest{ + Op: structs.CAOpSetProviderState, + ProviderState: &newState, + } + if err := c.Delegate.ApplyCARequest(args); err != nil { + return "", err + } + + return csr, nil +} + +// SetIntermediate validates that the given intermediate is for the right private key +// and writes the given intermediate and root certificates to the state. +func (c *ConsulProvider) SetIntermediate(intermediatePEM, rootPEM string) error { + _, providerState, err := c.getState() + if err != nil { + return err + } + + if c.isRoot { + return fmt.Errorf("cannot set an intermediate using another root in the primary datacenter") + } + + // Get the key from the incoming intermediate cert so we can compare it + // to the currently stored key. + intermediate, err := connect.ParseCert(intermediatePEM) + if err != nil { + return fmt.Errorf("error parsing intermediate PEM: %v", err) + } + privKey, err := connect.ParseSigner(providerState.PrivateKey) + if err != nil { + return err + } + + // Compare the two keys to make sure they match. + b1, err := x509.MarshalPKIXPublicKey(intermediate.PublicKey) + if err != nil { + return err + } + b2, err := x509.MarshalPKIXPublicKey(privKey.Public()) + if err != nil { + return err + } + if !bytes.Equal(b1, b2) { + return fmt.Errorf("intermediate cert is for a different private key") + } + + // Validate the remaining fields and make sure the intermediate validates against + // the given root cert. + if !intermediate.IsCA { + return fmt.Errorf("intermediate is not a CA certificate") + } + if uriCount := len(intermediate.URIs); uriCount != 1 { + return fmt.Errorf("incoming intermediate cert has unexpected number of URIs: %d", uriCount) + } + if got, want := intermediate.URIs[0].String(), c.spiffeID.URI().String(); got != want { + return fmt.Errorf("incoming cert URI %q does not match current URI: %q", got, want) + } + + pool := x509.NewCertPool() + pool.AppendCertsFromPEM([]byte(rootPEM)) + _, err = intermediate.Verify(x509.VerifyOptions{ + Roots: pool, + }) + if err != nil { + return fmt.Errorf("could not verify intermediate cert against root: %v", err) + } + + // Update the state + newState := *providerState + newState.IntermediateCert = intermediatePEM + newState.RootCert = rootPEM + args := &structs.CARequest{ + Op: structs.CAOpSetProviderState, + ProviderState: &newState, + } + if err := c.Delegate.ApplyCARequest(args); err != nil { + return err + } + + return nil +} + // We aren't maintaining separate root/intermediate CAs for the builtin // provider, so just return the root. func (c *ConsulProvider) ActiveIntermediate() (string, error) { - return c.ActiveRoot() + if c.isRoot { + return c.ActiveRoot() + } + + _, providerState, err := c.getState() + if err != nil { + return "", err + } + + return providerState.IntermediateCert, nil } // We aren't maintaining separate root/intermediate CAs for the builtin @@ -216,7 +335,7 @@ func (c *ConsulProvider) Sign(csr *x509.CertificateRequest) (string, error) { return "", err } if signer == nil { - return "", fmt.Errorf("error signing cert: Consul CA not initialized yet") + return "", ErrNotInitialized } keyId, err := connect.KeyId(signer.Public()) if err != nil { @@ -234,7 +353,11 @@ func (c *ConsulProvider) Sign(csr *x509.CertificateRequest) (string, error) { } // Parse the CA cert - caCert, err := connect.ParseCert(providerState.RootCert) + certPEM, err := c.ActiveIntermediate() + if err != nil { + return "", err + } + caCert, err := connect.ParseCert(certPEM) if err != nil { return "", fmt.Errorf("error parsing CA cert: %s", err) } @@ -290,6 +413,93 @@ func (c *ConsulProvider) Sign(csr *x509.CertificateRequest) (string, error) { return buf.String(), nil } +// SignIntermediate will validate the CSR to ensure the trust domain in the +// URI SAN matches the local one and that basic constraints for a CA certificate +// are met. It should return a signed CA certificate with a path length constraint +// of 0 to ensure that the certificate cannot be used to generate further CA certs. +func (c *ConsulProvider) SignIntermediate(csr *x509.CertificateRequest) (string, error) { + idx, providerState, err := c.getState() + if err != nil { + return "", err + } + + if uriCount := len(csr.URIs); uriCount != 1 { + return "", fmt.Errorf("incoming CSR has unexpected number of URIs: %d", uriCount) + } + certURI, err := connect.ParseCertURI(csr.URIs[0]) + if err != nil { + return "", err + } + + // Verify that the trust domain is valid. + if !c.spiffeID.CanSign(certURI) { + return "", fmt.Errorf("incoming CSR domain %q is not valid for our domain %q", + certURI.URI().String(), c.spiffeID.URI().String()) + } + + // Get the signing private key. + signer, err := connect.ParseSigner(providerState.PrivateKey) + if err != nil { + return "", err + } + subjectKeyId, err := connect.KeyId(csr.PublicKey) + if err != nil { + return "", err + } + + // Parse the CA cert + caCert, err := connect.ParseCert(providerState.RootCert) + if err != nil { + return "", fmt.Errorf("error parsing CA cert: %s", err) + } + + // Cert template for generation + sn := &big.Int{} + sn.SetUint64(idx + 1) + // Sign the certificate valid from 1 minute in the past, this helps it be + // accepted right away even when nodes are not in close time sync accross the + // cluster. A minute is more than enough for typical DC clock drift. + effectiveNow := time.Now().Add(-1 * time.Minute) + template := x509.Certificate{ + SerialNumber: sn, + Subject: csr.Subject, + URIs: csr.URIs, + Signature: csr.Signature, + SignatureAlgorithm: csr.SignatureAlgorithm, + PublicKeyAlgorithm: csr.PublicKeyAlgorithm, + PublicKey: csr.PublicKey, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | + x509.KeyUsageCRLSign | + x509.KeyUsageDigitalSignature, + IsCA: true, + MaxPathLenZero: true, + NotAfter: effectiveNow.Add(365 * 24 * time.Hour), + NotBefore: effectiveNow, + SubjectKeyId: subjectKeyId, + } + + // Create the certificate, PEM encode it and return that value. + var buf bytes.Buffer + bs, err := x509.CreateCertificate( + rand.Reader, &template, caCert, csr.PublicKey, signer) + if err != nil { + return "", fmt.Errorf("error generating certificate: %s", err) + } + err = pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: bs}) + if err != nil { + return "", fmt.Errorf("error encoding certificate: %s", err) + } + + err = c.incrementProviderIndex(providerState) + if err != nil { + return "", err + } + + // Set the response + return buf.String(), nil +} + // CrossSignCA returns the given CA cert signed by the current active root. func (c *ConsulProvider) CrossSignCA(cert *x509.Certificate) (string, error) { c.Lock() @@ -356,6 +566,22 @@ func (c *ConsulProvider) CrossSignCA(cert *x509.Certificate) (string, error) { return buf.String(), nil } +// getState returns the current provider state from the state delegate, and returns +// ErrNotInitialized if no entry is found. +func (c *ConsulProvider) getState() (uint64, *structs.CAConsulProviderState, error) { + state := c.Delegate.State() + idx, providerState, err := state.CAProviderState(c.id) + if err != nil { + return 0, nil, err + } + + if providerState == nil { + return 0, nil, ErrNotInitialized + } + + return idx, providerState, nil +} + // incrementProviderIndex does a write to increment the provider state store table index // used for serial numbers when generating certificates. func (c *ConsulProvider) incrementProviderIndex(providerState *structs.CAConsulProviderState) error { diff --git a/agent/connect/ca/provider_consul_test.go b/agent/connect/ca/provider_consul_test.go index 26f044e30a..9352e698d8 100644 --- a/agent/connect/ca/provider_consul_test.go +++ b/agent/connect/ca/provider_consul_test.go @@ -275,6 +275,75 @@ func testCrossSignProviders(t *testing.T, provider1, provider2 Provider) { } } +func TestConsulProvider_SignIntermediate(t *testing.T) { + t.Parallel() + require := require.New(t) + + conf1 := testConsulCAConfig() + delegate1 := newMockDelegate(t, conf1) + provider1 := &ConsulProvider{Delegate: delegate1} + require.NoError(provider1.Configure(conf1.ClusterID, true, conf1.Config)) + require.NoError(provider1.GenerateRoot()) + + conf2 := testConsulCAConfig() + conf2.CreateIndex = 10 + delegate2 := newMockDelegate(t, conf2) + provider2 := &ConsulProvider{Delegate: delegate2} + require.NoError(provider2.Configure(conf2.ClusterID, false, conf2.Config)) + + testSignIntermediateCrossDC(t, provider1, provider2) +} + +func testSignIntermediateCrossDC(t *testing.T, provider1, provider2 Provider) { + require := require.New(t) + + // Get the intermediate CSR from provider2. + csrPEM, err := provider2.GenerateIntermediateCSR() + require.NoError(err) + csr, err := connect.ParseCSR(csrPEM) + require.NoError(err) + + // Sign the CSR with provider1. + intermediatePEM, err := provider1.SignIntermediate(csr) + require.NoError(err) + rootPEM, err := provider1.ActiveRoot() + require.NoError(err) + + // Give the new intermediate to provider2 to use. + require.NoError(provider2.SetIntermediate(intermediatePEM, rootPEM)) + + // Have provider2 sign a leaf cert and make sure the chain is correct. + spiffeService := &connect.SpiffeIDService{ + Host: "node1", + Namespace: "default", + Datacenter: "dc1", + Service: "foo", + } + raw, _ := connect.TestCSR(t, spiffeService) + + leafCsr, err := connect.ParseCSR(raw) + require.NoError(err) + + leafPEM, err := provider2.Sign(leafCsr) + require.NoError(err) + + cert, err := connect.ParseCert(leafPEM) + require.NoError(err) + + // Check that the leaf signed by the new cert can be verified using the + // returned cert chain (signed intermediate + remote root). + intermediatePool := x509.NewCertPool() + intermediatePool.AppendCertsFromPEM([]byte(intermediatePEM)) + rootPool := x509.NewCertPool() + rootPool.AppendCertsFromPEM([]byte(rootPEM)) + + _, err = cert.Verify(x509.VerifyOptions{ + Intermediates: intermediatePool, + Roots: rootPool, + }) + require.NoError(err) +} + func TestConsulCAProvider_MigrateOldID(t *testing.T) { t.Parallel() diff --git a/agent/connect/ca/provider_vault.go b/agent/connect/ca/provider_vault.go index cbbccd7f8b..a583f9197f 100644 --- a/agent/connect/ca/provider_vault.go +++ b/agent/connect/ca/provider_vault.go @@ -102,6 +102,98 @@ func (v *VaultProvider) GenerateRoot() error { return nil } +// GenerateIntermediateCSR creates a private key and generates a CSR +// for another datacenter's root to sign, overwriting the intermediate backend +// in the process. +func (v *VaultProvider) GenerateIntermediateCSR() (string, error) { + if v.isRoot { + return "", fmt.Errorf("provider is the root certificate authority, " + + "cannot generate an intermediate CSR") + } + + return v.generateIntermediateCSR() +} + +func (v *VaultProvider) generateIntermediateCSR() (string, error) { + mounts, err := v.client.Sys().ListMounts() + if err != nil { + return "", err + } + + // Mount the backend if it isn't mounted already. + if _, ok := mounts[v.config.IntermediatePKIPath]; !ok { + err := v.client.Sys().Mount(v.config.IntermediatePKIPath, &vaultapi.MountInput{ + Type: "pki", + Description: "intermediate CA backend for Consul Connect", + Config: vaultapi.MountConfigInput{ + MaxLeaseTTL: "2160h", + }, + }) + + if err != nil { + return "", err + } + } + + // Create the role for issuing leaf certs if it doesn't exist yet + rolePath := v.config.IntermediatePKIPath + "roles/" + VaultCALeafCertRole + role, err := v.client.Logical().Read(rolePath) + if err != nil { + return "", err + } + spiffeID := connect.SpiffeIDSigning{ClusterID: v.clusterId, Domain: "consul"} + if role == nil { + _, err := v.client.Logical().Write(rolePath, map[string]interface{}{ + "allow_any_name": true, + "allowed_uri_sans": "spiffe://*", + "key_type": "any", + "max_ttl": v.config.LeafCertTTL.String(), + "no_store": true, + "require_cn": false, + }) + if err != nil { + return "", err + } + } + + // Generate a new intermediate CSR for the root to sign. + data, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/generate/internal", map[string]interface{}{ + "common_name": "Vault CA Intermediate Authority", + "key_bits": 224, + "key_type": "ec", + "uri_sans": spiffeID.URI().String(), + }) + if err != nil { + return "", err + } + if data == nil || data.Data["csr"] == "" { + return "", fmt.Errorf("got empty value when generating intermediate CSR") + } + csr, ok := data.Data["csr"].(string) + if !ok { + return "", fmt.Errorf("csr result is not a string") + } + + return csr, nil +} + +// SetIntermediate writes the incoming intermediate and root certificates to the +// intermediate backend (as a chain). +func (v *VaultProvider) SetIntermediate(intermediatePEM, rootPEM string) error { + if v.isRoot { + return fmt.Errorf("cannot set an intermediate using another root in the primary datacenter") + } + + _, err := v.client.Logical().Write(v.config.IntermediatePKIPath+"intermediate/set-signed", map[string]interface{}{ + "certificate": fmt.Sprintf("%s\n%s", intermediatePEM, rootPEM), + }) + if err != nil { + return err + } + + return nil +} + // ActiveIntermediate returns the current intermediate certificate. func (v *VaultProvider) ActiveIntermediate() (string, error) { return v.getCA(v.config.IntermediatePKIPath) @@ -141,61 +233,14 @@ func (v *VaultProvider) getCA(path string) (string, error) { // necessary, then generates and signs a new CA CSR using the root PKI backend // and updates the intermediate backend to use that new certificate. func (v *VaultProvider) GenerateIntermediate() (string, error) { - mounts, err := v.client.Sys().ListMounts() + csr, err := v.generateIntermediateCSR() if err != nil { return "", err } - // Mount the backend if it isn't mounted already. - if _, ok := mounts[v.config.IntermediatePKIPath]; !ok { - err := v.client.Sys().Mount(v.config.IntermediatePKIPath, &vaultapi.MountInput{ - Type: "pki", - Description: "intermediate CA backend for Consul Connect", - Config: vaultapi.MountConfigInput{ - MaxLeaseTTL: "2160h", - }, - }) - - if err != nil { - return "", err - } - } - - // Create the role for issuing leaf certs if it doesn't exist yet - rolePath := v.config.IntermediatePKIPath + "roles/" + VaultCALeafCertRole - role, err := v.client.Logical().Read(rolePath) - if err != nil { - return "", err - } - spiffeID := connect.SpiffeIDSigning{ClusterID: v.clusterId, Domain: "consul"} - if role == nil { - _, err := v.client.Logical().Write(rolePath, map[string]interface{}{ - "allow_any_name": true, - "allowed_uri_sans": "spiffe://*", - "key_type": "any", - "max_ttl": v.config.LeafCertTTL.String(), - "require_cn": false, - }) - if err != nil { - return "", err - } - } - - // Generate a new intermediate CSR for the root to sign. - 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 "", err - } - if csr == nil || csr.Data["csr"] == "" { - return "", fmt.Errorf("got empty value when generating intermediate CSR") - } - // Sign the CSR with the root backend. intermediate, err := v.client.Logical().Write(v.config.RootPKIPath+"root/sign-intermediate", map[string]interface{}{ - "csr": csr.Data["csr"], + "csr": csr, "format": "pem_bundle", }) if err != nil { @@ -249,6 +294,36 @@ func (v *VaultProvider) Sign(csr *x509.CertificateRequest) (string, error) { return fmt.Sprintf("%s\n%s", cert, ca), nil } +// SignIntermediate returns a signed CA certificate with a path length constraint +// of 0 to ensure that the certificate cannot be used to generate further CA certs. +func (v *VaultProvider) SignIntermediate(csr *x509.CertificateRequest) (string, error) { + var pemBuf bytes.Buffer + err := pem.Encode(&pemBuf, &pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csr.Raw}) + if err != nil { + return "", err + } + + // Sign the CSR with the root backend. + data, err := v.client.Logical().Write(v.config.RootPKIPath+"root/sign-intermediate", map[string]interface{}{ + "csr": pemBuf.String(), + "format": "pem_bundle", + "max_path_length": 0, + }) + if err != nil { + return "", err + } + if data == nil || data.Data["certificate"] == "" { + return "", fmt.Errorf("got empty value when generating intermediate certificate") + } + + intermediate, ok := data.Data["certificate"].(string) + if !ok { + return "", fmt.Errorf("signed intermediate result is not a string") + } + + return intermediate, nil +} + // CrossSignCA takes a CA certificate and cross-signs it to form a trust chain // back to our active root. func (v *VaultProvider) CrossSignCA(cert *x509.Certificate) (string, error) { diff --git a/agent/connect/ca/provider_vault_test.go b/agent/connect/ca/provider_vault_test.go index eee9e9544c..7409ae7c61 100644 --- a/agent/connect/ca/provider_vault_test.go +++ b/agent/connect/ca/provider_vault_test.go @@ -16,10 +16,10 @@ import ( ) func testVaultCluster(t *testing.T) (*VaultProvider, *vault.Core, net.Listener) { - return testVaultClusterWithConfig(t, nil) + return testVaultClusterWithConfig(t, true, nil) } -func testVaultClusterWithConfig(t *testing.T, rawConf map[string]interface{}) (*VaultProvider, *vault.Core, net.Listener) { +func testVaultClusterWithConfig(t *testing.T, isRoot bool, rawConf map[string]interface{}) (*VaultProvider, *vault.Core, net.Listener) { if err := vault.AddTestLogicalBackend("pki", pki.Factory); err != nil { t.Fatal(err) } @@ -41,10 +41,12 @@ func testVaultClusterWithConfig(t *testing.T, rawConf map[string]interface{}) (* require := require.New(t) provider := &VaultProvider{} - require.NoError(provider.Configure("asdf", true, conf)) - require.NoError(provider.GenerateRoot()) - _, err := provider.GenerateIntermediate() - require.NoError(err) + require.NoError(provider.Configure("asdf", isRoot, conf)) + if isRoot { + require.NoError(provider.GenerateRoot()) + _, err := provider.GenerateIntermediate() + require.NoError(err) + } return provider, core, ln } @@ -100,7 +102,7 @@ func TestVaultCAProvider_SignLeaf(t *testing.T) { t.Parallel() require := require.New(t) - provider, core, listener := testVaultClusterWithConfig(t, map[string]interface{}{ + provider, core, listener := testVaultClusterWithConfig(t, true, map[string]interface{}{ "LeafCertTTL": "1h", }) defer core.Shutdown() @@ -176,3 +178,52 @@ func TestVaultCAProvider_CrossSignCA(t *testing.T) { testCrossSignProviders(t, provider1, provider2) } + +func TestVaultProvider_SignIntermediate(t *testing.T) { + t.Parallel() + + provider1, core1, listener1 := testVaultCluster(t) + defer core1.Shutdown() + defer listener1.Close() + + provider2, core2, listener2 := testVaultClusterWithConfig(t, false, nil) + defer core2.Shutdown() + defer listener2.Close() + + testSignIntermediateCrossDC(t, provider1, provider2) +} + +func TestVaultProvider_SignIntermediateConsul(t *testing.T) { + t.Parallel() + + require := require.New(t) + + // primary = Vault, secondary = Consul + { + provider1, core, listener := testVaultCluster(t) + defer core.Shutdown() + defer listener.Close() + + conf := testConsulCAConfig() + delegate := newMockDelegate(t, conf) + provider2 := &ConsulProvider{Delegate: delegate} + require.NoError(provider2.Configure(conf.ClusterID, false, conf.Config)) + + testSignIntermediateCrossDC(t, provider1, provider2) + } + + // primary = Consul, secondary = Vault + { + conf := testConsulCAConfig() + delegate := newMockDelegate(t, conf) + provider1 := &ConsulProvider{Delegate: delegate} + require.NoError(provider1.Configure(conf.ClusterID, true, conf.Config)) + require.NoError(provider1.GenerateRoot()) + + provider2, core, listener := testVaultClusterWithConfig(t, false, nil) + defer core.Shutdown() + defer listener.Close() + + testSignIntermediateCrossDC(t, provider1, provider2) + } +} diff --git a/agent/connect/csr.go b/agent/connect/csr.go index 16a46af3fd..61a73ed33a 100644 --- a/agent/connect/csr.go +++ b/agent/connect/csr.go @@ -5,16 +5,19 @@ import ( "crypto" "crypto/rand" "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" "encoding/pem" "net/url" ) // CreateCSR returns a CSR to sign the given service along with the PEM-encoded // private key for this certificate. -func CreateCSR(uri CertURI, privateKey crypto.Signer) (string, error) { +func CreateCSR(uri CertURI, privateKey crypto.Signer, extensions ...pkix.Extension) (string, error) { template := &x509.CertificateRequest{ URIs: []*url.URL{uri.URI()}, SignatureAlgorithm: x509.ECDSAWithSHA256, + ExtraExtensions: extensions, } // Create the CSR itself @@ -31,3 +34,34 @@ func CreateCSR(uri CertURI, privateKey crypto.Signer) (string, error) { return csrBuf.String(), nil } + +// CreateCSR returns a CA CSR to sign the given service along with the PEM-encoded +// private key for this certificate. +func CreateCACSR(uri CertURI, privateKey crypto.Signer) (string, error) { + ext, err := CreateCAExtension() + if err != nil { + return "", err + } + + return CreateCSR(uri, privateKey, ext) +} + +// CreateCAExtension creates a pkix.Extension for the x509 Basic Constraints +// IsCA field () +func CreateCAExtension() (pkix.Extension, error) { + type basicConstraints struct { + IsCA bool `asn1:"optional"` + MaxPathLen int `asn1:"optional"` + } + basicCon := basicConstraints{IsCA: true, MaxPathLen: 0} + bitstr, err := asn1.Marshal(basicCon) + if err != nil { + return pkix.Extension{}, err + } + + return pkix.Extension{ + Id: []int{2, 5, 29, 19}, // from x509 package + Critical: true, + Value: bitstr, + }, nil +} diff --git a/agent/connect/uri_signing.go b/agent/connect/uri_signing.go index d934360ebe..1b4a4c2391 100644 --- a/agent/connect/uri_signing.go +++ b/agent/connect/uri_signing.go @@ -49,7 +49,7 @@ func (id *SpiffeIDSigning) CanSign(cu CertURI) bool { // that we could open this up later for example to support external // federation of roots and cross-signing external roots that have different // URI structure but it's simpler to start off restrictive. - return id == other + return id.URI().String() == other.URI().String() case *SpiffeIDService: // The host component of the service must be an exact match for now under // ascii case folding (since hostnames are case-insensitive). Later we might