231f422822
Previously we used the global backend-set crlLifetime as a default value. However, this was refactored into a new defaultCrlConfig instead, which we should reply with when the CRL configuration has not been set yet. In particular, the 72h default expiry (and new 12h auto-rebuild grace period) was added and made explicit. This fixes the broken UI test. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
976 lines
30 KiB
Go
976 lines
30 KiB
Go
package pki
|
|
|
|
import (
|
|
"context"
|
|
"encoding/asn1"
|
|
"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_CRLConfig(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
expiry string
|
|
disable bool
|
|
ocspDisable bool
|
|
}{
|
|
{expiry: "24h", disable: true, ocspDisable: true},
|
|
{expiry: "16h", disable: false, ocspDisable: true},
|
|
{expiry: "8h", disable: true, ocspDisable: false},
|
|
}
|
|
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,
|
|
})
|
|
requireSuccessNilResponse(t, resp, err)
|
|
|
|
resp, err = CBRead(b, s, "config/crl")
|
|
requireSuccessNonNilResponse(t, resp, err)
|
|
requireFieldsSetInResp(t, resp, "disable", "expiry", "ocsp_disable")
|
|
|
|
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"])
|
|
})
|
|
}
|
|
|
|
badValueTests := []struct {
|
|
expiry string
|
|
disable string
|
|
ocspDisable string
|
|
}{
|
|
{expiry: "not a duration", disable: "true", ocspDisable: "true"},
|
|
{expiry: "16h", disable: "not a boolean", ocspDisable: "true"},
|
|
{expiry: "8h", disable: "true", ocspDisable: "not a boolean"},
|
|
}
|
|
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,
|
|
})
|
|
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)
|
|
}
|
|
}
|
|
|
|
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 := crlBuilder{}
|
|
|
|
// 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 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.
|
|
newPeriod := 6 * time.Second
|
|
gracePeriod := (newPeriod + 1*time.Second).String()
|
|
crlTime := (newPeriod + 2*time.Second).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,
|
|
},
|
|
}
|
|
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.
|
|
_, err = client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
|
|
"ttl": "40h",
|
|
"common_name": "Root X1",
|
|
"key_type": "ec",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// 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)
|
|
|
|
// Safety guard: we play with rebuild timing below. We don't expect
|
|
// this entire test to take longer than 80s.
|
|
_, 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,
|
|
})
|
|
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)
|
|
|
|
// Serial should not appear on CRL.
|
|
crl = getCrlCertificateList(t, client, "pki")
|
|
thisCRLNumber := crl.Version
|
|
requireSerialNumberInCRL(t, crl, leafSerial)
|
|
|
|
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")
|
|
}
|
|
|
|
// 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 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
|
|
}
|