Skip to content

Commit

Permalink
Allow fetching the specified issuer's CRL
Browse files Browse the repository at this point in the history
We add a new API endpoint to fetch the specified issuer's CRL directly
(rather than the default issuer's CRL at /crl and /certs/crl). We also
add a new test to validate the CRL in a multi-root scenario and ensure
it is signed with the correct keys.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
  • Loading branch information
cipherboy committed Apr 22, 2022
1 parent 8cfe955 commit 39f8606
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 0 deletions.
1 change: 1 addition & 0 deletions builtin/logical/pki/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func Backend(conf *logical.BackendConfig) *backend {
// Issuer APIs
pathListIssuers(&b),
pathGetIssuer(&b),
pathGetIssuerCRL(&b),
pathImportIssuer(&b),
pathIssuerSignIntermediate(&b),
pathIssuerSignSelfIssued(&b),
Expand Down
56 changes: 56 additions & 0 deletions builtin/logical/pki/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3978,6 +3978,46 @@ func TestBackend_RevokePlusTidy_Intermediate(t *testing.T) {

func getParsedCrl(t *testing.T, client *api.Client, mountPoint string) *pkix.CertificateList {
path := fmt.Sprintf("/v1/%s/crl", mountPoint)
return getParsedCrlAtPath(t, client, path)
}

func getParsedCrlForIssuer(t *testing.T, client *api.Client, mountPoint string, issuer string) *pkix.CertificateList {
path := fmt.Sprintf("/v1/%v/issuer/%v/crl", mountPoint, issuer)
crl := getParsedCrlAtPath(t, client, path)

// Now fetch the issuer as well and verify the certificate
path = fmt.Sprintf("/v1/%v/issuer/%v/der", mountPoint, issuer)
req := client.NewRequest("GET", path)
resp, err := client.RawRequest(req)
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()

certBytes, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Fatalf("err: %s", err)
}
if len(certBytes) == 0 {
t.Fatalf("expected certificate in response body")
}

cert, err := x509.ParseCertificate(certBytes)
if err != nil {
t.Fatal(err)
}
if cert == nil {
t.Fatalf("expected parsed certificate")
}

if err := cert.CheckCRLSignature(crl); err != nil {
t.Fatalf("expected valid signature on CRL for issuer %v: %v", issuer, crl)
}

return crl
}

func getParsedCrlAtPath(t *testing.T, client *api.Client, path string) *pkix.CertificateList {
req := client.NewRequest("GET", path)
resp, err := client.RawRequest(req)
if err != nil {
Expand Down Expand Up @@ -4718,6 +4758,10 @@ func TestRootWithExistingKey(t *testing.T) {
require.NotEmpty(t, myIssuerId1)
require.NotEmpty(t, myKeyId1)

// Fetch the parsed CRL; it should be empty as we've not revoked anything
parsedCrl := getParsedCrlForIssuer(t, client, "pki-root", "my-issuer1")
require.Equal(t, len(parsedCrl.TBSCertList.RevokedCertificates), 0, "should have no revoked certificates")

// Fail if the specified issuer name is re-used.
_, err = client.Logical().WriteWithContext(ctx, "pki-root/issuers/generate/root/internal", map[string]interface{}{
"common_name": "root myvault.com",
Expand All @@ -4740,6 +4784,10 @@ func TestRootWithExistingKey(t *testing.T) {
require.NotEmpty(t, myIssuerId2)
require.NotEmpty(t, myKeyId2)

// Fetch the parsed CRL; it should be empty as we've not revoked anything
parsedCrl = getParsedCrlForIssuer(t, client, "pki-root", "my-issuer2")
require.Equal(t, len(parsedCrl.TBSCertList.RevokedCertificates), 0, "should have no revoked certificates")

// Fail if the specified key name is re-used.
_, err = client.Logical().WriteWithContext(ctx, "pki-root/issuers/generate/root/internal", map[string]interface{}{
"common_name": "root myvault.com",
Expand All @@ -4762,6 +4810,14 @@ func TestRootWithExistingKey(t *testing.T) {
require.NotEmpty(t, myIssuerId3)
require.NotEmpty(t, myKeyId3)

// Fetch the parsed CRL; it should be empty as we've not revoking anything.
parsedCrl = getParsedCrlForIssuer(t, client, "pki-root", "my-issuer3")
require.Equal(t, len(parsedCrl.TBSCertList.RevokedCertificates), 0, "should have no revoked certificates")
// Signatures should be the same since this is just a reissued cert. We
// use signature as a proxy for "these two CRLs are equal".
firstCrl := getParsedCrlForIssuer(t, client, "pki-root", "my-issuer1")
require.Equal(t, parsedCrl.SignatureValue, firstCrl.SignatureValue)

require.NotEqual(t, myIssuerId1, myIssuerId2)
require.NotEqual(t, myIssuerId1, myIssuerId3)
require.NotEqual(t, myKeyId1, myKeyId2)
Expand Down
94 changes: 94 additions & 0 deletions builtin/logical/pki/path_fetch_issuers.go
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,97 @@ Writing to /issuer/:ref allows updating of the name field associated with
the certificate.
`
)

func pathGetIssuerCRL(b *backend) *framework.Path {
pattern := "issuer/" + framework.GenericNameRegex(issuerRefParam) + "/crl(/pem)?"
return buildPathGetIssuerCRL(b, pattern)
}

func buildPathGetIssuerCRL(b *backend, pattern string) *framework.Path {
fields := map[string]*framework.FieldSchema{}
fields = addIssuerRefNameFields(fields)

return &framework.Path{
// Returns raw values.
Pattern: pattern,
Fields: fields,

Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathGetIssuerCRL,
},

HelpSynopsis: pathGetIssuerCRLHelpSyn,
HelpDescription: pathGetIssuerCRLHelpDesc,
}
}

func (b *backend) pathGetIssuerCRL(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
issuerName := getIssuerRef(data)
if len(issuerName) == 0 {
return logical.ErrorResponse("missing issuer reference"), nil
}

crlPath, err := resolveIssuerCRLPath(ctx, req.Storage, issuerName)
if err != nil {
return nil, err
}

crlEntry, err := req.Storage.Get(ctx, crlPath)
if err != nil {
return nil, err
}

var certificate []byte
if crlEntry != nil && len(crlEntry.Value) > 0 {
certificate = []byte(crlEntry.Value)
}

contentType := "application/pkix-crl"

if strings.HasSuffix(req.Path, "/pem") {
contentType = "application/x-pem-file"

// Rather return an empty response rather than an empty
// PEM blob.
if len(certificate) > 0 {
pemBlock := pem.Block{
Type: "X509 CRL",
Bytes: certificate,
}

certificate = pem.EncodeToMemory(&pemBlock)
}
}

statusCode := 200
if len(certificate) == 0 {
statusCode = 204
}

return &logical.Response{
Data: map[string]interface{}{
logical.HTTPContentType: contentType,
logical.HTTPRawBody: certificate,
logical.HTTPStatusCode: statusCode,
},
}, nil
}

const (
pathGetIssuerCRLHelpSyn = `Fetch an issuer's Certificate Revocation Log (CRL).`
pathGetIssuerCRLHelpDesc = `
This allows fetching the specified issuer's CRL. Note that this is different
than the legacy path (/crl and /certs/crl) in that this is per-issuer and not
just the default issuer's CRL.
Two issuers will have the same CRL if they have the same key material and if
they have the same Subject value.
:ref can be either the literal value "default", in which case /config/issuers
will be consulted for the present default issuer, an identifier of an issuer,
or its assigned name value.
Use /issuer/:ref/crl/pem to return just the certificate in PEM form; the raw
/issuer/:ref/crl is in DER form.
`
)

0 comments on commit 39f8606

Please sign in to comment.