Add full CA Chain to /pki/cert/ca_chain response (#13935)

* Include full chain in /cert/ca_chain response

This allows callers to get the full chain (including issuing
certificates) from a call to /cert/ca_chain. Previously, most endpoints
(including during issuance) do not include the root authority, requiring
an explicit call to /cert/ca to fetch. This allows full chains to be
constructed without without needing multiple calls to the API.

Resolves: #13489

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add test case for full CA issuance

We test three main scenarios:

 1. A root-only CA's `/cert/ca_chain`'s `.data.ca_chain` field should
    contain only the root,
 2. An intermediate CA (with root provide) should contain both the root
    and the intermediate.
 3. An external (e.g., `/config/ca`-provided) CA with both root and
    intermediate should contain both certs.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add documentation for new ca_chain field

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add changelog entry

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add note about where to find the entire chain

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
This commit is contained in:
Alexander Scheel 2022-02-07 14:37:01 -05:00 committed by GitHub
parent 8d169d48d3
commit 33a9218115
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 193 additions and 1 deletions

View File

@ -3724,6 +3724,159 @@ func TestBackend_RevokePlusTidy_Intermediate(t *testing.T) {
}
}
func TestBackend_Root_FullCAChain(t *testing.T) {
coreConfig := &vault.CoreConfig{
LogicalBackends: map[string]logical.Factory{
"pki": Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
var err error
// Generate a root CA at /pki-root
err = client.Sys().Mount("pki-root", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
DefaultLeaseTTL: "16h",
MaxLeaseTTL: "32h",
},
})
if err != nil {
t.Fatal(err)
}
resp, err := client.Logical().Write("pki-root/root/generate/exported", map[string]interface{}{
"common_name": "root myvault.com",
})
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected ca info")
}
rootData := resp.Data
rootCert := rootData["certificate"].(string)
// Validate that root's /cert/ca-chain now contains the certificate.
resp, err = client.Logical().Read("pki-root/cert/ca_chain")
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected intermediate chain information")
}
fullChain := resp.Data["ca_chain"].(string)
if !strings.Contains(fullChain, rootCert) {
t.Fatal("expected full chain to contain root certificate")
}
// Now generate an intermediate at /pki-intermediate, signed by the root.
err = client.Sys().Mount("pki-intermediate", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
DefaultLeaseTTL: "16h",
MaxLeaseTTL: "32h",
},
})
if err != nil {
t.Fatal(err)
}
resp, err = client.Logical().Write("pki-intermediate/intermediate/generate/exported", map[string]interface{}{
"common_name": "intermediate myvault.com",
})
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected intermediate CSR info")
}
intermediateData := resp.Data
intermediateKey := intermediateData["private_key"].(string)
resp, err = client.Logical().Write("pki-root/root/sign-intermediate", map[string]interface{}{
"csr": intermediateData["csr"],
"format": "pem_bundle",
})
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected signed intermediate info")
}
intermediateSignedData := resp.Data
intermediateCert := intermediateSignedData["certificate"].(string)
resp, err = client.Logical().Write("pki-intermediate/intermediate/set-signed", map[string]interface{}{
"certificate": intermediateCert + "\n" + rootCert + "\n",
})
if err != nil {
t.Fatal(err)
}
// Validate that intermediate's ca_chain field now includes the full
// chain.
resp, err = client.Logical().Read("pki-intermediate/cert/ca_chain")
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected intermediate chain information")
}
fullChain = resp.Data["ca_chain"].(string)
if !strings.Contains(fullChain, intermediateCert) {
t.Fatal("expected full chain to contain intermediate certificate")
}
if !strings.Contains(fullChain, rootCert) {
t.Fatal("expected full chain to contain root certificate")
}
// Finally, import this signing cert chain into a new mount to ensure
// "external" CAs behave as expected.
err = client.Sys().Mount("pki-external", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
DefaultLeaseTTL: "16h",
MaxLeaseTTL: "32h",
},
})
if err != nil {
t.Fatal(err)
}
resp, err = client.Logical().Write("pki-external/config/ca", map[string]interface{}{
"pem_bundle": intermediateKey + "\n" + intermediateCert + "\n" + rootCert + "\n",
})
if err != nil {
t.Fatal(err)
}
// Validate the external chain information was loaded correctly.
resp, err = client.Logical().Read("pki-external/cert/ca_chain")
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected intermediate chain information")
}
fullChain = resp.Data["ca_chain"].(string)
if !strings.Contains(fullChain, intermediateCert) {
t.Fatal("expected full chain to contain intermediate certificate")
}
if !strings.Contains(fullChain, rootCert) {
t.Fatal("expected full chain to contain root certificate")
}
}
var (
initTest sync.Once
rsaCAKey string

View File

@ -140,6 +140,7 @@ func (b *backend) pathFetchRead(ctx context.Context, req *logical.Request, data
var certEntry, revokedEntry *logical.StorageEntry
var funcErr error
var certificate []byte
var fullChain []byte
var revocationTime int64
response = &logical.Response{
Data: map[string]interface{}{},
@ -207,6 +208,18 @@ func (b *backend) pathFetchRead(ctx context.Context, req *logical.Request, data
certStr = strings.Join([]string{certStr, strings.TrimSpace(string(pem.EncodeToMemory(&block)))}, "\n")
}
certificate = []byte(strings.TrimSpace(certStr))
rawChain := caInfo.GetFullChain()
var chainStr string
for _, ca := range rawChain {
block := pem.Block{
Type: "CERTIFICATE",
Bytes: ca.Bytes,
}
chainStr = strings.Join([]string{certStr, strings.TrimSpace(string(pem.EncodeToMemory(&block)))}, "\n")
}
fullChain = []byte(strings.TrimSpace(chainStr))
goto reply
}
@ -288,6 +301,10 @@ reply:
default:
response.Data["certificate"] = string(certificate)
response.Data["revocation_time"] = revocationTime
if len(fullChain) > 0 {
response.Data["ca_chain"] = string(fullChain)
}
}
return

3
changelog/13935.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
secrets/pki: Return complete chain (in `ca_chain` field) on calls to `pki/cert/ca_chain`
```

View File

@ -687,6 +687,21 @@ func (b *CAInfoBundle) GetCAChain() []*CertBlock {
return chain
}
func (b *CAInfoBundle) GetFullChain() []*CertBlock {
var chain []*CertBlock
chain = append(chain, &CertBlock{
Certificate: b.Certificate,
Bytes: b.CertificateBytes,
})
if len(b.CAChain) > 0 {
chain = append(chain, b.CAChain...)
}
return chain
}
type CertExtKeyUsage int
const (

View File

@ -75,6 +75,9 @@ $ curl \
This endpoint retrieves the CA certificate chain, including the CA _in PEM
format_. This is a bare endpoint that does not return a standard Vault data
structure and cannot be read by the Vault CLI; use `/pki/cert` for that.
Additionally, note that this doesn't include the root authority and so may
return empty data depending on configuration; use `/pki/cert/ca_chain`'s
`ca_chain` JSON data field for the entire chain including issuing authority.
This is an unauthenticated endpoint.
@ -114,7 +117,8 @@ This is an unauthenticated endpoint.
- `<serial>` for the certificate with the given serial number
- `ca` for the CA certificate
- `crl` for the current CRL
- `ca_chain` for the CA trust chain or a serial number in either hyphen-separated or colon-separated octal format
- `ca_chain` for the CA trust chain; intermediate certificates in the `certificate`
field, full chain in the `ca_chain` field.
### Sample Request