Merge pull request #4672 from hashicorp/ca-refactor-2
connect/ca: add methods for generating and signing intermediate CSRs
This commit is contained in:
commit
fca5ed61e5
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue