diff --git a/builtin/logical/pki/backend.go b/builtin/logical/pki/backend.go index 8e09edc91..9f72d09e0 100644 --- a/builtin/logical/pki/backend.go +++ b/builtin/logical/pki/backend.go @@ -71,6 +71,7 @@ func Backend(conf *logical.BackendConfig) *backend { pathFetchCAChain(&b), pathFetchCRL(&b), pathFetchCRLViaCertPath(&b), + pathFetchValidRaw(&b), pathFetchValid(&b), pathFetchListCerts(&b), pathRevoke(&b), diff --git a/builtin/logical/pki/backend_test.go b/builtin/logical/pki/backend_test.go index 61c1324f8..1b346d5f6 100644 --- a/builtin/logical/pki/backend_test.go +++ b/builtin/logical/pki/backend_test.go @@ -1705,6 +1705,74 @@ func generateRoleSteps(t *testing.T, useCSRs bool) []logicaltest.TestStep { return ret } +func TestBackend_PathFetchValidRaw(t *testing.T) { + // create the backend + config := logical.TestBackendConfig() + storage := &logical.InmemStorage{} + config.StorageView = storage + + b := Backend(config) + err := b.Setup(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + expectedSerial := "17:67:16:b0:b9:45:58:c0:3a:29:e3:cb:d6:98:33:7a:a6:3b:66:c1" + expectedCert := []byte("test certificate") + entry := &logical.StorageEntry{ + Key: fmt.Sprintf("certs/%s", normalizeSerial(expectedSerial)), + Value: expectedCert, + } + err = storage.Put(context.Background(), entry) + + // get der cert + resp, err := b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: fmt.Sprintf("cert/%s/raw", expectedSerial), + Storage: storage, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to get raw cert, %#v", resp) + } + if err != nil { + t.Fatal(err) + } + + // check the raw cert matches the response body + if bytes.Compare(resp.Data[logical.HTTPRawBody].([]byte), expectedCert) != 0 { + t.Fatalf("failed to get raw cert") + } + if resp.Data[logical.HTTPContentType] != "application/pkix-cert" { + t.Fatalf("failed to get raw cert content-type") + } + + // get pem + resp, err = b.HandleRequest(context.Background(), &logical.Request{ + Operation: logical.ReadOperation, + Path: fmt.Sprintf("cert/%s/raw/pem", expectedSerial), + Storage: storage, + }) + if resp != nil && resp.IsError() { + t.Fatalf("failed to get raw, %#v", resp) + } + if err != nil { + t.Fatal(err) + } + + pemBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: expectedCert, + } + pemCert := []byte(strings.TrimSpace(string(pem.EncodeToMemory(pemBlock)))) + // check the pem cert matches the response body + if bytes.Compare(resp.Data[logical.HTTPRawBody].([]byte), pemCert) != 0 { + t.Fatalf("failed to get pem cert") + } + if resp.Data[logical.HTTPContentType] != "application/pkix-cert" { + t.Fatalf("failed to get raw cert content-type") + } +} + func TestBackend_PathFetchCertList(t *testing.T) { // create the backend config := logical.TestBackendConfig() @@ -3039,7 +3107,7 @@ func TestBackend_AllowedURISANsTemplate(t *testing.T) { // Write test policy for userpass auth method. err := client.Sys().PutPolicy("test", ` - path "pki/*" { + path "pki/*" { capabilities = ["update"] }`) if err != nil { diff --git a/builtin/logical/pki/path_fetch.go b/builtin/logical/pki/path_fetch.go index 220e406c1..07a8eeccf 100644 --- a/builtin/logical/pki/path_fetch.go +++ b/builtin/logical/pki/path_fetch.go @@ -53,6 +53,27 @@ func pathFetchCRL(b *backend) *framework.Path { } } +// Returns any valid (non-revoked) cert in raw format. +func pathFetchValidRaw(b *backend) *framework.Path { + return &framework.Path{ + Pattern: `cert/(?P[0-9A-Fa-f-:]+)/raw(/pem)?`, + Fields: map[string]*framework.FieldSchema{ + "serial": { + Type: framework.TypeString, + Description: `Certificate serial number, in colon- or +hyphen-separated octal`, + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathFetchRead, + }, + + HelpSynopsis: pathFetchHelpSyn, + HelpDescription: pathFetchHelpDesc, + } +} + // Returns any valid (non-revoked) cert. Since "ca" fits the pattern, this path // also handles returning the CA cert in a non-raw format. func pathFetchValid(b *backend) *framework.Path { @@ -150,6 +171,12 @@ func (b *backend) pathFetchRead(ctx context.Context, req *logical.Request, data case req.Path == "cert/crl": serial = "crl" pemType = "X509 CRL" + case strings.HasSuffix(req.Path, "/pem") || strings.HasSuffix(req.Path, "/raw"): + serial = data.Get("serial").(string) + contentType = "application/pkix-cert" + if strings.HasSuffix(req.Path, "/pem") { + pemType = "CERTIFICATE" + } default: serial = data.Get("serial").(string) pemType = "CERTIFICATE" diff --git a/changelog/10948.txt b/changelog/10948.txt new file mode 100644 index 000000000..fdf33e8b9 --- /dev/null +++ b/changelog/10948.txt @@ -0,0 +1,3 @@ +```release-note:improvement +secrets/pki: Add ability to fetch individual certificate as DER or PEM +``` \ No newline at end of file diff --git a/website/content/api-docs/secret/pki.mdx b/website/content/api-docs/secret/pki.mdx index 68f4117ee..f107fc128 100644 --- a/website/content/api-docs/secret/pki.mdx +++ b/website/content/api-docs/secret/pki.mdx @@ -19,6 +19,7 @@ update your API calls accordingly. - [Read CA Certificate](#read-ca-certificate) - [Read CA Certificate Chain](#read-ca-certificate-chain) - [Read Certificate](#read-certificate) +- [Read Raw Certificate](#read-raw-certificate) - [List Certificates](#list-certificates) - [Submit CA Information](#submit-ca-information) - [Read CRL Configuration](#read-crl-configuration) @@ -132,6 +133,37 @@ $ curl \ } ``` +## Read Raw Certificate + +This endpoint retrieves one of a selection of certificates _in raw DER-encoded +form_. 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/:serial` for that. If +`/pem` is added to the endpoint, the selected certificate is returned in PEM format. + +This is an unauthenticated endpoint. + +| Method | Path | +| :----- | :---------------------------- | +| `GET` | `/pki/cert/:serial/raw(/pem)` | + +### Parameters + +- `serial` `(string: )` – Specifies the serial number of the + certificate to select, in hyphen-separated or colon-separated octal. + +### Sample Request + +```shell-session +$ curl \ + http://127.0.0.1:8200/v1/pki/cert/39:dd:2e.../raw +``` + +### Sample Response + +```text + +``` + ## List Certificates This endpoint returns a list of the current certificates by serial number only.