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

513 lines
19 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package pki
import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/base64"
"fmt"
"math/big"
"testing"
"time"
"github.com/hashicorp/vault/sdk/helper/testhelpers/schema"
"github.com/hashicorp/vault/api"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/vault"
"github.com/hashicorp/vault/sdk/logical"
"github.com/stretchr/testify/require"
)
func TestResignCrls_ForbidSigningOtherIssuerCRL(t *testing.T) {
t.Parallel()
// Some random CRL from another issuer
pem1 := "-----BEGIN X509 CRL-----\nMIIBvjCBpwIBATANBgkqhkiG9w0BAQsFADAbMRkwFwYDVQQDExByb290LWV4YW1w\nbGUuY29tFw0yMjEwMjYyMTI5MzlaFw0yMjEwMjkyMTI5MzlaMCcwJQIUSnVf8wsd\nHjOt9drCYFhWxS9QqGoXDTIyMTAyNjIxMjkzOVqgLzAtMB8GA1UdIwQYMBaAFHki\nZ0XDUQVSajNRGXrg66OaIFlYMAoGA1UdFAQDAgEDMA0GCSqGSIb3DQEBCwUAA4IB\nAQBGIdtqTwemnLZF5AoP+jzvKZ26S3y7qvRIzd7f4A0EawzYmWXSXfwqo4TQ4DG3\nnvT+AaA1zCCOlH/1U+ufN9gSSN0j9ax58brSYMnMskMCqhLKIp0qnvS4jr/gopmF\nv8grbvLHEqNYTu1T7umMLdNQUsWT3Qc+EIjfoKj8xD2FHsZwJ+EMbytwl8Unipjr\nhz4rmcES/65vavfdFpOI6YXfi+UAaHBdkTqmHgg4BdpuXfYtlf+iotFSOkygD5fl\n0D+RVFW9uJv2WfbQ7kRt1X/VcFk/onw0AQqxZRVUzvjoMw+EMcxSq3UKOlXcWDxm\nEFz9rFQQ66L388EP8RD7Dh3X\n-----END X509 CRL-----"
b, s := CreateBackendWithStorage(t)
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"common_name": "test.com",
"key_type": "ec",
})
requireSuccessNonNilResponse(t, resp, err)
resp, err = CBWrite(b, s, "issuer/default/resign-crls", map[string]interface{}{
"crl_number": "2",
"next_update": "1h",
"format": "pem",
"crls": []string{pem1},
})
require.ErrorContains(t, err, "was not signed by requested issuer")
}
func TestResignCrls_NormalCrl(t *testing.T) {
t.Parallel()
b1, s1 := CreateBackendWithStorage(t)
b2, s2 := CreateBackendWithStorage(t)
// Setup two backends, with the same key material/certificate with a different leaf in each that is revoked.
caCert, serial1, serial2, crl1, crl2 := setupResignCrlMounts(t, b1, s1, b2, s2)
// Attempt to combine the CRLs
resp, err := CBWrite(b1, s1, "issuer/default/resign-crls", map[string]interface{}{
"crl_number": "2",
"next_update": "1h",
"format": "pem",
"crls": []string{crl1, crl2},
})
schema.ValidateResponse(t, schema.GetResponseSchema(t, b1.Route("issuer/default/resign-crls"), logical.UpdateOperation), resp, true)
requireSuccessNonNilResponse(t, resp, err)
requireFieldsSetInResp(t, resp, "crl")
pemCrl := resp.Data["crl"].(string)
combinedCrl, err := decodePemCrl(pemCrl)
require.NoError(t, err, "failed decoding combined CRL")
serials := extractSerialsFromCrl(t, combinedCrl)
require.Contains(t, serials, serial1)
require.Contains(t, serials, serial2)
require.Equal(t, 2, len(serials), "serials contained more serials than expected")
require.Equal(t, big.NewInt(int64(2)), combinedCrl.Number)
require.Equal(t, combinedCrl.ThisUpdate.Add(1*time.Hour), combinedCrl.NextUpdate)
extensions := combinedCrl.Extensions
requireExtensionOid(t, []int{2, 5, 29, 20}, extensions) // CRL Number Extension
requireExtensionOid(t, []int{2, 5, 29, 35}, extensions) // akidOid
require.Equal(t, 2, len(extensions))
err = combinedCrl.CheckSignatureFrom(caCert)
require.NoError(t, err, "failed signature check of CRL")
}
func TestResignCrls_EliminateDuplicates(t *testing.T) {
t.Parallel()
b1, s1 := CreateBackendWithStorage(t)
b2, s2 := CreateBackendWithStorage(t)
// Setup two backends, with the same key material/certificate with a different leaf in each that is revoked.
_, serial1, _, crl1, _ := setupResignCrlMounts(t, b1, s1, b2, s2)
// Pass in the same CRLs to make sure we do not duplicate entries
resp, err := CBWrite(b1, s1, "issuer/default/resign-crls", map[string]interface{}{
"crl_number": "2",
"next_update": "1h",
"format": "pem",
"crls": []string{crl1, crl1},
})
requireSuccessNonNilResponse(t, resp, err)
requireFieldsSetInResp(t, resp, "crl")
pemCrl := resp.Data["crl"].(string)
combinedCrl, err := decodePemCrl(pemCrl)
require.NoError(t, err, "failed decoding combined CRL")
// Technically this will die if we have duplicates.
serials := extractSerialsFromCrl(t, combinedCrl)
// We should have no warnings about collisions if they have the same revoked time
require.Empty(t, resp.Warnings, "expected no warnings in response")
require.Contains(t, serials, serial1)
require.Equal(t, 1, len(serials), "serials contained more serials than expected")
}
func TestResignCrls_ConflictingExpiry(t *testing.T) {
t.Parallel()
b1, s1 := CreateBackendWithStorage(t)
b2, s2 := CreateBackendWithStorage(t)
// Setup two backends, with the same key material/certificate with a different leaf in each that is revoked.
_, serial1, serial2, crl1, _ := setupResignCrlMounts(t, b1, s1, b2, s2)
timeAfterMountSetup := time.Now()
// Read in serial1 from mount 1
resp, err := CBRead(b1, s1, "cert/"+serial1)
requireSuccessNonNilResponse(t, resp, err, "failed reading serial 1's certificate")
requireFieldsSetInResp(t, resp, "certificate")
cert1 := resp.Data["certificate"].(string)
// Wait until at least we have rolled over to the next second to match sure the generated CRL time
// on backend 2 for the serial 1 will be different
for {
if time.Now().After(timeAfterMountSetup.Add(1 * time.Second)) {
break
}
}
// Use BYOC to revoke the same certificate on backend 2 now
resp, err = CBWrite(b2, s2, "revoke", map[string]interface{}{
"certificate": cert1,
})
requireSuccessNonNilResponse(t, resp, err, "failed revoking serial 1 on backend 2")
// Fetch the new CRL from backend2 now
resp, err = CBRead(b2, s2, "cert/crl")
requireSuccessNonNilResponse(t, resp, err, "error fetch crl from backend 2")
requireFieldsSetInResp(t, resp, "certificate")
crl2 := resp.Data["certificate"].(string)
// Attempt to combine the CRLs
resp, err = CBWrite(b1, s1, "issuer/default/resign-crls", map[string]interface{}{
"crl_number": "2",
"next_update": "1h",
"format": "pem",
"crls": []string{crl2, crl1}, // Make sure we don't just grab the first colliding entry...
})
requireSuccessNonNilResponse(t, resp, err)
requireFieldsSetInResp(t, resp, "crl")
pemCrl := resp.Data["crl"].(string)
combinedCrl, err := decodePemCrl(pemCrl)
require.NoError(t, err, "failed decoding combined CRL")
combinedSerials := extractSerialsFromCrl(t, combinedCrl)
require.Contains(t, combinedSerials, serial1)
require.Contains(t, combinedSerials, serial2)
require.Equal(t, 2, len(combinedSerials), "serials contained more serials than expected")
// Make sure we issued a warning about the time collision
require.NotEmpty(t, resp.Warnings, "expected at least one warning")
require.Contains(t, resp.Warnings[0], "different revocation times detected")
// Make sure we have the initial revocation time from backend 1 within the combined CRL.
decodedCrl1, err := decodePemCrl(crl1)
require.NoError(t, err, "failed decoding crl from backend 1")
serialsFromBackend1 := extractSerialsFromCrl(t, decodedCrl1)
require.Equal(t, serialsFromBackend1[serial1], combinedSerials[serial1])
// Make sure we have the initial revocation time from backend 1 does not match with backend 2's time
decodedCrl2, err := decodePemCrl(crl2)
require.NoError(t, err, "failed decoding crl from backend 2")
serialsFromBackend2 := extractSerialsFromCrl(t, decodedCrl2)
require.NotEqual(t, serialsFromBackend1[serial1], serialsFromBackend2[serial1])
}
func TestResignCrls_DeltaCrl(t *testing.T) {
t.Parallel()
b1, s1 := CreateBackendWithStorage(t)
b2, s2 := CreateBackendWithStorage(t)
// Setup two backends, with the same key material/certificate with a different leaf in each that is revoked.
caCert, serial1, serial2, crl1, crl2 := setupResignCrlMounts(t, b1, s1, b2, s2)
resp, err := CBWrite(b1, s1, "issuer/default/resign-crls", map[string]interface{}{
"crl_number": "5",
"delta_crl_base_number": "4",
"next_update": "12h",
"format": "pem",
"crls": []string{crl1, crl2},
})
requireSuccessNonNilResponse(t, resp, err)
requireFieldsSetInResp(t, resp, "crl")
pemCrl := resp.Data["crl"].(string)
combinedCrl, err := decodePemCrl(pemCrl)
require.NoError(t, err, "failed decoding combined CRL")
serials := extractSerialsFromCrl(t, combinedCrl)
require.Contains(t, serials, serial1)
require.Contains(t, serials, serial2)
require.Equal(t, 2, len(serials), "serials contained more serials than expected")
require.Equal(t, big.NewInt(int64(5)), combinedCrl.Number)
require.Equal(t, combinedCrl.ThisUpdate.Add(12*time.Hour), combinedCrl.NextUpdate)
extensions := combinedCrl.Extensions
requireExtensionOid(t, []int{2, 5, 29, 27}, extensions) // Delta CRL Extension
requireExtensionOid(t, []int{2, 5, 29, 20}, extensions) // CRL Number Extension
requireExtensionOid(t, []int{2, 5, 29, 35}, extensions) // akidOid
require.Equal(t, 3, len(extensions))
err = combinedCrl.CheckSignatureFrom(caCert)
require.NoError(t, err, "failed signature check of CRL")
}
func TestSignRevocationList(t *testing.T) {
t.Parallel()
coreConfig := &vault.CoreConfig{
LogicalBackends: map[string]logical.Factory{
"pki": Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
// Mount PKI, use this form of backend so our request is closer to reality (json parsed)
err := client.Sys().Mount("pki", &api.MountInput{
Type: "pki",
Config: api.MountConfigInput{
DefaultLeaseTTL: "16h",
MaxLeaseTTL: "60h",
},
})
require.NoError(t, err)
// Generate internal CA.
resp, err := client.Logical().Write("pki/root/generate/internal", map[string]interface{}{
"ttl": "40h",
"common_name": "myvault.com",
})
require.NoError(t, err)
caCert := parseCert(t, resp.Data["certificate"].(string))
resp, err = client.Logical().Write("pki/issuer/default/sign-revocation-list", map[string]interface{}{
"crl_number": "1",
"next_update": "12h",
"format": "pem",
"revoked_certs": []map[string]interface{}{
{
"serial_number": "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8",
"revocation_time": "1668614976",
"extensions": []map[string]interface{}{},
},
{
"serial_number": "27:03:89:76:5a:d4:d8:19:48:47:ca:96:db:6f:27:86:31:92:9f:82",
"revocation_time": "2022-11-16T16:09:36.739592Z",
},
{
"serial_number": "27:03:89:76:5a:d4:d8:19:48:47:ca:96:db:6f:27:86:31:92:9f:81",
"revocation_time": "2022-10-16T16:09:36.739592Z",
"extensions": []map[string]interface{}{
{
"id": "2.5.29.100",
"critical": "true",
"value": "aGVsbG8=", // "hello" base64 encoded
},
{
"id": "2.5.29.101",
"critical": "false",
"value": "Ynll", // "bye" base64 encoded
},
},
},
},
"extensions": []map[string]interface{}{
{
"id": "2.5.29.200",
"critical": "true",
"value": "aGVsbG8=", // "hello" base64 encoded
},
{
"id": "2.5.29.201",
"critical": "false",
"value": "Ynll", // "bye" base64 encoded
},
},
})
require.NoError(t, err)
pemCrl := resp.Data["crl"].(string)
crl, err := decodePemCrl(pemCrl)
require.NoError(t, err, "failed decoding CRL")
serials := extractSerialsFromCrl(t, crl)
require.Contains(t, serials, "37:60:16:e4:85:d5:96:38:3a:ed:31:06:8d:ed:7a:46:d4:22:63:d8")
require.Contains(t, serials, "27:03:89:76:5a:d4:d8:19:48:47:ca:96:db:6f:27:86:31:92:9f:82")
require.Contains(t, serials, "27:03:89:76:5a:d4:d8:19:48:47:ca:96:db:6f:27:86:31:92:9f:81")
require.Equal(t, 3, len(serials), "expected 3 serials within CRL")
// Make sure extensions on serials match what we expect.
require.Equal(t, 0, len(crl.RevokedCertificates[0].Extensions), "Expected no extensions on 1st serial")
require.Equal(t, 0, len(crl.RevokedCertificates[1].Extensions), "Expected no extensions on 2nd serial")
require.Equal(t, 2, len(crl.RevokedCertificates[2].Extensions), "Expected 2 extensions on 3 serial")
require.Equal(t, "2.5.29.100", crl.RevokedCertificates[2].Extensions[0].Id.String())
require.True(t, crl.RevokedCertificates[2].Extensions[0].Critical)
require.Equal(t, []byte("hello"), crl.RevokedCertificates[2].Extensions[0].Value)
require.Equal(t, "2.5.29.101", crl.RevokedCertificates[2].Extensions[1].Id.String())
require.False(t, crl.RevokedCertificates[2].Extensions[1].Critical)
require.Equal(t, []byte("bye"), crl.RevokedCertificates[2].Extensions[1].Value)
// CRL Number and times
require.Equal(t, big.NewInt(int64(1)), crl.Number)
require.Equal(t, crl.ThisUpdate.Add(12*time.Hour), crl.NextUpdate)
// Verify top level extensions are present
extensions := crl.Extensions
requireExtensionOid(t, []int{2, 5, 29, 20}, extensions) // CRL Number Extension
requireExtensionOid(t, []int{2, 5, 29, 35}, extensions) // akidOid
requireExtensionOid(t, []int{2, 5, 29, 200}, extensions) // Added value from param
requireExtensionOid(t, []int{2, 5, 29, 201}, extensions) // Added value from param
require.Equal(t, 4, len(extensions))
// Signature
err = crl.CheckSignatureFrom(caCert)
require.NoError(t, err, "failed signature check of CRL")
}
func TestSignRevocationList_NoRevokedCerts(t *testing.T) {
t.Parallel()
b, s := CreateBackendWithStorage(t)
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"common_name": "test.com",
})
requireSuccessNonNilResponse(t, resp, err)
resp, err = CBWrite(b, s, "issuer/default/sign-revocation-list", map[string]interface{}{
"crl_number": "10000",
"next_update": "12h",
"format": "pem",
})
schema.ValidateResponse(t, schema.GetResponseSchema(t, b.Route("issuer/default/sign-revocation-list"), logical.UpdateOperation), resp, true)
requireSuccessNonNilResponse(t, resp, err)
requireFieldsSetInResp(t, resp, "crl")
pemCrl := resp.Data["crl"].(string)
crl, err := decodePemCrl(pemCrl)
require.NoError(t, err, "failed decoding CRL")
serials := extractSerialsFromCrl(t, crl)
require.Equal(t, 0, len(serials), "no serials were expected in CRL")
require.Equal(t, big.NewInt(int64(10000)), crl.Number)
require.Equal(t, crl.ThisUpdate.Add(12*time.Hour), crl.NextUpdate)
}
func TestSignRevocationList_ReservedExtensions(t *testing.T) {
t.Parallel()
reservedOids := []asn1.ObjectIdentifier{
akOid, deltaCrlOid, crlNumOid,
}
// Validate there isn't copy/paste issues with our constants...
require.Equal(t, asn1.ObjectIdentifier{2, 5, 29, 27}, deltaCrlOid) // Delta CRL Extension
require.Equal(t, asn1.ObjectIdentifier{2, 5, 29, 20}, crlNumOid) // CRL Number Extension
require.Equal(t, asn1.ObjectIdentifier{2, 5, 29, 35}, akOid) // akidOid
for _, reservedOid := range reservedOids {
t.Run(reservedOid.String(), func(t *testing.T) {
b, s := CreateBackendWithStorage(t)
resp, err := CBWrite(b, s, "root/generate/internal", map[string]interface{}{
"common_name": "test.com",
})
requireSuccessNonNilResponse(t, resp, err)
resp, err = CBWrite(b, s, "issuer/default/sign-revocation-list", map[string]interface{}{
"crl_number": "1",
"next_update": "12h",
"format": "pem",
"extensions": []map[string]interface{}{
{
"id": reservedOid.String(),
"critical": "false",
"value": base64.StdEncoding.EncodeToString([]byte("hello")),
},
},
})
require.ErrorContains(t, err, "is reserved")
})
}
}
func setupResignCrlMounts(t *testing.T, b1 *backend, s1 logical.Storage, b2 *backend, s2 logical.Storage) (*x509.Certificate, string, string, string, string) {
t.Helper()
// Setup two mounts with the same CA/key material
resp, err := CBWrite(b1, s1, "root/generate/exported", map[string]interface{}{
"common_name": "test.com",
})
requireSuccessNonNilResponse(t, resp, err)
requireFieldsSetInResp(t, resp, "certificate", "private_key")
pemCaCert := resp.Data["certificate"].(string)
caCert := parseCert(t, pemCaCert)
privKey := resp.Data["private_key"].(string)
// Import the above key/cert into another mount
resp, err = CBWrite(b2, s2, "config/ca", map[string]interface{}{
"pem_bundle": pemCaCert + "\n" + privKey,
})
requireSuccessNonNilResponse(t, resp, err, "error setting up CA on backend 2")
// Create the same role in both mounts
resp, err = CBWrite(b1, s1, "roles/test", map[string]interface{}{
"allowed_domains": "test.com",
"allow_subdomains": "true",
"max_ttl": "1h",
})
requireSuccessNonNilResponse(t, resp, err, "error setting up pki role on backend 1")
resp, err = CBWrite(b2, s2, "roles/test", map[string]interface{}{
"allowed_domains": "test.com",
"allow_subdomains": "true",
"max_ttl": "1h",
})
requireSuccessNonNilResponse(t, resp, err, "error setting up pki role on backend 2")
// Issue and revoke a cert in backend 1
resp, err = CBWrite(b1, s1, "issue/test", map[string]interface{}{
"common_name": "test1.test.com",
})
requireSuccessNonNilResponse(t, resp, err, "error issuing cert from backend 1")
requireFieldsSetInResp(t, resp, "serial_number")
serial1 := resp.Data["serial_number"].(string)
resp, err = CBWrite(b1, s1, "revoke", map[string]interface{}{"serial_number": serial1})
requireSuccessNonNilResponse(t, resp, err, "error revoking cert from backend 2")
// Issue and revoke a cert in backend 2
resp, err = CBWrite(b2, s2, "issue/test", map[string]interface{}{
"common_name": "test1.test.com",
})
requireSuccessNonNilResponse(t, resp, err, "error issuing cert from backend 2")
requireFieldsSetInResp(t, resp, "serial_number")
serial2 := resp.Data["serial_number"].(string)
resp, err = CBWrite(b2, s2, "revoke", map[string]interface{}{"serial_number": serial2})
requireSuccessNonNilResponse(t, resp, err, "error revoking cert from backend 2")
// Fetch PEM CRLs from each
resp, err = CBRead(b1, s1, "cert/crl")
requireSuccessNonNilResponse(t, resp, err, "error fetch crl from backend 1")
requireFieldsSetInResp(t, resp, "certificate")
crl1 := resp.Data["certificate"].(string)
resp, err = CBRead(b2, s2, "cert/crl")
requireSuccessNonNilResponse(t, resp, err, "error fetch crl from backend 2")
requireFieldsSetInResp(t, resp, "certificate")
crl2 := resp.Data["certificate"].(string)
return caCert, serial1, serial2, crl1, crl2
}
func requireExtensionOid(t *testing.T, identifier asn1.ObjectIdentifier, extensions []pkix.Extension, msgAndArgs ...interface{}) {
t.Helper()
found := false
var oidsInExtensions []string
for _, extension := range extensions {
oidsInExtensions = append(oidsInExtensions, extension.Id.String())
if extension.Id.Equal(identifier) {
found = true
break
}
}
if !found {
msg := fmt.Sprintf("Failed to find matching asn oid %s out of %v", identifier.String(), oidsInExtensions)
require.Fail(t, msg, msgAndArgs)
}
}
func extractSerialsFromCrl(t *testing.T, crl *x509.RevocationList) map[string]time.Time {
t.Helper()
serials := map[string]time.Time{}
for _, revokedCert := range crl.RevokedCertificates {
serial := serialFromBigInt(revokedCert.SerialNumber)
if _, exists := serials[serial]; exists {
t.Fatalf("Serial number %s was duplicated in CRL", serial)
}
serials[serial] = revokedCert.RevocationTime
}
return serials
}