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:
parent
8d169d48d3
commit
33a9218115
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
secrets/pki: Return complete chain (in `ca_chain` field) on calls to `pki/cert/ca_chain`
|
||||
```
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue