Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 5 commits into from
Feb 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 153 additions & 0 deletions builtin/logical/pki/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 17 additions & 0 deletions builtin/logical/pki/path_fetch.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}{},
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions changelog/13935.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
secrets/pki: Return complete chain (in `ca_chain` field) on calls to `pki/cert/ca_chain`
```
15 changes: 15 additions & 0 deletions sdk/helper/certutil/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down
6 changes: 5 additions & 1 deletion website/content/api-docs/secret/pki.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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

Expand Down