open-vault/builtin/logical/pki/crl_test.go
Alexander Scheel e03fb14be4
Support for generating Delta CRLs (#16773)
* Allow generation of up-to-date delta CRLs

While switching to periodic rebuilds of CRLs alleviates the constant
rebuild pressure on Vault during times of high revocation, the CRL
proper becomes stale. One response to this is to switch to OCSP, but not
every system has support for this. Additionally, OCSP usually requires
connectivity and isn't used to augment a pre-distributed CRL (and is
instead used independently).

By generating delta CRLs containing only new revocations, an existing
CRL can be supplemented with newer revocations without requiring Vault
to rebuild all complete CRLs. Admins can periodically fetch the delta
CRL and add it to the existing CRL and applications should be able to
support using serials from both.

Because delta CRLs are emptied when the next complete CRL is rebuilt, it
is important that applications fetch the delta CRL and correlate it to
their complete CRL; if their complete CRL is older than the delta CRL's
extension number, applications MUST fetch the newer complete CRL to
ensure they have a correct combination.

This modifies the revocation process and adds several new configuration
options, controlling whether Delta CRLs are enabled and when we'll
rebuild it.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add tests for delta CRLs

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add changelog entry

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Add documentation on delta CRLs

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Address review feedback: fix several bugs

Thanks Steve!

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

* Correctly invoke periodic func on active nodes

We need to ensure we read the updated config (in case of OCSP request
handling on standby nodes), but otherwise want to avoid CRL/DeltaCRL
re-building.

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>

Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
2022-08-29 11:37:09 -04:00

1293 lines
41 KiB
Go

