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 (
|
var (
|
||||||
initTest sync.Once
|
initTest sync.Once
|
||||||
rsaCAKey string
|
rsaCAKey string
|
||||||
|
|
|
@ -140,6 +140,7 @@ func (b *backend) pathFetchRead(ctx context.Context, req *logical.Request, data
|
||||||
var certEntry, revokedEntry *logical.StorageEntry
|
var certEntry, revokedEntry *logical.StorageEntry
|
||||||
var funcErr error
|
var funcErr error
|
||||||
var certificate []byte
|
var certificate []byte
|
||||||
|
var fullChain []byte
|
||||||
var revocationTime int64
|
var revocationTime int64
|
||||||
response = &logical.Response{
|
response = &logical.Response{
|
||||||
Data: map[string]interface{}{},
|
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")
|
certStr = strings.Join([]string{certStr, strings.TrimSpace(string(pem.EncodeToMemory(&block)))}, "\n")
|
||||||
}
|
}
|
||||||
certificate = []byte(strings.TrimSpace(certStr))
|
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
|
goto reply
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -288,6 +301,10 @@ reply:
|
||||||
default:
|
default:
|
||||||
response.Data["certificate"] = string(certificate)
|
response.Data["certificate"] = string(certificate)
|
||||||
response.Data["revocation_time"] = revocationTime
|
response.Data["revocation_time"] = revocationTime
|
||||||
|
|
||||||
|
if len(fullChain) > 0 {
|
||||||
|
response.Data["ca_chain"] = string(fullChain)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
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
|
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
|
type CertExtKeyUsage int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -75,6 +75,9 @@ $ curl \
|
||||||
This endpoint retrieves the CA certificate chain, including the CA _in PEM
|
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
|
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.
|
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.
|
This is an unauthenticated endpoint.
|
||||||
|
|
||||||
|
@ -114,7 +117,8 @@ This is an unauthenticated endpoint.
|
||||||
- `<serial>` for the certificate with the given serial number
|
- `<serial>` for the certificate with the given serial number
|
||||||
- `ca` for the CA certificate
|
- `ca` for the CA certificate
|
||||||
- `crl` for the current CRL
|
- `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
|
### Sample Request
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue