open-vault/builtin/logical/pki/ocsp_test.go

544 lines
21 KiB
Go
Raw Normal View History

package pki
import (
"bytes"
"context"
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"testing"
"github.com/hashicorp/vault/sdk/logical"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ocsp"
)
// If the ocsp_disabled flag is set to true in the crl configuration make sure we always
// return an Unauthorized error back as we assume an end-user disabling the feature does
// not want us to act as the OCSP authority and the RFC specifies this is the appropriate response.
func TestOcsp_Disabled(t *testing.T) {
t.Parallel()
type testArgs struct {
reqType string
}
var tests []testArgs
for _, reqType := range []string{"get", "post"} {
tests = append(tests, testArgs{
reqType: reqType,
})
}
for _, tt := range tests {
localTT := tt
t.Run(localTT.reqType, func(t *testing.T) {
b, s, testEnv := setupOcspEnv(t, "rsa")
resp, err := CBWrite(b, s, "config/crl", map[string]interface{}{
"ocsp_disable": "true",
})
requireSuccessNilResponse(t, resp, err)
resp, err = sendOcspRequest(t, b, s, localTT.reqType, testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
require.NoError(t, err)
requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
require.Equal(t, 401, resp.Data["http_status_code"])
require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
respDer := resp.Data["http_raw_body"].([]byte)
require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer)
})
}
}
// If we can't find the issuer within the request and have no default issuer to sign an Unknown response
// with return an UnauthorizedErrorResponse/according to/the RFC, similar to if we are disabled (lack of authority)
// This behavior differs from CRLs when an issuer is removed from a mount.
func TestOcsp_UnknownIssuerWithNoDefault(t *testing.T) {
t.Parallel()
_, _, testEnv := setupOcspEnv(t, "ec")
// Create another completely empty mount so the created issuer/certificate above is unknown
b, s := createBackendWithStorage(t)
resp, err := sendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
require.NoError(t, err)
requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
require.Equal(t, 401, resp.Data["http_status_code"])
require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
respDer := resp.Data["http_raw_body"].([]byte)
require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer)
}
// If the issuer in the request does exist, but the request coming in associates the serial with the
// wrong issuer return an Unknown response back to the caller.
func TestOcsp_WrongIssuerInRequest(t *testing.T) {
t.Parallel()
b, s, testEnv := setupOcspEnv(t, "ec")
serial := serialFromCert(testEnv.leafCertIssuer1)
resp, err := CBWrite(b, s, "revoke", map[string]interface{}{
"serial_number": serial,
})
requireSuccessNonNilResponse(t, resp, err, "revoke")
resp, err = sendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer2, crypto.SHA1)
require.NoError(t, err)
requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
require.Equal(t, 200, resp.Data["http_status_code"])
require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
respDer := resp.Data["http_raw_body"].([]byte)
ocspResp, err := ocsp.ParseResponse(respDer, testEnv.issuer1)
require.NoError(t, err, "parsing ocsp get response")
require.Equal(t, ocsp.Unknown, ocspResp.Status)
}
// Verify that requests we can't properly decode result in the correct response of MalformedRequestError
func TestOcsp_MalformedRequests(t *testing.T) {
t.Parallel()
type testArgs struct {
reqType string
}
var tests []testArgs
for _, reqType := range []string{"get", "post"} {
tests = append(tests, testArgs{
reqType: reqType,
})
}
for _, tt := range tests {
localTT := tt
t.Run(localTT.reqType, func(t *testing.T) {
b, s, _ := setupOcspEnv(t, "rsa")
badReq := []byte("this is a bad request")
var resp *logical.Response
var err error
switch localTT.reqType {
case "get":
resp, err = sendOcspGetRequest(b, s, badReq)
case "post":
resp, err = sendOcspPostRequest(b, s, badReq)
default:
t.Fatalf("bad request type")
}
require.NoError(t, err)
requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
require.Equal(t, 400, resp.Data["http_status_code"])
require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
respDer := resp.Data["http_raw_body"].([]byte)
require.Equal(t, ocsp.MalformedRequestErrorResponse, respDer)
})
}
}
// Validate that we properly handle a revocation entry that contains an issuer ID that no longer exists,
// the best we can do in this use case is to respond back with the default issuer that we don't know
// the issuer that they are requesting (we can't guarantee that the client is actually requesting a serial
// from that issuer)
func TestOcsp_InvalidIssuerIdInRevocationEntry(t *testing.T) {
t.Parallel()
b, s, testEnv := setupOcspEnv(t, "ec")
ctx := context.Background()
// Revoke the entry
serial := serialFromCert(testEnv.leafCertIssuer1)
resp, err := CBWrite(b, s, "revoke", map[string]interface{}{
"serial_number": serial,
})
requireSuccessNonNilResponse(t, resp, err, "revoke")
// Twiddle the entry so that the issuer id is no longer valid.
storagePath := revokedPath + normalizeSerial(serial)
var revInfo revocationInfo
revEntry, err := s.Get(ctx, storagePath)
require.NoError(t, err, "failed looking up storage path: %s", storagePath)
err = revEntry.DecodeJSON(&revInfo)
require.NoError(t, err, "failed decoding storage entry: %v", revEntry)
revInfo.CertificateIssuer = "00000000-0000-0000-0000-000000000000"
revEntry, err = logical.StorageEntryJSON(storagePath, revInfo)
require.NoError(t, err, "failed re-encoding revocation info: %v", revInfo)
err = s.Put(ctx, revEntry)
require.NoError(t, err, "failed writing out new revocation entry: %v", revEntry)
// Send the request
resp, err = sendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
require.NoError(t, err)
requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
require.Equal(t, 200, resp.Data["http_status_code"])
require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
respDer := resp.Data["http_raw_body"].([]byte)
ocspResp, err := ocsp.ParseResponse(respDer, testEnv.issuer1)
require.NoError(t, err, "parsing ocsp get response")
require.Equal(t, ocsp.Unknown, ocspResp.Status)
}
// Validate that we properly handle an unknown issuer use-case but that the default issuer
// does not have the OCSP usage flag set, we can't do much else other than reply with an
// Unauthorized response.
func TestOcsp_UnknownIssuerIdWithDefaultHavingOcspUsageRemoved(t *testing.T) {
t.Parallel()
b, s, testEnv := setupOcspEnv(t, "ec")
ctx := context.Background()
// Revoke the entry
serial := serialFromCert(testEnv.leafCertIssuer1)
resp, err := CBWrite(b, s, "revoke", map[string]interface{}{
"serial_number": serial,
})
requireSuccessNonNilResponse(t, resp, err, "revoke")
// Twiddle the entry so that the issuer id is no longer valid.
storagePath := revokedPath + normalizeSerial(serial)
var revInfo revocationInfo
revEntry, err := s.Get(ctx, storagePath)
require.NoError(t, err, "failed looking up storage path: %s", storagePath)
err = revEntry.DecodeJSON(&revInfo)
require.NoError(t, err, "failed decoding storage entry: %v", revEntry)
revInfo.CertificateIssuer = "00000000-0000-0000-0000-000000000000"
revEntry, err = logical.StorageEntryJSON(storagePath, revInfo)
require.NoError(t, err, "failed re-encoding revocation info: %v", revInfo)
err = s.Put(ctx, revEntry)
require.NoError(t, err, "failed writing out new revocation entry: %v", revEntry)
// Update our issuers to no longer have the OcspSigning usage
resp, err = CBPatch(b, s, "issuer/"+testEnv.issuerId1.String(), map[string]interface{}{
"usage": "read-only,issuing-certificates,crl-signing",
})
requireSuccessNonNilResponse(t, resp, err, "failed resetting usage flags on issuer1")
resp, err = CBPatch(b, s, "issuer/"+testEnv.issuerId2.String(), map[string]interface{}{
"usage": "read-only,issuing-certificates,crl-signing",
})
requireSuccessNonNilResponse(t, resp, err, "failed resetting usage flags on issuer2")
// Send the request
resp, err = sendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
require.NoError(t, err)
requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
require.Equal(t, 401, resp.Data["http_status_code"])
require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
respDer := resp.Data["http_raw_body"].([]byte)
require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer)
}
// Verify that if we do have a revoked certificate entry for the request, that matches an
// issuer but that issuer does not have the OcspUsage flag set that we return an Unauthorized
// response back to the caller
func TestOcsp_RevokedCertHasIssuerWithoutOcspUsage(t *testing.T) {
b, s, testEnv := setupOcspEnv(t, "ec")
// Revoke our certificate
resp, err := CBWrite(b, s, "revoke", map[string]interface{}{
"serial_number": serialFromCert(testEnv.leafCertIssuer1),
})
requireSuccessNonNilResponse(t, resp, err, "revoke")
// Update our issuer to no longer have the OcspSigning usage
resp, err = CBPatch(b, s, "issuer/"+testEnv.issuerId1.String(), map[string]interface{}{
"usage": "read-only,issuing-certificates,crl-signing",
})
requireSuccessNonNilResponse(t, resp, err, "failed resetting usage flags on issuer")
requireFieldsSetInResp(t, resp, "usage")
// Do not assume a specific ordering for usage...
usages, err := NewIssuerUsageFromNames(strings.Split(resp.Data["usage"].(string), ","))
require.NoError(t, err, "failed parsing usage return value")
require.True(t, usages.HasUsage(IssuanceUsage))
require.True(t, usages.HasUsage(CRLSigningUsage))
require.False(t, usages.HasUsage(OCSPSigningUsage))
// Request an OCSP request from it, we should get an Unauthorized response back
resp, err = sendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
requireSuccessNonNilResponse(t, resp, err, "ocsp get request")
requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
require.Equal(t, 401, resp.Data["http_status_code"])
require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
respDer := resp.Data["http_raw_body"].([]byte)
require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer)
}
// Verify if our matching issuer for a revocation entry has no key associated with it that
// we bail with an Unauthorized response.
func TestOcsp_RevokedCertHasIssuerWithoutAKey(t *testing.T) {
b, s, testEnv := setupOcspEnv(t, "ec")
// Revoke our certificate
resp, err := CBWrite(b, s, "revoke", map[string]interface{}{
"serial_number": serialFromCert(testEnv.leafCertIssuer1),
})
requireSuccessNonNilResponse(t, resp, err, "revoke")
// Delete the key associated with our issuer
resp, err = CBRead(b, s, "issuer/"+testEnv.issuerId1.String())
requireSuccessNonNilResponse(t, resp, err, "failed reading issuer")
requireFieldsSetInResp(t, resp, "key_id")
keyId := resp.Data["key_id"].(keyID)
// This is a bit naughty but allow me to delete the key...
sc := b.makeStorageContext(context.Background(), s)
issuer, err := sc.fetchIssuerById(testEnv.issuerId1)
require.NoError(t, err, "failed to get issuer from storage")
issuer.KeyID = ""
err = sc.writeIssuer(issuer)
require.NoError(t, err, "failed to write issuer update")
resp, err = CBDelete(b, s, "key/"+keyId.String())
requireSuccessNonNilResponse(t, resp, err, "failed deleting key")
// Request an OCSP request from it, we should get an Unauthorized response back
resp, err = sendOcspRequest(t, b, s, "get", testEnv.leafCertIssuer1, testEnv.issuer1, crypto.SHA1)
requireSuccessNonNilResponse(t, resp, err, "ocsp get request")
requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
require.Equal(t, 401, resp.Data["http_status_code"])
require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
respDer := resp.Data["http_raw_body"].([]byte)
require.Equal(t, ocsp.UnauthorizedErrorResponse, respDer)
}
func TestOcsp_ValidRequests(t *testing.T) {
t.Parallel()
type testArgs struct {
reqType string
caKeyType string
reqHash crypto.Hash
}
var tests []testArgs
for _, reqType := range []string{"get", "post"} {
for _, caKeyType := range []string{"rsa", "ec"} { // "ed25519" is not supported at the moment in x/crypto/ocsp
for _, requestHash := range []crypto.Hash{crypto.SHA1, crypto.SHA256} {
tests = append(tests, testArgs{
reqType: reqType,
caKeyType: caKeyType,
reqHash: requestHash,
})
}
}
}
for _, tt := range tests {
localTT := tt
testName := fmt.Sprintf("%s-%s-%s", localTT.reqType, localTT.caKeyType, localTT.reqHash)
t.Run(testName, func(t *testing.T) {
runOcspRequestTest(t, localTT.reqType, localTT.caKeyType, localTT.reqHash)
})
}
}
func runOcspRequestTest(t *testing.T, requestType string, caKeyType string, requestHash crypto.Hash) {
b, s, testEnv := setupOcspEnv(t, caKeyType)
// Non-revoked cert
resp, err := sendOcspRequest(t, b, s, requestType, testEnv.leafCertIssuer1, testEnv.issuer1, requestHash)
requireSuccessNonNilResponse(t, resp, err, "ocsp get request")
requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
require.Equal(t, 200, resp.Data["http_status_code"])
require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
respDer := resp.Data["http_raw_body"].([]byte)
ocspResp, err := ocsp.ParseResponse(respDer, testEnv.issuer1)
require.NoError(t, err, "parsing ocsp get response")
require.Equal(t, ocsp.Good, ocspResp.Status)
require.Equal(t, requestHash, ocspResp.IssuerHash)
require.Equal(t, testEnv.issuer1, ocspResp.Certificate)
require.Equal(t, 0, ocspResp.RevocationReason)
require.Equal(t, testEnv.leafCertIssuer1.SerialNumber, ocspResp.SerialNumber)
requireOcspSignatureAlgoForKey(t, testEnv.issuer1.PublicKey, ocspResp.SignatureAlgorithm)
requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer1.PublicKey)
// Now revoke it
resp, err = CBWrite(b, s, "revoke", map[string]interface{}{
"serial_number": serialFromCert(testEnv.leafCertIssuer1),
})
requireSuccessNonNilResponse(t, resp, err, "revoke")
resp, err = sendOcspRequest(t, b, s, requestType, testEnv.leafCertIssuer1, testEnv.issuer1, requestHash)
requireSuccessNonNilResponse(t, resp, err, "ocsp get request with revoked")
requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
require.Equal(t, 200, resp.Data["http_status_code"])
require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
respDer = resp.Data["http_raw_body"].([]byte)
ocspResp, err = ocsp.ParseResponse(respDer, testEnv.issuer1)
require.NoError(t, err, "parsing ocsp get response with revoked")
require.Equal(t, ocsp.Revoked, ocspResp.Status)
require.Equal(t, requestHash, ocspResp.IssuerHash)
require.Equal(t, testEnv.issuer1, ocspResp.Certificate)
require.Equal(t, 0, ocspResp.RevocationReason)
require.Equal(t, testEnv.leafCertIssuer1.SerialNumber, ocspResp.SerialNumber)
requireOcspSignatureAlgoForKey(t, testEnv.issuer1.PublicKey, ocspResp.SignatureAlgorithm)
requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer1.PublicKey)
// Request status for our second issuer
resp, err = sendOcspRequest(t, b, s, requestType, testEnv.leafCertIssuer2, testEnv.issuer2, requestHash)
requireSuccessNonNilResponse(t, resp, err, "ocsp get request")
requireFieldsSetInResp(t, resp, "http_content_type", "http_status_code", "http_raw_body")
require.Equal(t, 200, resp.Data["http_status_code"])
require.Equal(t, ocspResponseContentType, resp.Data["http_content_type"])
respDer = resp.Data["http_raw_body"].([]byte)
ocspResp, err = ocsp.ParseResponse(respDer, testEnv.issuer2)
require.NoError(t, err, "parsing ocsp get response")
require.Equal(t, ocsp.Good, ocspResp.Status)
require.Equal(t, requestHash, ocspResp.IssuerHash)
require.Equal(t, testEnv.issuer2, ocspResp.Certificate)
require.Equal(t, 0, ocspResp.RevocationReason)
require.Equal(t, testEnv.leafCertIssuer2.SerialNumber, ocspResp.SerialNumber)
requireOcspSignatureAlgoForKey(t, testEnv.issuer2.PublicKey, ocspResp.SignatureAlgorithm)
requireOcspResponseSignedBy(t, ocspResp, testEnv.issuer2.PublicKey)
}
func requireOcspSignatureAlgoForKey(t *testing.T, key crypto.PublicKey, algorithm x509.SignatureAlgorithm) {
switch key.(type) {
case *rsa.PublicKey:
require.Equal(t, x509.SHA256WithRSA, algorithm)
case *ecdsa.PublicKey:
require.Equal(t, x509.ECDSAWithSHA256, algorithm)
case ed25519.PublicKey:
require.Equal(t, x509.PureEd25519, algorithm)
default:
t.Fatalf("unsupported public key type %T", key)
}
}
type ocspTestEnv struct {
issuer1 *x509.Certificate
issuer2 *x509.Certificate
issuerId1 issuerID
issuerId2 issuerID
leafCertIssuer1 *x509.Certificate
leafCertIssuer2 *x509.Certificate
}
func setupOcspEnv(t *testing.T, keyType string) (*backend, logical.Storage, *ocspTestEnv) {
b, s := createBackendWithStorage(t)
var issuerCerts []*x509.Certificate
var leafCerts []*x509.Certificate
var issuerIds []issuerID
for i := 0; i < 2; i++ {
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"key_type": keyType,
"ttl": "40h",
"common_name": "example-ocsp.com",
})
requireSuccessNonNilResponse(t, resp, err, "root/generate/internal")
requireFieldsSetInResp(t, resp, "issuer_id")
issuerId := resp.Data["issuer_id"].(issuerID)
resp, err = CBWrite(b, s, "roles/test"+strconv.FormatInt(int64(i), 10), map[string]interface{}{
"allow_bare_domains": true,
"allow_subdomains": true,
"allowed_domains": "foobar.com",
"no_store": false,
"generate_lease": false,
"issuer_ref": issuerId,
"key_type": keyType,
})
requireSuccessNilResponse(t, resp, err, "roles/test"+strconv.FormatInt(int64(i), 10))
resp, err = CBWrite(b, s, "issue/test"+strconv.FormatInt(int64(i), 10), map[string]interface{}{
"common_name": "test.foobar.com",
})
requireSuccessNonNilResponse(t, resp, err, "roles/test"+strconv.FormatInt(int64(i), 10))
requireFieldsSetInResp(t, resp, "certificate", "issuing_ca", "serial_number")
leafCert := parseCert(t, resp.Data["certificate"].(string))
issuingCa := parseCert(t, resp.Data["issuing_ca"].(string))
issuerCerts = append(issuerCerts, issuingCa)
leafCerts = append(leafCerts, leafCert)
issuerIds = append(issuerIds, issuerId)
}
testEnv := &ocspTestEnv{
issuerId1: issuerIds[0],
issuer1: issuerCerts[0],
leafCertIssuer1: leafCerts[0],
issuerId2: issuerIds[1],
issuer2: issuerCerts[1],
leafCertIssuer2: leafCerts[1],
}
return b, s, testEnv
}
func sendOcspRequest(t *testing.T, b *backend, s logical.Storage, getOrPost string, cert, issuer *x509.Certificate, requestHash crypto.Hash) (*logical.Response, error) {
ocspRequest := generateRequest(t, requestHash, cert, issuer)
switch strings.ToLower(getOrPost) {
case "get":
return sendOcspGetRequest(b, s, ocspRequest)
case "post":
return sendOcspPostRequest(b, s, ocspRequest)
default:
t.Fatalf("unsupported value for sendOcspRequest getOrPost arg: %s", getOrPost)
}
return nil, nil
}
func sendOcspGetRequest(b *backend, s logical.Storage, ocspRequest []byte) (*logical.Response, error) {
urlEncoded := base64.StdEncoding.EncodeToString(ocspRequest)
return CBRead(b, s, "ocsp/"+urlEncoded)
}
func sendOcspPostRequest(b *backend, s logical.Storage, ocspRequest []byte) (*logical.Response, error) {
reader := io.NopCloser(bytes.NewReader(ocspRequest))
resp, err := b.HandleRequest(context.Background(), &logical.Request{
Operation: logical.UpdateOperation,
Path: "ocsp",
Storage: s,
MountPoint: "pki/",
HTTPRequest: &http.Request{
Body: reader,
},
})
return resp, err
}
func generateRequest(t *testing.T, requestHash crypto.Hash, cert *x509.Certificate, issuer *x509.Certificate) []byte {
opts := &ocsp.RequestOptions{Hash: requestHash}
ocspRequestDer, err := ocsp.CreateRequest(cert, issuer, opts)
require.NoError(t, err, "Failed generating OCSP request")
return ocspRequestDer
}
func requireOcspResponseSignedBy(t *testing.T, ocspResp *ocsp.Response, key crypto.PublicKey) {
require.Contains(t, []x509.SignatureAlgorithm{x509.SHA256WithRSA, x509.ECDSAWithSHA256}, ocspResp.SignatureAlgorithm)
hasher := sha256.New()
hashAlgo := crypto.SHA256
hasher.Write(ocspResp.TBSResponseData)
hashData := hasher.Sum(nil)
switch key.(type) {
case *rsa.PublicKey:
err := rsa.VerifyPKCS1v15(key.(*rsa.PublicKey), hashAlgo, hashData, ocspResp.Signature)
require.NoError(t, err, "the ocsp response was not signed by the expected public rsa key.")
case *ecdsa.PublicKey:
verify := ecdsa.VerifyASN1(key.(*ecdsa.PublicKey), hashData, ocspResp.Signature)
require.True(t, verify, "the certificate was not signed by the expected public ecdsa key.")
}
}