package pki
import (
"context"
"encoding/asn1"
"encoding/json"
"fmt"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/api"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
"github.com/stretchr/testify/require"
)
func TestBackend_CRL_EnableDisableRoot(t *testing.T) {
b, s := createBackendWithStorage(t)
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"ttl": "40h",
"common_name": "myvault.com",
})
if err != nil {
t.Fatal(err)
}
caSerial := resp.Data["serial_number"].(string)
crlEnableDisableTestForBackend(t, b, s, []string{caSerial})
}
func TestBackend_CRLConfigUpdate(t *testing.T) {
b, s := createBackendWithStorage(t)
// Write a legacy config to storage.
type legacyConfig struct {
Expiry string `json:"expiry"`
Disable bool `json:"disable"`
}
oldConfig := legacyConfig{Expiry: "24h", Disable: false}
entry, err := logical.StorageEntryJSON("config/crl", oldConfig)
require.NoError(t, err, "generate storage entry objection with legacy config")
err = s.Put(ctx, entry)
require.NoError(t, err, "failed writing legacy config")
// Now lets read it.
resp, err := CBRead(b, s, "config/crl")
requireSuccessNonNilResponse(t, resp, err)
requireFieldsSetInResp(t, resp, "disable", "expiry", "ocsp_disable", "auto_rebuild", "auto_rebuild_grace_period")
require.Equal(t, "24h", resp.Data["expiry"])
require.Equal(t, false, resp.Data["disable"])
require.Equal(t, defaultCrlConfig.OcspDisable, resp.Data["ocsp_disable"])
require.Equal(t, defaultCrlConfig.OcspExpiry, resp.Data["ocsp_expiry"])
require.Equal(t, defaultCrlConfig.AutoRebuild, resp.Data["auto_rebuild"])
require.Equal(t, defaultCrlConfig.AutoRebuildGracePeriod, resp.Data["auto_rebuild_grace_period"])
}
func TestBackend_CRLConfig(t *testing.T) {
t.Parallel()
tests := []struct {
expiry string
disable bool
ocspDisable bool
ocspExpiry string
autoRebuild bool
autoRebuildGracePeriod string
}{
{expiry: "24h", disable: true, ocspDisable: true, ocspExpiry: "72h", autoRebuild: false, autoRebuildGracePeriod: "36h"},
{expiry: "16h", disable: false, ocspDisable: true, ocspExpiry: "0h", autoRebuild: true, autoRebuildGracePeriod: "1h"},
{expiry: "8h", disable: true, ocspDisable: false, ocspExpiry: "24h", autoRebuild: false, autoRebuildGracePeriod: "24h"},
}
for _, tc := range tests {
name := fmt.Sprintf("%s-%t-%t", tc.expiry, tc.disable, tc.ocspDisable)
t.Run(name, func(t *testing.T) {
b, s := createBackendWithStorage(t)
resp, err := CBWrite(b, s, "config/crl", map[string]interface{}{
"expiry": tc.expiry,
"disable": tc.disable,
"ocsp_disable": tc.ocspDisable,
"ocsp_expiry": tc.ocspExpiry,
"auto_rebuild": tc.autoRebuild,
"auto_rebuild_grace_period": tc.autoRebuildGracePeriod,
})
requireSuccessNilResponse(t, resp, err)
resp, err = CBRead(b, s, "config/crl")
requireSuccessNonNilResponse(t, resp, err)
requireFieldsSetInResp(t, resp, "disable", "expiry", "ocsp_disable", "auto_rebuild", "auto_rebuild_grace_period")
require.Equal(t, tc.expiry, resp.Data["expiry"])
require.Equal(t, tc.disable, resp.Data["disable"])
require.Equal(t, tc.ocspDisable, resp.Data["ocsp_disable"])
require.Equal(t, tc.ocspExpiry, resp.Data["ocsp_expiry"])
require.Equal(t, tc.autoRebuild, resp.Data["auto_rebuild"])
require.Equal(t, tc.autoRebuildGracePeriod, resp.Data["auto_rebuild_grace_period"])
})
}
badValueTests := []struct {
expiry string
disable string
ocspDisable string
ocspExpiry string
autoRebuild string
autoRebuildGracePeriod string
}{
{expiry: "not a duration", disable: "true", ocspDisable: "true", ocspExpiry: "72h", autoRebuild: "true", autoRebuildGracePeriod: "1d"},
{expiry: "16h", disable: "not a boolean", ocspDisable: "true", ocspExpiry: "72h", autoRebuild: "true", autoRebuildGracePeriod: "1d"},
{expiry: "8h", disable: "true", ocspDisable: "not a boolean", ocspExpiry: "72h", autoRebuild: "true", autoRebuildGracePeriod: "1d"},
{expiry: "8h", disable: "true", ocspDisable: "true", ocspExpiry: "not a duration", autoRebuild: "true", autoRebuildGracePeriod: "1d"},
{expiry: "8h", disable: "true", ocspDisable: "true", ocspExpiry: "-1", autoRebuild: "true", autoRebuildGracePeriod: "1d"},
{expiry: "8h", disable: "true", ocspDisable: "true", ocspExpiry: "72h", autoRebuild: "not a boolean", autoRebuildGracePeriod: "1d"},
{expiry: "8h", disable: "true", ocspDisable: "true", ocspExpiry: "-1", autoRebuild: "true", autoRebuildGracePeriod: "not a duration"},
}
for _, tc := range badValueTests {
name := fmt.Sprintf("bad-%s-%s-%s", tc.expiry, tc.disable, tc.ocspDisable)
t.Run(name, func(t *testing.T) {
b, s := createBackendWithStorage(t)
_, err := CBWrite(b, s, "config/crl", map[string]interface{}{
"expiry": tc.expiry,
"disable": tc.disable,
"ocsp_disable": tc.ocspDisable,
"ocsp_expiry": tc.ocspExpiry,
"auto_rebuild": tc.autoRebuild,
"auto_rebuild_grace_period": tc.autoRebuildGracePeriod,
})
require.Error(t, err)
})
}
}
func TestBackend_CRL_AllKeyTypeSigAlgos(t *testing.T) {
type testCase struct {
KeyType string
KeyBits int
SigAlgo string
}
testCases := []testCase{
{"rsa", 2048, "SHA256WithRSA"},
{"rsa", 2048, "SHA384WithRSA"},
{"rsa", 2048, "SHA512WithRSA"},
{"rsa", 2048, "SHA256WithRSAPSS"},
{"rsa", 2048, "SHA384WithRSAPSS"},
{"rsa", 2048, "SHA512WithRSAPSS"},
{"ec", 256, "ECDSAWithSHA256"},
{"ec", 384, "ECDSAWithSHA384"},
{"ec", 521, "ECDSAWithSHA512"},
{"ed25519", 0, "PureEd25519"},
}
for index, tc := range testCases {
t.Logf("tv %v", index)
b, s := createBackendWithStorage(t)
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"ttl": "40h",
"common_name": "myvault.com",
"key_type": tc.KeyType,
"key_bits": tc.KeyBits,
})
if err != nil {
t.Fatalf("tc %v: %v", index, err)
}
caSerial := resp.Data["serial_number"].(string)
_, err = CBPatch(b, s, "issuer/default", map[string]interface{}{
"revocation_signature_algorithm": tc.SigAlgo,
})
if err != nil {
t.Fatalf("tc %v: %v", index, err)
}
crlEnableDisableTestForBackend(t, b, s, []string{caSerial})
crl := getParsedCrlFromBackend(t, b, s, "crl")
if strings.HasSuffix(tc.SigAlgo, "PSS") {
algo := crl.SignatureAlgorithm
pssOid := asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10}
if !algo.Algorithm.Equal(pssOid) {
t.Fatalf("tc %v failed: expected sig-alg to be %v / got %v", index, pssOid, algo)
}
}
}
}
func TestBackend_CRL_EnableDisableIntermediateWithRoot(t *testing.T) {
crlEnableDisableIntermediateTestForBackend(t, true)
}
func TestBackend_CRL_EnableDisableIntermediateWithoutRoot(t *testing.T) {
crlEnableDisableIntermediateTestForBackend(t, false)
}
func crlEnableDisableIntermediateTestForBackend(t *testing.T, withRoot bool) {
b_root, s_root := createBackendWithStorage(t)
resp, err := CBWrite(b_root, s_root, "root/generate/internal", map[string]interface{}{
"ttl": "40h",
"common_name": "myvault.com",
})
if err != nil {
t.Fatal(err)
}
rootSerial := resp.Data["serial_number"].(string)
b_int, s_int := createBackendWithStorage(t)
resp, err = CBWrite(b_int, s_int, "intermediate/generate/internal", 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
resp, err = CBWrite(b_root, s_root, "root/sign-intermediate", map[string]interface{}{
"ttl": "30h",
"csr": intermediateData["csr"],
})
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("expected signed intermediate info")
}
intermediateSignedData := resp.Data
var certs string = intermediateSignedData["certificate"].(string)
caSerial := intermediateSignedData["serial_number"].(string)
caSerials := []string{caSerial}
if withRoot {
intermediateAndRootCert := intermediateSignedData["ca_chain"].([]string)
certs = strings.Join(intermediateAndRootCert, "\n")
caSerials = append(caSerials, rootSerial)
}
resp, err = CBWrite(b_int, s_int, "intermediate/set-signed", map[string]interface{}{
"certificate": certs,
})
crlEnableDisableTestForBackend(t, b_int, s_int, caSerials)
}
func crlEnableDisableTestForBackend(t *testing.T, b *backend, s logical.Storage, caSerials []string) {
var err error
_, err = CBWrite(b, s, "roles/test", map[string]interface{}{
"allow_bare_domains": true,
"allow_subdomains": true,
"allowed_domains": "foobar.com",
"generate_lease": true,
})
if err != nil {
t.Fatal(err)
}
serials := make(map[int]string)
for i := 0; i < 6; i++ {
resp, err := CBWrite(b, s, "issue/test", map[string]interface{}{
"common_name": "test.foobar.com",
})
if err != nil {
t.Fatal(err)
}
serials[i] = resp.Data["serial_number"].(string)
}
test := func(numRevokedExpected int, expectedSerials ...string) {
certList := getParsedCrlFromBackend(t, b, s, "crl").TBSCertList
lenList := len(certList.RevokedCertificates)
if lenList != numRevokedExpected {
t.Fatalf("expected %d revoked certificates, found %d", numRevokedExpected, lenList)
}
for _, serialNum := range expectedSerials {
requireSerialNumberInCRL(t, certList, serialNum)
}
// Since this test assumes a complete CRL was rebuilt, we can grab
// the delta CRL and ensure it is empty.
deltaList := getParsedCrlFromBackend(t, b, s, "crl/delta").TBSCertList
lenDeltaList := len(deltaList.RevokedCertificates)
if lenDeltaList != 0 {
t.Fatalf("expected zero revoked certificates on the delta CRL due to complete CRL rebuild, found %d", lenDeltaList)
}
}
revoke := func(serialIndex int) {
_, err = CBWrite(b, s, "revoke", map[string]interface{}{
"serial_number": serials[serialIndex],
})
if err != nil {
t.Fatal(err)
}
for _, caSerial := range caSerials {
_, err = CBWrite(b, s, "revoke", map[string]interface{}{
"serial_number": caSerial,
})
if err == nil {
t.Fatal("expected error")
}
}
}
toggle := func(disabled bool) {
_, err = CBWrite(b, s, "config/crl", map[string]interface{}{
"disable": disabled,
})
if err != nil {
t.Fatal(err)
}
}
test(0)
revoke(0)
revoke(1)
test(2, serials[0], serials[1])
toggle(true)
test(0)
revoke(2)
revoke(3)
test(0)
toggle(false)
test(4, serials[0], serials[1], serials[2], serials[3])
revoke(4)
revoke(5)
test(6)
toggle(true)
test(0)
toggle(false)
test(6)
// The rotate command should reset the update time of the CRL.
crlCreationTime1 := getParsedCrlFromBackend(t, b, s, "crl").TBSCertList.ThisUpdate
time.Sleep(1 * time.Second)
_, err = CBRead(b, s, "crl/rotate")
require.NoError(t, err)
crlCreationTime2 := getParsedCrlFromBackend(t, b, s, "crl").TBSCertList.ThisUpdate
require.NotEqual(t, crlCreationTime1, crlCreationTime2)
}
func TestBackend_Secondary_CRL_Rebuilding(t *testing.T) {
ctx := context.Background()
b, s := createBackendWithStorage(t)
sc := b.makeStorageContext(ctx, s)
// Write out the issuer/key to storage without going through the api call as replication would.
bundle := genCertBundle(t, b, s)
issuer, _, err := sc.writeCaBundle(bundle, "", "")
require.NoError(t, err)
// Just to validate, before we call the invalidate function, make sure our CRL has not been generated
// and we get a nil response
resp := requestCrlFromBackend(t, s, b)
require.Nil(t, resp.Data["http_raw_body"])
// This should force any calls from now on to rebuild our CRL even a read
b.invalidate(ctx, issuerPrefix+issuer.ID.String())
// Perform the read operation again, we should have a valid CRL now...
resp = requestCrlFromBackend(t, s, b)
crl := parseCrlPemBytes(t, resp.Data["http_raw_body"].([]byte))
require.Equal(t, 0, len(crl.RevokedCertificates))
}
func TestCrlRebuilder(t *testing.T) {
ctx := context.Background()
b, s := createBackendWithStorage(t)
sc := b.makeStorageContext(ctx, s)
// Write out the issuer/key to storage without going through the api call as replication would.
bundle := genCertBundle(t, b, s)
_, _, err := sc.writeCaBundle(bundle, "", "")
require.NoError(t, err)
req := &logical.Request{Storage: s}
cb := newCRLBuilder()
// Force an initial build
err = cb.rebuild(ctx, b, req, true)
require.NoError(t, err, "Failed to rebuild CRL")
resp := requestCrlFromBackend(t, s, b)
crl1 := parseCrlPemBytes(t, resp.Data["http_raw_body"].([]byte))
// We shouldn't rebuild within this call.
err = cb.rebuildIfForced(ctx, b, req)
require.NoError(t, err, "Failed to rebuild if forced CRL")
resp = requestCrlFromBackend(t, s, b)
crl2 := parseCrlPemBytes(t, resp.Data["http_raw_body"].([]byte))
require.Equal(t, crl1.ThisUpdate, crl2.ThisUpdate, "According to the update field, we rebuilt the CRL")
// Make sure we have ticked over to the next second
for {
diff := time.Now().Sub(crl1.ThisUpdate)
if diff.Seconds() >= 1 {
break
}
time.Sleep(100 * time.Millisecond)
}
// This should rebuild the CRL
cb.requestRebuildIfActiveNode(b)
err = cb.rebuildIfForced(ctx, b, req)
require.NoError(t, err, "Failed to rebuild if forced CRL")
resp = requestCrlFromBackend(t, s, b)
crl3 := parseCrlPemBytes(t, resp.Data["http_raw_body"].([]byte))
require.True(t, crl1.ThisUpdate.Before(crl3.ThisUpdate),
"initial crl time: %#v not before next crl rebuild time: %#v", crl1.ThisUpdate, crl3.ThisUpdate)
}
func TestBYOC(t *testing.T) {
t.Parallel()
b, s := createBackendWithStorage(t)
// Create a root CA.
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"common_name": "root example.com",
"issuer_name": "root",
"key_type": "ec",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
oldRoot := resp.Data["certificate"].(string)
// Create a role for issuance.
_, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{
"allow_any_name": true,
"enforce_hostnames": false,
"key_type": "ec",
"ttl": "75s",
"no_store": "true",
})
require.NoError(t, err)
// Issue a leaf cert and ensure we can revoke it.
resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{
"common_name": "testing",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
_, err = CBWrite(b, s, "revoke", map[string]interface{}{
"certificate": resp.Data["certificate"],
})
require.NoError(t, err)
// Issue a second leaf, but hold onto it for now.
resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{
"common_name": "testing2",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
notStoredCert := resp.Data["certificate"].(string)
// Update the role to make things stored and issue another cert.
_, err = CBWrite(b, s, "roles/stored-testing", map[string]interface{}{
"allow_any_name": true,
"enforce_hostnames": false,
"key_type": "ec",
"ttl": "75s",
"no_store": "false",
})
require.NoError(t, err)
// Issue a leaf cert and ensure we can revoke it.
resp, err = CBWrite(b, s, "issue/stored-testing", map[string]interface{}{
"common_name": "testing",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
storedCert := resp.Data["certificate"].(string)
// Delete the root and regenerate a new one.
_, err = CBDelete(b, s, "issuer/default")
require.NoError(t, err)
resp, err = CBList(b, s, "issuers")
require.NoError(t, err)
require.Equal(t, len(resp.Data), 0)
_, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"common_name": "root2 example.com",
"issuer_name": "root2",
"key_type": "ec",
})
require.NoError(t, err)
// Issue a new leaf and revoke that one.
resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{
"common_name": "testing3",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
_, err = CBWrite(b, s, "revoke", map[string]interface{}{
"certificate": resp.Data["certificate"],
})
require.NoError(t, err)
// Now attempt to revoke the earlier leaves. The first should fail since
// we deleted its issuer, but the stored one should succeed.
_, err = CBWrite(b, s, "revoke", map[string]interface{}{
"certificate": notStoredCert,
})
require.Error(t, err)
_, err = CBWrite(b, s, "revoke", map[string]interface{}{
"certificate": storedCert,
})
require.NoError(t, err)
// Import the old root again and revoke the no stored leaf should work.
_, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{
"pem_bundle": oldRoot,
})
require.NoError(t, err)
_, err = CBWrite(b, s, "revoke", map[string]interface{}{
"certificate": notStoredCert,
})
require.NoError(t, err)
}
func TestPoP(t *testing.T) {
t.Parallel()
b, s := createBackendWithStorage(t)
// Create a root CA.
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"common_name": "root example.com",
"issuer_name": "root",
"key_type": "ec",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
oldRoot := resp.Data["certificate"].(string)
// Create a role for issuance.
_, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{
"allow_any_name": true,
"enforce_hostnames": false,
"key_type": "ec",
"ttl": "75s",
"no_store": "true",
})
require.NoError(t, err)
// Issue a leaf cert and ensure we can revoke it with the private key and
// an explicit certificate.
resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{
"common_name": "testing1",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
"certificate": resp.Data["certificate"],
"private_key": resp.Data["private_key"],
})
require.NoError(t, err)
// Issue a second leaf, but hold onto it for now.
resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{
"common_name": "testing2",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
notStoredCert := resp.Data["certificate"].(string)
notStoredKey := resp.Data["private_key"].(string)
// Update the role to make things stored and issue another cert.
_, err = CBWrite(b, s, "roles/stored-testing", map[string]interface{}{
"allow_any_name": true,
"enforce_hostnames": false,
"key_type": "ec",
"ttl": "75s",
"no_store": "false",
})
require.NoError(t, err)
// Issue a leaf and ensure we can revoke it via serial number and private key.
resp, err = CBWrite(b, s, "issue/stored-testing", map[string]interface{}{
"common_name": "testing3",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
require.NotEmpty(t, resp.Data["serial_number"])
require.NotEmpty(t, resp.Data["private_key"])
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
"serial_number": resp.Data["serial_number"],
"private_key": resp.Data["private_key"],
})
require.NoError(t, err)
// Issue a leaf cert and ensure we can revoke it after removing its root;
// hold onto it for now.
resp, err = CBWrite(b, s, "issue/stored-testing", map[string]interface{}{
"common_name": "testing4",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
storedCert := resp.Data["certificate"].(string)
storedKey := resp.Data["private_key"].(string)
// Delete the root and regenerate a new one.
_, err = CBDelete(b, s, "issuer/default")
require.NoError(t, err)
resp, err = CBList(b, s, "issuers")
require.NoError(t, err)
require.Equal(t, len(resp.Data), 0)
_, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"common_name": "root2 example.com",
"issuer_name": "root2",
"key_type": "ec",
})
require.NoError(t, err)
// Issue a new leaf and revoke that one.
resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{
"common_name": "testing5",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
"certificate": resp.Data["certificate"],
"private_key": resp.Data["private_key"],
})
require.NoError(t, err)
// Now attempt to revoke the earlier leaves. The first should fail since
// we deleted its issuer, but the stored one should succeed.
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
"certificate": notStoredCert,
"private_key": notStoredKey,
})
require.Error(t, err)
// Incorrect combination (stored with not stored key) should fail.
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
"certificate": storedCert,
"private_key": notStoredKey,
})
require.Error(t, err)
// Correct combination (stored with stored) should succeed.
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
"certificate": storedCert,
"private_key": storedKey,
})
require.NoError(t, err)
// Import the old root again and revoke the no stored leaf should work.
_, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{
"pem_bundle": oldRoot,
})
require.NoError(t, err)
// Incorrect combination (not stored with stored key) should fail.
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
"certificate": notStoredCert,
"private_key": storedKey,
})
require.Error(t, err)
// Correct combination (not stored with not stored) should succeed.
_, err = CBWrite(b, s, "revoke-with-key", map[string]interface{}{
"certificate": notStoredCert,
"private_key": notStoredKey,
})
require.NoError(t, err)
}
func TestIssuerRevocation(t *testing.T) {
t.Parallel()
b, s := createBackendWithStorage(t)
// Create a root CA.
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"common_name": "root example.com",
"issuer_name": "root",
"key_type": "ec",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
require.NotEmpty(t, resp.Data["serial_number"])
// oldRoot := resp.Data["certificate"].(string)
oldRootSerial := resp.Data["serial_number"].(string)
// Create a second root CA. We'll revoke this one and ensure it
// doesn't appear on the former's CRL.
resp, err = CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"common_name": "root2 example.com",
"issuer_name": "root2",
"key_type": "ec",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
require.NotEmpty(t, resp.Data["serial_number"])
// revokedRoot := resp.Data["certificate"].(string)
revokedRootSerial := resp.Data["serial_number"].(string)
// Shouldn't be able to revoke it by serial number.
_, err = CBWrite(b, s, "revoke", map[string]interface{}{
"serial_number": revokedRootSerial,
})
require.Error(t, err)
// Revoke it.
resp, err = CBWrite(b, s, "issuer/root2/revoke", map[string]interface{}{})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotZero(t, resp.Data["revocation_time"])
// Regenerate the CRLs
_, err = CBRead(b, s, "crl/rotate")
require.NoError(t, err)
// Ensure the old cert isn't on its own CRL.
crl := getParsedCrlFromBackend(t, b, s, "issuer/root2/crl/der")
if requireSerialNumberInCRL(nil, crl.TBSCertList, revokedRootSerial) {
t.Fatalf("the serial number %v should not be on its own CRL as self-CRL appearance should not occur", revokedRootSerial)
}
// Ensure the old cert isn't on the one's CRL.
crl = getParsedCrlFromBackend(t, b, s, "issuer/root/crl/der")
if requireSerialNumberInCRL(nil, crl.TBSCertList, revokedRootSerial) {
t.Fatalf("the serial number %v should not be on %v's CRL as they're separate roots", revokedRootSerial, oldRootSerial)
}
// Create a role and ensure we can't use the revoked root.
_, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{
"allow_any_name": true,
"enforce_hostnames": false,
"key_type": "ec",
"ttl": "75s",
})
require.NoError(t, err)
// Issue a leaf cert and ensure it fails (because the issuer is revoked).
_, err = CBWrite(b, s, "issuer/root2/issue/local-testing", map[string]interface{}{
"common_name": "testing",
})
require.Error(t, err)
// Issue an intermediate and ensure we can revoke it.
resp, err = CBWrite(b, s, "intermediate/generate/internal", map[string]interface{}{
"common_name": "intermediate example.com",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["csr"])
intCsr := resp.Data["csr"].(string)
resp, err = CBWrite(b, s, "root/sign-intermediate", map[string]interface{}{
"ttl": "30h",
"csr": intCsr,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
require.NotEmpty(t, resp.Data["serial_number"])
intCert := resp.Data["certificate"].(string)
intCertSerial := resp.Data["serial_number"].(string)
resp, err = CBWrite(b, s, "intermediate/set-signed", map[string]interface{}{
"certificate": intCert,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["imported_issuers"])
importedIssuers := resp.Data["imported_issuers"].([]string)
require.Equal(t, len(importedIssuers), 1)
intId := importedIssuers[0]
_, err = CBPatch(b, s, "issuer/"+intId, map[string]interface{}{
"issuer_name": "int1",
})
require.NoError(t, err)
// Now issue a leaf with the intermediate.
resp, err = CBWrite(b, s, "issuer/int1/issue/local-testing", map[string]interface{}{
"common_name": "testing",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
require.NotEmpty(t, resp.Data["serial_number"])
issuedSerial := resp.Data["serial_number"].(string)
// Now revoke the intermediate.
resp, err = CBWrite(b, s, "issuer/int1/revoke", map[string]interface{}{})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotZero(t, resp.Data["revocation_time"])
// Update the CRLs and ensure it appears.
_, err = CBRead(b, s, "crl/rotate")
require.NoError(t, err)
crl = getParsedCrlFromBackend(t, b, s, "issuer/root/crl/der")
requireSerialNumberInCRL(t, crl.TBSCertList, intCertSerial)
// Ensure we can still revoke the issued leaf.
resp, err = CBWrite(b, s, "revoke", map[string]interface{}{
"serial_number": issuedSerial,
})
require.NoError(t, err)
require.NotNil(t, resp)
// Ensure it appears on the intermediate's CRL.
_, err = CBRead(b, s, "crl/rotate")
require.NoError(t, err)
crl = getParsedCrlFromBackend(t, b, s, "issuer/int1/crl/der")
requireSerialNumberInCRL(t, crl.TBSCertList, issuedSerial)
// Ensure we can't fetch the intermediate's cert by serial any more.
resp, err = CBRead(b, s, "cert/"+intCertSerial)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["revocation_time"])
}
func TestAutoRebuild(t *testing.T) {
t.Parallel()
// While we'd like to reduce this duration, we need to wait until
// the rollback manager timer ticks. With the new helper, we can
// modify the rollback manager timer period directly, allowing us
// to shorten the total test time significantly.
//
// We set the delta CRL time to ensure it executes prior to the
// main CRL rebuild, and the new CRL doesn't rebuild until after
// we're done.
newPeriod := 1 * time.Second
deltaPeriod := (newPeriod + 1*time.Second).String()
crlTime := (6*newPeriod + 2*time.Second).String()
gracePeriod := (3 * newPeriod).String()
delta := 2 * newPeriod
// This test requires the periodicFunc to trigger, which requires we stand
// up a full test cluster.
coreConfig := &vault.CoreConfig{
LogicalBackends: map[string]logical.Factory{
"pki": Factory,
},
// See notes below about usage of /sys/raw for reading cluster
// storage without barrier encryption.
EnableRaw: true,
}
oldPeriod := vault.SetRollbackPeriodForTesting(newPeriod)
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
vault.SetRollbackPeriodForTesting(oldPeriod)
// Mount PKI
err := client.Sys().Mount("pki", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
DefaultLeaseTTL: "16h",
MaxLeaseTTL: "60h",
},
})
require.NoError(t, err)
// Generate root.
resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
"ttl": "40h",
"common_name": "Root X1",
"key_type": "ec",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data)
require.NotEmpty(t, resp.Data["issuer_id"])
rootIssuer := resp.Data["issuer_id"].(string)
// Setup a testing role.
_, err = client.Logical().Write("pki/roles/local-testing", map[string]interface{}{
"allow_any_name": true,
"enforce_hostnames": false,
"key_type": "ec",
})
require.NoError(t, err)
// Regression test: ensure we respond with the default values for CRL
// config when we haven't set any values yet.
resp, err = client.Logical().Read("pki/config/crl")
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.Equal(t, resp.Data["expiry"], defaultCrlConfig.Expiry)
require.Equal(t, resp.Data["disable"], defaultCrlConfig.Disable)
require.Equal(t, resp.Data["ocsp_disable"], defaultCrlConfig.OcspDisable)
require.Equal(t, resp.Data["auto_rebuild"], defaultCrlConfig.AutoRebuild)
require.Equal(t, resp.Data["auto_rebuild_grace_period"], defaultCrlConfig.AutoRebuildGracePeriod)
require.Equal(t, resp.Data["enable_delta"], defaultCrlConfig.EnableDelta)
require.Equal(t, resp.Data["delta_rebuild_interval"], defaultCrlConfig.DeltaRebuildInterval)
// Safety guard: we play with rebuild timing below.
_, err = client.Logical().Write("pki/config/crl", map[string]interface{}{
"expiry": crlTime,
})
require.NoError(t, err)
// Issue a cert and revoke it. It should appear on the CRL right away.
resp, err = client.Logical().Write("pki/issue/local-testing", map[string]interface{}{
"common_name": "example.com",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.NotEmpty(t, resp.Data["serial_number"])
leafSerial := resp.Data["serial_number"].(string)
_, err = client.Logical().Write("pki/revoke", map[string]interface{}{
"serial_number": leafSerial,
})
require.NoError(t, err)
crl := getCrlCertificateList(t, client, "pki")
lastCRLNumber := crl.Version
lastCRLExpiry := crl.NextUpdate
requireSerialNumberInCRL(t, crl, leafSerial)
// Enable periodic rebuild of the CRL.
_, err = client.Logical().Write("pki/config/crl", map[string]interface{}{
"expiry": crlTime,
"auto_rebuild": true,
"auto_rebuild_grace_period": gracePeriod,
"enable_delta": true,
"delta_rebuild_interval": deltaPeriod,
})
require.NoError(t, err)
// Issue a cert and revoke it.
resp, err = client.Logical().Write("pki/issue/local-testing", map[string]interface{}{
"common_name": "example.com",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.NotEmpty(t, resp.Data["serial_number"])
newLeafSerial := resp.Data["serial_number"].(string)
_, err = client.Logical().Write("pki/revoke", map[string]interface{}{
"serial_number": newLeafSerial,
})
require.NoError(t, err)
// Now, we want to test the issuer identification on revocation. This
// only happens as a distinct "step" when CRL building isn't done on
// each revocation. Pull the storage from the cluster (via the sys/raw
// endpoint which requires the mount UUID) and verify the revInfo contains
// a matching issuer.
resp, err = client.Logical().Read("sys/mounts/pki")
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.NotEmpty(t, resp.Data["uuid"])
pkiMount := resp.Data["uuid"].(string)
require.NotEmpty(t, pkiMount)
revEntryPath := "logical/" + pkiMount + "/" + revokedPath + normalizeSerial(newLeafSerial)
// storage from cluster.Core[0] is a physical storage copy, not a logical
// storage. This difference means, if we were to do a storage.Get(...)
// on the above path, we'd read the barrier-encrypted value. This is less
// than useful for decoding, and fetching the proper storage view is a
// touch much work. So, assert EnableRaw above and (ab)use it here.
resp, err = client.Logical().Read("sys/raw/" + revEntryPath)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.NotEmpty(t, resp.Data["value"])
revEntryValue := resp.Data["value"].(string)
var revInfo revocationInfo
err = json.Unmarshal([]byte(revEntryValue), &revInfo)
require.NoError(t, err)
require.Equal(t, revInfo.CertificateIssuer, issuerID(rootIssuer))
// New serial should not appear on CRL.
crl = getCrlCertificateList(t, client, "pki")
thisCRLNumber := crl.Version
requireSerialNumberInCRL(t, crl, leafSerial) // But the old one should.
now := time.Now()
graceInterval, _ := time.ParseDuration(gracePeriod)
expectedUpdate := lastCRLExpiry.Add(-1 * graceInterval)
if requireSerialNumberInCRL(nil, crl, newLeafSerial) {
// If we somehow lagged and we ended up needing to rebuild
// the CRL, we should avoid throwing an error.
if thisCRLNumber == lastCRLNumber {
t.Fatalf("unexpected failure: last (%v) and current (%v) leaf certificate might have the same serial number?", leafSerial, newLeafSerial)
}
if !now.After(expectedUpdate) {
t.Fatalf("expected newly generated certificate with serial %v not to appear on this CRL but it did, prematurely: %v", newLeafSerial, crl)
}
t.Fatalf("shouldn't be here")
}
// This serial should exist in the delta WAL section for the mount...
resp, err = client.Logical().List("sys/raw/logical/" + pkiMount + "/" + deltaWALPath)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data)
require.NotEmpty(t, resp.Data["keys"])
require.Contains(t, resp.Data["keys"], normalizeSerial(newLeafSerial))
haveUpdatedDeltaCRL := false
interruptChan := time.After(4*newPeriod + delta)
for {
if haveUpdatedDeltaCRL {
break
}
select {
case <-interruptChan:
t.Fatalf("expected to regenerate delta CRL within a couple of periodicFunc invocations (plus %v grace period)", delta)
default:
// Check and see if there's a storage entry for the last rebuild
// serial. If so, validate the delta CRL contains this entry.
resp, err = client.Logical().List("sys/raw/logical/" + pkiMount + "/" + deltaWALPath)
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data)
require.NotEmpty(t, resp.Data["keys"])
haveRebuildMarker := false
for _, rawEntry := range resp.Data["keys"].([]interface{}) {
entry := rawEntry.(string)
if entry == deltaWALLastRevokedSerialName {
haveRebuildMarker = true
break
}
}
if !haveRebuildMarker {
time.Sleep(1 * time.Second)
continue
}
// Read the marker and see if its correct.
resp, err = client.Logical().Read("sys/raw/logical/" + pkiMount + "/" + deltaWALLastBuildSerial)
require.NoError(t, err)
if resp == nil {
time.Sleep(1 * time.Second)
continue
}
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data)
require.NotEmpty(t, resp.Data["value"])
// Easy than JSON decoding...
if !strings.Contains(resp.Data["value"].(string), newLeafSerial) {
time.Sleep(1 * time.Second)
continue
}
haveUpdatedDeltaCRL = true
// Ensure it has what we want.
deltaCrl := getParsedCrlAtPath(t, client, "/v1/pki/crl/delta").TBSCertList
if !requireSerialNumberInCRL(nil, deltaCrl, newLeafSerial) {
// Check if it is on the main CRL because its already regenerated.
mainCRL := getParsedCrlAtPath(t, client, "/v1/pki/crl").TBSCertList
requireSerialNumberInCRL(t, mainCRL, newLeafSerial)
}
break
}
}
// Now, wait until we're within the grace period... Then start prompting
// for regeneration.
if expectedUpdate.After(now) {
time.Sleep(expectedUpdate.Sub(now))
}
// Otherwise, the absolute latest we're willing to wait is some delta
// after CRL expiry (to let stuff regenerate &c).
interruptChan = time.After(lastCRLExpiry.Sub(now) + delta)
for {
select {
case <-interruptChan:
t.Fatalf("expected CRL to regenerate prior to CRL expiry (plus %v grace period)", delta)
default:
crl = getCrlCertificateList(t, client, "pki")
if crl.NextUpdate.Equal(lastCRLExpiry) {
// Hack to ensure we got a net-new CRL. If we didn't, we can
// exit this default conditional and wait for the next
// go-round. When the timer fires, it'll populate the channel
// and we'll exit correctly.
time.Sleep(1 * time.Second)
break
}
now := time.Now()
require.True(t, crl.ThisUpdate.Before(now))
require.True(t, crl.NextUpdate.After(now))
requireSerialNumberInCRL(t, crl, leafSerial)
requireSerialNumberInCRL(t, crl, newLeafSerial)
return
}
}
}
func TestTidyIssuerAssociation(t *testing.T) {
t.Parallel()
b, s := createBackendWithStorage(t)
// Create a root CA.
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"common_name": "root example.com",
"issuer_name": "root",
"key_type": "ec",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["certificate"])
require.NotEmpty(t, resp.Data["issuer_id"])
rootCert := resp.Data["certificate"].(string)
rootID := resp.Data["issuer_id"].(issuerID)
// Create a role for issuance.
_, err = CBWrite(b, s, "roles/local-testing", map[string]interface{}{
"allow_any_name": true,
"enforce_hostnames": false,
"key_type": "ec",
"ttl": "75m",
})
require.NoError(t, err)
// Issue a leaf cert and ensure we can revoke it.
resp, err = CBWrite(b, s, "issue/local-testing", map[string]interface{}{
"common_name": "testing",
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotEmpty(t, resp.Data["serial_number"])
leafSerial := resp.Data["serial_number"].(string)
_, err = CBWrite(b, s, "revoke", map[string]interface{}{
"serial_number": leafSerial,
})
require.NoError(t, err)
// This leaf's revInfo entry should have an issuer associated
// with it.
entry, err := s.Get(ctx, revokedPath+normalizeSerial(leafSerial))
require.NoError(t, err)
require.NotNil(t, entry)
require.NotNil(t, entry.Value)
var leafInfo revocationInfo
err = entry.DecodeJSON(&leafInfo)
require.NoError(t, err)
require.Equal(t, rootID, leafInfo.CertificateIssuer)
// Now remove the root and run tidy.
_, err = CBDelete(b, s, "issuer/default")
require.NoError(t, err)
_, err = CBWrite(b, s, "tidy", map[string]interface{}{
"tidy_revoked_cert_issuer_associations": true,
})
require.NoError(t, err)
// Wait for tidy to finish.
for {
time.Sleep(125 * time.Millisecond)
resp, err = CBRead(b, s, "tidy-status")
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.NotEmpty(t, resp.Data["state"])
state := resp.Data["state"].(string)
if state == "Finished" {
break
}
if state == "Error" {
t.Fatalf("unexpected state for tidy operation: Error:\nStatus: %v", resp.Data)
}
}
// Ensure we don't have an association on this leaf any more.
entry, err = s.Get(ctx, revokedPath+normalizeSerial(leafSerial))
require.NoError(t, err)
require.NotNil(t, entry)
require.NotNil(t, entry.Value)
err = entry.DecodeJSON(&leafInfo)
require.NoError(t, err)
require.Empty(t, leafInfo.CertificateIssuer)
// Now, re-import the root and try again.
resp, err = CBWrite(b, s, "issuers/import/bundle", map[string]interface{}{
"pem_bundle": rootCert,
})
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.NotNil(t, resp.Data["imported_issuers"])
importedIssuers := resp.Data["imported_issuers"].([]string)
require.Equal(t, 1, len(importedIssuers))
newRootID := importedIssuers[0]
require.NotEmpty(t, newRootID)
// Re-run tidy...
_, err = CBWrite(b, s, "tidy", map[string]interface{}{
"tidy_revoked_cert_issuer_associations": true,
})
require.NoError(t, err)
// Wait for tidy to finish.
for {
time.Sleep(125 * time.Millisecond)
resp, err = CBRead(b, s, "tidy-status")
require.NoError(t, err)
require.NotNil(t, resp)
require.NotNil(t, resp.Data)
require.NotEmpty(t, resp.Data["state"])
state := resp.Data["state"].(string)
if state == "Finished" {
break
}
if state == "Error" {
t.Fatalf("unexpected state for tidy operation: Error:\nStatus: %v", resp.Data)
}
}
// Finally, double-check we associated things correctly.
entry, err = s.Get(ctx, revokedPath+normalizeSerial(leafSerial))
require.NoError(t, err)
require.NotNil(t, entry)
require.NotNil(t, entry.Value)
err = entry.DecodeJSON(&leafInfo)
require.NoError(t, err)
require.Equal(t, newRootID, string(leafInfo.CertificateIssuer))
}
func requestCrlFromBackend(t *testing.T, s logical.Storage, b *backend) *logical.Response {
crlReq := &logical.Request{
Operation: logical.ReadOperation,
Path: "crl/pem",
Storage: s,
}
resp, err := b.HandleRequest(context.Background(), crlReq)
require.NoError(t, err, "crl req failed with an error")
require.NotNil(t, resp, "crl response was nil with no error")
require.False(t, resp.IsError(), "crl error response: %v", resp)
return resp
}