blob: e76df359e9ed1b107c18fc23f5fd8c330b9481b8 [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
package pki
import (
"bytes"
"context"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"fmt"
"strconv"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/sdk/logical"
)
// For speed, all keys are ECDSA.
type CBGenerateKey struct {
Name string
}
func (c CBGenerateKey) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) {
resp, err := CBWrite(b, s, "keys/generate/exported", map[string]interface{}{
"name": c.Name,
"algo": "ec",
"bits": 256,
})
if err != nil {
t.Fatalf("failed to provision key (%v): %v", c.Name, err)
}
knownKeys[c.Name] = resp.Data["private"].(string)
}
// Generate a root.
type CBGenerateRoot struct {
Key string
Existing bool
Name string
CommonName string
ErrorMessage string
}
func (c CBGenerateRoot) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) {
url := "issuers/generate/root/"
data := make(map[string]interface{})
if c.Existing {
url += "existing"
data["key_ref"] = c.Key
} else {
url += "exported"
data["key_type"] = "ec"
data["key_bits"] = 256
data["key_name"] = c.Key
}
data["issuer_name"] = c.Name
data["common_name"] = c.Name
if len(c.CommonName) > 0 {
data["common_name"] = c.CommonName
}
resp, err := CBWrite(b, s, url, data)
if err != nil {
if len(c.ErrorMessage) > 0 {
if !strings.Contains(err.Error(), c.ErrorMessage) {
t.Fatalf("failed to generate root cert for issuer (%v): expected (%v) in error message but got %v", c.Name, c.ErrorMessage, err)
}
return
}
t.Fatalf("failed to provision issuer (%v): %v / body: %v", c.Name, err, data)
} else if len(c.ErrorMessage) > 0 {
t.Fatalf("expected to fail generation of issuer (%v) with error message containing (%v)", c.Name, c.ErrorMessage)
}
if !c.Existing {
knownKeys[c.Key] = resp.Data["private_key"].(string)
}
knownCerts[c.Name] = resp.Data["certificate"].(string)
// Validate key_id matches.
url = "key/" + c.Key
resp, err = CBRead(b, s, url)
if err != nil {
t.Fatalf("failed to fetch key for name %v: %v", c.Key, err)
}
if resp == nil {
t.Fatalf("failed to fetch key for name %v: nil response", c.Key)
}
expectedKeyId := resp.Data["key_id"]
url = "issuer/" + c.Name
resp, err = CBRead(b, s, url)
if err != nil {
t.Fatalf("failed to fetch issuer for name %v: %v", c.Name, err)
}
if resp == nil {
t.Fatalf("failed to fetch issuer for name %v: nil response", c.Name)
}
actualKeyId := resp.Data["key_id"]
if expectedKeyId != actualKeyId {
t.Fatalf("expected issuer %v to have key matching %v but got mismatch: %v vs %v", c.Name, c.Key, actualKeyId, expectedKeyId)
}
}
// Generate an intermediate. Might not really be an intermediate; might be
// a cross-signed cert.
type CBGenerateIntermediate struct {
Key string
Existing bool
Name string
CommonName string
SKID string
Parent string
ImportErrorMessage string
}
func (c CBGenerateIntermediate) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) {
// Build CSR
url := "issuers/generate/intermediate/"
data := make(map[string]interface{})
if c.Existing {
url += "existing"
data["key_ref"] = c.Key
} else {
url += "exported"
data["key_type"] = "ec"
data["key_bits"] = 256
data["key_name"] = c.Key
}
resp, err := CBWrite(b, s, url, data)
if err != nil {
t.Fatalf("failed to generate CSR for issuer (%v): %v / body: %v", c.Name, err, data)
}
if !c.Existing {
knownKeys[c.Key] = resp.Data["private_key"].(string)
}
csr := resp.Data["csr"].(string)
// Sign CSR
url = fmt.Sprintf("issuer/%s/sign-intermediate", c.Parent)
data = make(map[string]interface{})
data["csr"] = csr
data["common_name"] = c.Name
if len(c.CommonName) > 0 {
data["common_name"] = c.CommonName
}
if len(c.SKID) > 0 {
// Copy the SKID from an existing, already-issued cert.
otherPEM := knownCerts[c.SKID]
otherCert := ToCertificate(t, otherPEM)
data["skid"] = hex.EncodeToString(otherCert.SubjectKeyId)
}
resp, err = CBWrite(b, s, url, data)
if err != nil {
t.Fatalf("failed to sign CSR for issuer (%v): %v / body: %v", c.Name, err, data)
}
knownCerts[c.Name] = strings.TrimSpace(resp.Data["certificate"].(string))
// Verify SKID if one was requested.
if len(c.SKID) > 0 {
otherPEM := knownCerts[c.SKID]
otherCert := ToCertificate(t, otherPEM)
ourCert := ToCertificate(t, knownCerts[c.Name])
if !bytes.Equal(otherCert.SubjectKeyId, ourCert.SubjectKeyId) {
t.Fatalf("Expected two certs to have equal SKIDs but differed: them: %v vs us: %v", otherCert.SubjectKeyId, ourCert.SubjectKeyId)
}
}
// Set the signed intermediate
url = "intermediate/set-signed"
data = make(map[string]interface{})
data["certificate"] = knownCerts[c.Name]
data["issuer_name"] = c.Name
resp, err = CBWrite(b, s, url, data)
if err != nil {
if len(c.ImportErrorMessage) > 0 {
if !strings.Contains(err.Error(), c.ImportErrorMessage) {
t.Fatalf("failed to import signed cert for issuer (%v): expected (%v) in error message but got %v", c.Name, c.ImportErrorMessage, err)
}
return
}
t.Fatalf("failed to import signed cert for issuer (%v): %v / body: %v", c.Name, err, data)
} else if len(c.ImportErrorMessage) > 0 {
t.Fatalf("expected to fail import (with error %v) of cert for issuer (%v) but was success: response: %v", c.ImportErrorMessage, c.Name, resp)
}
// Update the name since set-signed doesn't actually take an issuer name
// parameter.
rawNewCerts := resp.Data["imported_issuers"].([]string)
if len(rawNewCerts) != 1 {
t.Fatalf("Expected a single new certificate during import of signed cert for %v: got %v\nresp: %v", c.Name, len(rawNewCerts), resp)
}
newCertId := rawNewCerts[0]
_, err = CBWrite(b, s, "issuer/"+newCertId, map[string]interface{}{
"issuer_name": c.Name,
})
if err != nil {
t.Fatalf("failed to update name for issuer (%v/%v): %v", c.Name, newCertId, err)
}
// Validate key_id matches.
url = "key/" + c.Key
resp, err = CBRead(b, s, url)
if err != nil {
t.Fatalf("failed to fetch key for name %v: %v", c.Key, err)
}
if resp == nil {
t.Fatalf("failed to fetch key for name %v: nil response", c.Key)
}
expectedKeyId := resp.Data["key_id"]
url = "issuer/" + c.Name
resp, err = CBRead(b, s, url)
if err != nil {
t.Fatalf("failed to fetch issuer for name %v: %v", c.Name, err)
}
if resp == nil {
t.Fatalf("failed to fetch issuer for name %v: nil response", c.Name)
}
actualKeyId := resp.Data["key_id"]
if expectedKeyId != actualKeyId {
t.Fatalf("expected issuer %v to have key matching %v but got mismatch: %v vs %v", c.Name, c.Key, actualKeyId, expectedKeyId)
}
}
// Delete an issuer; breaks chains.
type CBDeleteIssuer struct {
Issuer string
}
func (c CBDeleteIssuer) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) {
url := fmt.Sprintf("issuer/%v", c.Issuer)
_, err := CBDelete(b, s, url)
if err != nil {
t.Fatalf("failed to delete issuer (%v): %v", c.Issuer, err)
}
delete(knownCerts, c.Issuer)
}
// Validate the specified chain exists, by name.
type CBValidateChain struct {
Chains map[string][]string
Aliases map[string]string
}
func (c CBValidateChain) ChainToPEMs(t testing.TB, parent string, chain []string, knownCerts map[string]string) []string {
var result []string
for entryIndex, entry := range chain {
var chainEntry string
modifiedEntry := entry
if entryIndex == 0 && entry == "self" {
modifiedEntry = parent
}
for pattern, replacement := range c.Aliases {
modifiedEntry = strings.ReplaceAll(modifiedEntry, pattern, replacement)
}
for _, issuer := range strings.Split(modifiedEntry, ",") {
cert, ok := knownCerts[issuer]
if !ok {
t.Fatalf("Unknown issuer %v in chain for %v: %v", issuer, parent, chain)
}
chainEntry += cert
}
result = append(result, chainEntry)
}
return result
}
func (c CBValidateChain) FindNameForCert(t testing.TB, cert string, knownCerts map[string]string) string {
for issuer, known := range knownCerts {
if strings.TrimSpace(known) == strings.TrimSpace(cert) {
return issuer
}
}
t.Fatalf("Unable to find cert:\n[%v]\nin known map:\n%v\n", cert, knownCerts)
return ""
}
func (c CBValidateChain) PrettyChain(t testing.TB, chain []string, knownCerts map[string]string) []string {
var prettyChain []string
for _, cert := range chain {
prettyChain = append(prettyChain, c.FindNameForCert(t, cert, knownCerts))
}
return prettyChain
}
func ToCertificate(t testing.TB, cert string) *x509.Certificate {
t.Helper()
block, _ := pem.Decode([]byte(cert))
if block == nil {
t.Fatalf("Unable to parse certificate: nil PEM block\n[%v]\n", cert)
}
ret, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("Unable to parse certificate: %v\n[%v]\n", err, cert)
}
return ret
}
func ToCRL(t testing.TB, crl string, issuer *x509.Certificate) *pkix.CertificateList {
t.Helper()
block, _ := pem.Decode([]byte(crl))
if block == nil {
t.Fatalf("Unable to parse CRL: nil PEM block\n[%v]\n", crl)
}
ret, err := x509.ParseCRL(block.Bytes)
if err != nil {
t.Fatalf("Unable to parse CRL: %v\n[%v]\n", err, crl)
}
if issuer != nil {
if err := issuer.CheckCRLSignature(ret); err != nil {
t.Fatalf("Unable to check CRL signature: %v\n[%v]\n[%v]\n", err, crl, issuer)
}
}
return ret
}
func (c CBValidateChain) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) {
for issuer, chain := range c.Chains {
resp, err := CBRead(b, s, "issuer/"+issuer)
if err != nil {
t.Fatalf("failed to get chain for issuer (%v): %v", issuer, err)
}
rawCurrentChain := resp.Data["ca_chain"].([]string)
var currentChain []string
for _, entry := range rawCurrentChain {
currentChain = append(currentChain, strings.TrimSpace(entry))
}
// Ensure the issuer cert is always first.
if currentChain[0] != knownCerts[issuer] {
pretty := c.FindNameForCert(t, currentChain[0], knownCerts)
t.Fatalf("expected certificate at index 0 to be self:\n[%v]\n[pretty: %v]\nis not the issuer's cert:\n[%v]\n[pretty: %v]", currentChain[0], pretty, knownCerts[issuer], issuer)
}
// Validate it against the expected chain.
expectedChain := c.ChainToPEMs(t, issuer, chain, knownCerts)
if len(currentChain) != len(expectedChain) {
prettyCurrentChain := c.PrettyChain(t, currentChain, knownCerts)
t.Fatalf("Lengths of chains for issuer %v mismatched: got %v vs expected %v:\n[%v]\n[pretty: %v]\n[%v]\n[pretty: %v]", issuer, len(currentChain), len(expectedChain), currentChain, prettyCurrentChain, expectedChain, chain)
}
for currentIndex, currentCert := range currentChain {
// Chains might be forked so we may not be able to strictly validate
// the chain against a single value. Instead, use strings.Contains
// to validate the current cert is in the list of allowed
// possibilities.
if !strings.Contains(expectedChain[currentIndex], currentCert) {
pretty := c.FindNameForCert(t, currentCert, knownCerts)
t.Fatalf("chain mismatch at index %v for issuer %v: got cert:\n[%v]\n[pretty: %v]\nbut expected one of\n[%v]\n[pretty: %v]\n", currentIndex, issuer, currentCert, pretty, expectedChain[currentIndex], chain[currentIndex])
}
}
// Due to alternate paths, the above doesn't ensure ensure each cert
// in the chain is only used once. Validate that now.
for thisIndex, thisCert := range currentChain {
for otherIndex, otherCert := range currentChain[thisIndex+1:] {
if thisCert == otherCert {
thisPretty := c.FindNameForCert(t, thisCert, knownCerts)
otherPretty := c.FindNameForCert(t, otherCert, knownCerts)
otherIndex += thisIndex + 1
t.Fatalf("cert reused in chain for %v:\n[%v]\n[pretty: %v / index: %v]\n[%v]\n[pretty: %v / index: %v]\n", issuer, thisCert, thisPretty, thisIndex, otherCert, otherPretty, otherIndex)
}
}
}
// Finally, validate that all certs verify something that came before
// it. In the linear chain sense, this should strictly mean that the
// parent comes before the child.
for thisIndex, thisCertPem := range currentChain[1:] {
thisIndex += 1 // Absolute index.
parentCert := ToCertificate(t, thisCertPem)
// Iterate backwards; prefer the most recent cert to the older
// certs.
foundCert := false
for otherIndex := thisIndex - 1; otherIndex >= 0; otherIndex-- {
otherCertPem := currentChain[otherIndex]
childCert := ToCertificate(t, otherCertPem)
if err := childCert.CheckSignatureFrom(parentCert); err == nil {
foundCert = true
}
}
if !foundCert {
pretty := c.FindNameForCert(t, thisCertPem, knownCerts)
t.Fatalf("malformed test scenario: certificate at chain index %v when validating %v does not validate any previous certificates:\n[%v]\n[pretty: %v]\n", thisIndex, issuer, thisCertPem, pretty)
}
}
}
}
// Update an issuer
type CBUpdateIssuer struct {
Name string
CAChain []string
Usage string
}
func (c CBUpdateIssuer) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) {
url := "issuer/" + c.Name
data := make(map[string]interface{})
data["issuer_name"] = c.Name
resp, err := CBRead(b, s, url)
if err != nil {
t.Fatalf("failed to read issuer (%v): %v", c.Name, err)
}
if len(c.CAChain) == 1 && c.CAChain[0] == "existing" {
data["manual_chain"] = resp.Data["manual_chain"]
} else {
data["manual_chain"] = c.CAChain
}
if c.Usage == "existing" {
data["usage"] = resp.Data["usage"]
} else if len(c.Usage) == 0 {
data["usage"] = "read-only,issuing-certificates,crl-signing"
} else {
data["usage"] = c.Usage
}
_, err = CBWrite(b, s, url, data)
if err != nil {
t.Fatalf("failed to update issuer (%v): %v / body: %v", c.Name, err, data)
}
}
// Issue a leaf, revoke it, and then validate it appears on the CRL.
type CBIssueLeaf struct {
Issuer string
Role string
}
func (c CBIssueLeaf) IssueLeaf(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string, errorMessage string) *logical.Response {
// Write a role
url := "roles/" + c.Role
data := make(map[string]interface{})
data["allow_localhost"] = true
data["ttl"] = "200s"
data["key_type"] = "ec"
_, err := CBWrite(b, s, url, data)
if err != nil {
t.Fatalf("failed to update role (%v): %v / body: %v", c.Role, err, data)
}
// Issue the certificate.
url = "issuer/" + c.Issuer + "/issue/" + c.Role
data = make(map[string]interface{})
data["common_name"] = "localhost"
resp, err := CBWrite(b, s, url, data)
if err != nil {
if len(errorMessage) >= 0 {
if !strings.Contains(err.Error(), errorMessage) {
t.Fatalf("failed to issue cert (%v via %v): %v / body: %v\nExpected error message: %v", c.Issuer, c.Role, err, data, errorMessage)
}
return nil
}
t.Fatalf("failed to issue cert (%v via %v): %v / body: %v", c.Issuer, c.Role, err, data)
}
if resp == nil {
t.Fatalf("failed to issue cert (%v via %v): nil response / body: %v", c.Issuer, c.Role, data)
}
raw_cert := resp.Data["certificate"].(string)
cert := ToCertificate(t, raw_cert)
raw_issuer := resp.Data["issuing_ca"].(string)
issuer := ToCertificate(t, raw_issuer)
// Validate issuer and signatures are good.
if strings.TrimSpace(raw_issuer) != strings.TrimSpace(knownCerts[c.Issuer]) {
t.Fatalf("signing certificate ended with wrong certificate for issuer %v:\n[%v]\n\nvs\n\n[%v]\n", c.Issuer, raw_issuer, knownCerts[c.Issuer])
}
if err := cert.CheckSignatureFrom(issuer); err != nil {
t.Fatalf("failed to verify signature on issued certificate from %v: %v\n[%v]\n[%v]\n", c.Issuer, err, raw_cert, raw_issuer)
}
return resp
}
func (c CBIssueLeaf) RevokeLeaf(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string, issueResponse *logical.Response, hasCRL bool, isDefault bool) {
api_serial := issueResponse.Data["serial_number"].(string)
raw_cert := issueResponse.Data["certificate"].(string)
cert := ToCertificate(t, raw_cert)
raw_issuer := issueResponse.Data["issuing_ca"].(string)
issuer := ToCertificate(t, raw_issuer)
// Revoke the certificate.
url := "revoke"
data := make(map[string]interface{})
data["serial_number"] = api_serial
resp, err := CBWrite(b, s, url, data)
if err != nil {
t.Fatalf("failed to revoke issued certificate (%v) under role %v / issuer %v: %v", api_serial, c.Role, c.Issuer, err)
}
if resp == nil {
t.Fatalf("failed to revoke issued certificate (%v) under role %v / issuer %v: nil response", api_serial, c.Role, c.Issuer)
}
if _, ok := resp.Data["revocation_time"]; !ok {
t.Fatalf("failed to revoke issued certificate (%v) under role %v / issuer %v: expected response parameter revocation_time was missing from response:\n%v", api_serial, c.Role, c.Issuer, resp.Data)
}
if !hasCRL {
// Nothing further we can test here. We could re-enable CRL building
// and check that it works, but that seems like a stretch. Other
// issuers might be functionally the same as this issuer (and thus
// this CRL will still be issued), but that requires more work to
// check and verify.
return
}
// Verify it is on this issuer's CRL.
url = "issuer/" + c.Issuer + "/crl"
resp, err = CBRead(b, s, url)
if err != nil {
t.Fatalf("failed to fetch CRL for issuer %v: %v", c.Issuer, err)
}
if resp == nil {
t.Fatalf("failed to fetch CRL for issuer %v: nil response", c.Issuer)
}
raw_crl := resp.Data["crl"].(string)
crl := ToCRL(t, raw_crl, issuer)
foundCert := requireSerialNumberInCRL(nil, crl.TBSCertList, api_serial)
if !foundCert {
if !hasCRL && !isDefault {
// Update the issuer we expect to find this on.
resp, err := CBRead(b, s, "config/issuers")
if err != nil {
t.Fatalf("failed to read default issuer config: %v", err)
}
if resp == nil {
t.Fatalf("failed to read default issuer config: nil response")
}
defaultID := resp.Data["default"].(issuerID).String()
c.Issuer = defaultID
issuer = nil
}
// Verify it is on the default issuer's CRL.
url = "issuer/" + c.Issuer + "/crl"
resp, err = CBRead(b, s, url)
if err != nil {
t.Fatalf("failed to fetch CRL for issuer %v: %v", c.Issuer, err)
}
if resp == nil {
t.Fatalf("failed to fetch CRL for issuer %v: nil response", c.Issuer)
}
raw_crl = resp.Data["crl"].(string)
crl = ToCRL(t, raw_crl, issuer)
foundCert = requireSerialNumberInCRL(nil, crl.TBSCertList, api_serial)
}
if !foundCert {
// If CRL building is broken, this is useful for finding which issuer's
// CRL the revoked cert actually appears on.
for issuerName := range knownCerts {
url = "issuer/" + issuerName + "/crl"
resp, err = CBRead(b, s, url)
if err != nil {
t.Fatalf("failed to fetch CRL for issuer %v: %v", issuerName, err)
}
if resp == nil {
t.Fatalf("failed to fetch CRL for issuer %v: nil response", issuerName)
}
raw_crl := resp.Data["crl"].(string)
crl := ToCRL(t, raw_crl, nil)
for index, revoked := range crl.TBSCertList.RevokedCertificates {
// t.Logf("[%v] revoked serial number: %v -- vs -- %v", index, revoked.SerialNumber, cert.SerialNumber)
if revoked.SerialNumber.Cmp(cert.SerialNumber) == 0 {
t.Logf("found revoked cert at index: %v for unexpected issuer: %v", index, issuerName)
break
}
}
}
t.Fatalf("expected to find certificate with serial [%v] on issuer %v's CRL but was missing: %v revoked certs\n\nCRL:\n[%v]\n\nLeaf:\n[%v]\n\nIssuer (hasCRL: %v):\n[%v]\n", api_serial, c.Issuer, len(crl.TBSCertList.RevokedCertificates), raw_crl, raw_cert, hasCRL, raw_issuer)
}
}
func (c CBIssueLeaf) Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) {
if len(c.Role) == 0 {
c.Role = "testing"
}
resp, err := CBRead(b, s, "config/issuers")
if err != nil {
t.Fatalf("failed to read default issuer config: %v", err)
}
if resp == nil {
t.Fatalf("failed to read default issuer config: nil response")
}
defaultID := resp.Data["default"].(issuerID).String()
resp, err = CBRead(b, s, "issuer/"+c.Issuer)
if err != nil {
t.Fatalf("failed to read issuer %v: %v", c.Issuer, err)
}
if resp == nil {
t.Fatalf("failed to read issuer %v: nil response", c.Issuer)
}
ourID := resp.Data["issuer_id"].(issuerID).String()
areDefault := ourID == defaultID
for _, usage := range []string{"read-only", "crl-signing", "issuing-certificates", "issuing-certificates,crl-signing"} {
ui := CBUpdateIssuer{
Name: c.Issuer,
CAChain: []string{"existing"},
Usage: usage,
}
ui.Run(t, b, s, knownKeys, knownCerts)
ilError := "requested usage issuing-certificates for issuer"
hasIssuing := strings.Contains(usage, "issuing-certificates")
if hasIssuing {
ilError = ""
}
hasCRL := strings.Contains(usage, "crl-signing")
resp := c.IssueLeaf(t, b, s, knownKeys, knownCerts, ilError)
if resp == nil && !hasIssuing {
continue
}
c.RevokeLeaf(t, b, s, knownKeys, knownCerts, resp, hasCRL, areDefault)
}
}
// Stable ordering
func ensureStableOrderingOfChains(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string) {
// Start by fetching all chains
certChains := make(map[string][]string)
for issuer := range knownCerts {
resp, err := CBRead(b, s, "issuer/"+issuer)
if err != nil {
t.Fatalf("failed to get chain for issuer (%v): %v", issuer, err)
}
rawCurrentChain := resp.Data["ca_chain"].([]string)
var currentChain []string
for _, entry := range rawCurrentChain {
currentChain = append(currentChain, strings.TrimSpace(entry))
}
certChains[issuer] = currentChain
}
// Now, generate a bunch of arbitrary roots and validate the chain is
// consistent.
var runs []time.Duration
for i := 0; i < 10; i++ {
name := "stable-order-root-" + strconv.Itoa(i)
step := CBGenerateRoot{
Key: name,
Name: name,
}
step.Run(t, b, s, make(map[string]string), make(map[string]string))
before := time.Now()
_, err := CBDelete(b, s, "issuer/"+name)
if err != nil {
t.Fatalf("failed to delete temporary testing issuer %v: %v", name, err)
}
after := time.Now()
elapsed := after.Sub(before)
runs = append(runs, elapsed)
for issuer := range knownCerts {
resp, err := CBRead(b, s, "issuer/"+issuer)
if err != nil {
t.Fatalf("failed to get chain for issuer (%v): %v", issuer, err)
}
rawCurrentChain := resp.Data["ca_chain"].([]string)
for index, entry := range rawCurrentChain {
if strings.TrimSpace(entry) != certChains[issuer][index] {
t.Fatalf("iteration %d - chain for issuer %v differed at index %d\n%v\nvs\n%v", i, issuer, index, entry, certChains[issuer][index])
}
}
}
}
min := runs[0]
max := runs[0]
var avg time.Duration
for _, run := range runs {
if run < min {
min = run
}
if run > max {
max = run
}
avg += run
}
avg = avg / time.Duration(len(runs))
t.Logf("Chain building run time (deletion) - min: %v / avg: %v / max: %v - entries: %v", min, avg, max, runs)
}
type CBTestStep interface {
Run(t testing.TB, b *backend, s logical.Storage, knownKeys map[string]string, knownCerts map[string]string)
}
type CBTestScenario struct {
Steps []CBTestStep
}
var chainBuildingTestCases = []CBTestScenario{
{
// This test builds up two cliques lined by a cycle, dropping into
// a single intermediate.
Steps: []CBTestStep{
// Create a reissued certificate using the same key. These
// should validate themselves.
CBGenerateRoot{
Key: "key-root-old",
Name: "root-old-a",
CommonName: "root-old",
},
CBValidateChain{
Chains: map[string][]string{
"root-old-a": {"self"},
},
},
// After adding the second root using the same key and common
// name, there should now be two certs in each chain.
CBGenerateRoot{
Key: "key-root-old",
Existing: true,
Name: "root-old-b",
CommonName: "root-old",
},
CBValidateChain{
Chains: map[string][]string{
"root-old-a": {"self", "root-old-b"},
"root-old-b": {"self", "root-old-a"},
},
},
// After adding a third root, there are now two possibilities for
// each later chain entry.
CBGenerateRoot{
Key: "key-root-old",
Existing: true,
Name: "root-old-c",
CommonName: "root-old",
},
CBValidateChain{
Chains: map[string][]string{
"root-old-a": {"self", "root-old-bc", "root-old-bc"},
"root-old-b": {"self", "root-old-ac", "root-old-ac"},
"root-old-c": {"self", "root-old-ab", "root-old-ab"},
},
Aliases: map[string]string{
"root-old-ac": "root-old-a,root-old-c",
"root-old-ab": "root-old-a,root-old-b",
"root-old-bc": "root-old-b,root-old-c",
},
},
// If we generate an unrelated issuer, it shouldn't affect either
// chain.
CBGenerateRoot{
Key: "key-root-new",
Name: "root-new-a",
CommonName: "root-new",
},
CBValidateChain{
Chains: map[string][]string{
"root-old-a": {"self", "root-old-bc", "root-old-bc"},
"root-old-b": {"self", "root-old-ac", "root-old-ac"},
"root-old-c": {"self", "root-old-ab", "root-old-ab"},
"root-new-a": {"self"},
},
Aliases: map[string]string{
"root-old-ac": "root-old-a,root-old-c",
"root-old-ab": "root-old-a,root-old-b",
"root-old-bc": "root-old-b,root-old-c",
},
},
// Reissuing this new root should form another clique.
CBGenerateRoot{
Key: "key-root-new",
Existing: true,
Name: "root-new-b",
CommonName: "root-new",
},
CBValidateChain{
Chains: map[string][]string{
"root-old-a": {"self", "root-old-bc", "root-old-bc"},
"root-old-b": {"self", "root-old-ac", "root-old-ac"},
"root-old-c": {"self", "root-old-ab", "root-old-ab"},
"root-new-a": {"self", "root-new-b"},
"root-new-b": {"self", "root-new-a"},
},
Aliases: map[string]string{
"root-old-ac": "root-old-a,root-old-c",
"root-old-ab": "root-old-a,root-old-b",
"root-old-bc": "root-old-b,root-old-c",
},
},
// Generating a cross-signed cert from old->new should result
// in all old clique certs showing up in the new root's paths.
// This does not form a cycle.
CBGenerateIntermediate{
// In order to validate the existing root-new clique, we
// have to reuse the key and common name here for
// cross-signing.
Key: "key-root-new",
Existing: true,
Name: "cross-old-new",
CommonName: "root-new",
SKID: "root-new-a",
// Which old issuer is used here doesn't matter as they have
// the same CN and key.
Parent: "root-old-a",
},
CBValidateChain{
Chains: map[string][]string{
"root-old-a": {"self", "root-old-bc", "root-old-bc"},
"root-old-b": {"self", "root-old-ac", "root-old-ac"},
"root-old-c": {"self", "root-old-ab", "root-old-ab"},
"cross-old-new": {"self", "root-old-abc", "root-old-abc", "root-old-abc"},
"root-new-a": {"self", "root-new-b", "cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"},
"root-new-b": {"self", "root-new-a", "cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"},
},
Aliases: map[string]string{
"root-old-ac": "root-old-a,root-old-c",
"root-old-ab": "root-old-a,root-old-b",
"root-old-bc": "root-old-b,root-old-c",
"root-old-abc": "root-old-a,root-old-b,root-old-c",
},
},
// If we create a new intermediate off of the root-new, we should
// simply add to the existing chain.
CBGenerateIntermediate{
Key: "key-inter-a-root-new",
Name: "inter-a-root-new",
Parent: "root-new-a",
},
CBValidateChain{
Chains: map[string][]string{
"root-old-a": {"self", "root-old-bc", "root-old-bc"},
"root-old-b": {"self", "root-old-ac", "root-old-ac"},
"root-old-c": {"self", "root-old-ab", "root-old-ab"},
"cross-old-new": {"self", "root-old-abc", "root-old-abc", "root-old-abc"},
"root-new-a": {"self", "root-new-b", "cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"},
"root-new-b": {"self", "root-new-a", "cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"},
// If we find cross-old-new first, the old clique will be ahead
// of the new clique; otherwise the new clique will appear first.
"inter-a-root-new": {"self", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle"},
},
Aliases: map[string]string{
"root-old-ac": "root-old-a,root-old-c",
"root-old-ab": "root-old-a,root-old-b",
"root-old-bc": "root-old-b,root-old-c",
"root-old-abc": "root-old-a,root-old-b,root-old-c",
"full-cycle": "root-old-a,root-old-b,root-old-c,cross-old-new,root-new-a,root-new-b",
},
},
// Now, if we cross-sign back from new to old, we should
// form cycle with multiple reissued cliques. This means
// all nodes will have the same chain.
CBGenerateIntermediate{
// In order to validate the existing root-old clique, we
// have to reuse the key and common name here for
// cross-signing.
Key: "key-root-old",
Existing: true,
Name: "cross-new-old",
CommonName: "root-old",
SKID: "root-old-a",
// Which new issuer is used here doesn't matter as they have
// the same CN and key.
Parent: "root-new-a",
},
CBValidateChain{
Chains: map[string][]string{
"root-old-a": {"self", "root-old-bc", "root-old-bc", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"},
"root-old-b": {"self", "root-old-ac", "root-old-ac", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"},
"root-old-c": {"self", "root-old-ab", "root-old-ab", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"},
"cross-old-new": {"self", "cross-new-old", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"},
"cross-new-old": {"self", "cross-old-new", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"},
"root-new-a": {"self", "root-new-b", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"},
"root-new-b": {"self", "root-new-a", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"},
"inter-a-root-new": {"self", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle"},
},
Aliases: map[string]string{
"root-old-ac": "root-old-a,root-old-c",
"root-old-ab": "root-old-a,root-old-b",
"root-old-bc": "root-old-b,root-old-c",
"root-old-abc": "root-old-a,root-old-b,root-old-c",
"root-new-ab": "root-new-a,root-new-b",
"both-cross-old-new": "cross-old-new,cross-new-old",
"both-cliques": "root-old-a,root-old-b,root-old-c,root-new-a,root-new-b",
"full-cycle": "root-old-a,root-old-b,root-old-c,cross-old-new,cross-new-old,root-new-a,root-new-b",
},
},
// Update each old root to only include itself.
CBUpdateIssuer{
Name: "root-old-a",
CAChain: []string{"root-old-a"},
},
CBUpdateIssuer{
Name: "root-old-b",
CAChain: []string{"root-old-b"},
},
CBUpdateIssuer{
Name: "root-old-c",
CAChain: []string{"root-old-c"},
},
// Step 19
CBValidateChain{
Chains: map[string][]string{
"root-old-a": {"self"},
"root-old-b": {"self"},
"root-old-c": {"self"},
"cross-old-new": {"self", "cross-new-old", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"},
"cross-new-old": {"self", "cross-old-new", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"},
"root-new-a": {"self", "root-new-b", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"},
"root-new-b": {"self", "root-new-a", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"},
"inter-a-root-new": {"self", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle"},
},
Aliases: map[string]string{
"root-old-ac": "root-old-a,root-old-c",
"root-old-ab": "root-old-a,root-old-b",
"root-old-bc": "root-old-b,root-old-c",
"root-old-abc": "root-old-a,root-old-b,root-old-c",
"root-new-ab": "root-new-a,root-new-b",
"both-cross-old-new": "cross-old-new,cross-new-old",
"both-cliques": "root-old-a,root-old-b,root-old-c,root-new-a,root-new-b",
"full-cycle": "root-old-a,root-old-b,root-old-c,cross-old-new,cross-new-old,root-new-a,root-new-b",
},
},
// Reset the old roots; should get the original chains back.
CBUpdateIssuer{
Name: "root-old-a",
},
CBUpdateIssuer{
Name: "root-old-b",
},
CBUpdateIssuer{
Name: "root-old-c",
},
CBValidateChain{
Chains: map[string][]string{
"root-old-a": {"self", "root-old-bc", "root-old-bc", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"},
"root-old-b": {"self", "root-old-ac", "root-old-ac", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"},
"root-old-c": {"self", "root-old-ab", "root-old-ab", "both-cross-old-new", "both-cross-old-new", "root-new-ab", "root-new-ab"},
"cross-old-new": {"self", "cross-new-old", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"},
"cross-new-old": {"self", "cross-old-new", "both-cliques", "both-cliques", "both-cliques", "both-cliques", "both-cliques"},
"root-new-a": {"self", "root-new-b", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"},
"root-new-b": {"self", "root-new-a", "both-cross-old-new", "both-cross-old-new", "root-old-abc", "root-old-abc", "root-old-abc"},
"inter-a-root-new": {"self", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle", "full-cycle"},
},
Aliases: map[string]string{
"root-old-ac": "root-old-a,root-old-c",
"root-old-ab": "root-old-a,root-old-b",
"root-old-bc": "root-old-b,root-old-c",
"root-old-abc": "root-old-a,root-old-b,root-old-c",
"root-new-ab": "root-new-a,root-new-b",
"both-cross-old-new": "cross-old-new,cross-new-old",
"both-cliques": "root-old-a,root-old-b,root-old-c,root-new-a,root-new-b",
"full-cycle": "root-old-a,root-old-b,root-old-c,cross-old-new,cross-new-old,root-new-a,root-new-b",
},
},
CBIssueLeaf{Issuer: "root-old-a"},
CBIssueLeaf{Issuer: "root-old-b"},
CBIssueLeaf{Issuer: "root-old-c"},
CBIssueLeaf{Issuer: "cross-old-new"},
CBIssueLeaf{Issuer: "cross-new-old"},
CBIssueLeaf{Issuer: "root-new-a"},
CBIssueLeaf{Issuer: "root-new-b"},
CBIssueLeaf{Issuer: "inter-a-root-new"},
},
},
{
// Here we're testing our chain capacity. First we'll create a
// bunch of unique roots to form a cycle of length 10.
Steps: []CBTestStep{
CBGenerateRoot{
Key: "key-root-a",
Name: "root-a",
CommonName: "root-a",
},
CBGenerateRoot{
Key: "key-root-b",
Name: "root-b",
CommonName: "root-b",
},
CBGenerateRoot{
Key: "key-root-c",
Name: "root-c",
CommonName: "root-c",
},
CBGenerateRoot{
Key: "key-root-d",
Name: "root-d",
CommonName: "root-d",
},
CBGenerateRoot{
Key: "key-root-e",
Name: "root-e",
CommonName: "root-e",
},
// They should all be disjoint to start.
CBValidateChain{
Chains: map[string][]string{
"root-a": {"self"},
"root-b": {"self"},
"root-c": {"self"},
"root-d": {"self"},
"root-e": {"self"},
},
},
// Start the cross-signing chains. These are all linear, so there's
// no error expected; they're just long.
CBGenerateIntermediate{
Key: "key-root-b",
Existing: true,
Name: "cross-a-b",
CommonName: "root-b",
Parent: "root-a",
},
CBValidateChain{
Chains: map[string][]string{
"root-a": {"self"},
"cross-a-b": {"self", "root-a"},
"root-b": {"self", "cross-a-b", "root-a"},
"root-c": {"self"},
"root-d": {"self"},
"root-e": {"self"},
},
},
CBGenerateIntermediate{
Key: "key-root-c",
Existing: true,
Name: "cross-b-c",
CommonName: "root-c",
Parent: "root-b",
},
CBValidateChain{
Chains: map[string][]string{
"root-a": {"self"},
"cross-a-b": {"self", "root-a"},
"root-b": {"self", "cross-a-b", "root-a"},
"cross-b-c": {"self", "b-or-cross", "b-chained-cross", "b-chained-cross"},
"root-c": {"self", "cross-b-c", "b-or-cross", "b-chained-cross", "b-chained-cross"},
"root-d": {"self"},
"root-e": {"self"},
},
Aliases: map[string]string{
"b-or-cross": "root-b,cross-a-b",
"b-chained-cross": "root-b,cross-a-b,root-a",
},
},
CBGenerateIntermediate{
Key: "key-root-d",
Existing: true,
Name: "cross-c-d",
CommonName: "root-d",
Parent: "root-c",
},
CBValidateChain{
Chains: map[string][]string{
"root-a": {"self"},
"cross-a-b": {"self", "root-a"},
"root-b": {"self", "cross-a-b", "root-a"},
"cross-b-c": {"self", "b-or-cross", "b-chained-cross", "b-chained-cross"},
"root-c": {"self", "cross-b-c", "b-or-cross", "b-chained-cross", "b-chained-cross"},
"cross-c-d": {"self", "c-or-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross"},
"root-d": {"self", "cross-c-d", "c-or-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross"},
"root-e": {"self"},
},
Aliases: map[string]string{
"b-or-cross": "root-b,cross-a-b",
"b-chained-cross": "root-b,cross-a-b,root-a",
"c-or-cross": "root-c,cross-b-c",
"c-chained-cross": "root-c,cross-b-c,root-b,cross-a-b,root-a",
},
},
CBGenerateIntermediate{
Key: "key-root-e",
Existing: true,
Name: "cross-d-e",
CommonName: "root-e",
Parent: "root-d",
},
CBValidateChain{
Chains: map[string][]string{
"root-a": {"self"},
"cross-a-b": {"self", "root-a"},
"root-b": {"self", "cross-a-b", "root-a"},
"cross-b-c": {"self", "b-or-cross", "b-chained-cross", "b-chained-cross"},
"root-c": {"self", "cross-b-c", "b-or-cross", "b-chained-cross", "b-chained-cross"},
"cross-c-d": {"self", "c-or-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross"},
"root-d": {"self", "cross-c-d", "c-or-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross", "c-chained-cross"},
"cross-d-e": {"self", "d-or-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross"},
"root-e": {"self", "cross-d-e", "d-or-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross", "d-chained-cross"},
},
Aliases: map[string]string{
"b-or-cross": "root-b,cross-a-b",
"b-chained-cross": "root-b,cross-a-b,root-a",
"c-or-cross": "root-c,cross-b-c",
"c-chained-cross": "root-c,cross-b-c,root-b,cross-a-b,root-a",
"d-or-cross": "root-d,cross-c-d",
"d-chained-cross": "root-d,cross-c-d,root-c,cross-b-c,root-b,cross-a-b,root-a",
},
},
CBIssueLeaf{Issuer: "root-a"},
CBIssueLeaf{Issuer: "cross-a-b"},
CBIssueLeaf{Issuer: "root-b"},
CBIssueLeaf{Issuer: "cross-b-c"},
CBIssueLeaf{Issuer: "root-c"},
CBIssueLeaf{Issuer: "cross-c-d"},
CBIssueLeaf{Issuer: "root-d"},
CBIssueLeaf{Issuer: "cross-d-e"},
CBIssueLeaf{Issuer: "root-e"},
// Importing the new e->a cross fails because the cycle
// it builds is too long.
CBGenerateIntermediate{
Key: "key-root-a",
Existing: true,
Name: "cross-e-a",
CommonName: "root-a",
Parent: "root-e",
ImportErrorMessage: "exceeds max size",
},
// Deleting any root and one of its crosses (either a->b or b->c)
// should fix this.
CBDeleteIssuer{"root-b"},
CBDeleteIssuer{"cross-b-c"},
// Importing the new e->a cross fails because the cycle
// it builds is too long.
CBGenerateIntermediate{
Key: "key-root-a",
Existing: true,
Name: "cross-e-a",
CommonName: "root-a",
Parent: "root-e",
},
CBIssueLeaf{Issuer: "root-a"},
CBIssueLeaf{Issuer: "cross-a-b"},
CBIssueLeaf{Issuer: "root-c"},
CBIssueLeaf{Issuer: "cross-c-d"},
CBIssueLeaf{Issuer: "root-d"},
CBIssueLeaf{Issuer: "cross-d-e"},
CBIssueLeaf{Issuer: "root-e"},
CBIssueLeaf{Issuer: "cross-e-a"},
},
},
{
// Here we're testing our clique capacity. First we'll create a
// bunch of unique roots to form a cycle of length 10.
Steps: []CBTestStep{
CBGenerateRoot{
Key: "key-root",
Name: "root-a",
CommonName: "root",
},
CBGenerateRoot{
Key: "key-root",
Existing: true,
Name: "root-b",
CommonName: "root",
},
CBGenerateRoot{
Key: "key-root",
Existing: true,
Name: "root-c",
CommonName: "root",
},
CBGenerateRoot{
Key: "key-root",
Existing: true,
Name: "root-d",
CommonName: "root",
},
CBGenerateRoot{
Key: "key-root",
Existing: true,
Name: "root-e",
CommonName: "root",
},
CBGenerateRoot{
Key: "key-root",
Existing: true,
Name: "root-f",
CommonName: "root",
},
CBIssueLeaf{Issuer: "root-a"},
CBIssueLeaf{Issuer: "root-b"},
CBIssueLeaf{Issuer: "root-c"},
CBIssueLeaf{Issuer: "root-d"},
CBIssueLeaf{Issuer: "root-e"},
CBIssueLeaf{Issuer: "root-f"},
// Seventh reissuance fails.
CBGenerateRoot{
Key: "key-root",
Existing: true,
Name: "root-g",
CommonName: "root",
ErrorMessage: "excessively reissued certificate",
},
// Deleting one and trying again should succeed.
CBDeleteIssuer{"root-a"},
CBGenerateRoot{
Key: "key-root",
Existing: true,
Name: "root-g",
CommonName: "root",
},
CBIssueLeaf{Issuer: "root-b"},
CBIssueLeaf{Issuer: "root-c"},
CBIssueLeaf{Issuer: "root-d"},
CBIssueLeaf{Issuer: "root-e"},
CBIssueLeaf{Issuer: "root-f"},
CBIssueLeaf{Issuer: "root-g"},
},
},
{
// There's one more pathological case here: we have a cycle
// which validates a clique/cycle via cross-signing. We call
// the parent cycle new roots and the child cycle/clique the
// old roots.
Steps: []CBTestStep{
// New Cycle
CBGenerateRoot{
Key: "key-root-new-a",
Name: "root-new-a",
},
CBGenerateRoot{
Key: "key-root-new-b",
Name: "root-new-b",
},
CBGenerateIntermediate{
Key: "key-root-new-b",
Existing: true,
Name: "cross-root-new-b-sig-a",
CommonName: "root-new-b",
Parent: "root-new-a",
},
CBGenerateIntermediate{
Key: "key-root-new-a",
Existing: true,
Name: "cross-root-new-a-sig-b",
CommonName: "root-new-a",
Parent: "root-new-b",
},
// Old Cycle + Clique
CBGenerateRoot{
Key: "key-root-old-a",
Name: "root-old-a",
},
CBGenerateRoot{
Key: "key-root-old-a",
Existing: true,
Name: "root-old-a-reissued",
CommonName: "root-old-a",
},
CBGenerateRoot{
Key: "key-root-old-b",
Name: "root-old-b",
},
CBGenerateRoot{
Key: "key-root-old-b",
Existing: true,
Name: "root-old-b-reissued",
CommonName: "root-old-b",
},
CBGenerateIntermediate{
Key: "key-root-old-b",
Existing: true,
Name: "cross-root-old-b-sig-a",
CommonName: "root-old-b",
Parent: "root-old-a",
},
CBGenerateIntermediate{
Key: "key-root-old-a",
Existing: true,
Name: "cross-root-old-a-sig-b",
CommonName: "root-old-a",
Parent: "root-old-b",
},
// Validate the chains are separate before linking them.
CBValidateChain{
Chains: map[string][]string{
// New stuff
"root-new-a": {"self", "cross-root-new-a-sig-b", "root-new-b-or-cross", "root-new-b-or-cross"},
"root-new-b": {"self", "cross-root-new-b-sig-a", "root-new-a-or-cross", "root-new-a-or-cross"},
"cross-root-new-b-sig-a": {"self", "any-root-new", "any-root-new", "any-root-new"},
"cross-root-new-a-sig-b": {"self", "any-root-new", "any-root-new", "any-root-new"},
// Old stuff
"root-old-a": {"self", "root-old-a-reissued", "cross-root-old-a-sig-b", "cross-root-old-b-sig-a", "both-root-old-b", "both-root-old-b"},
"root-old-a-reissued": {"self", "root-old-a", "cross-root-old-a-sig-b", "cross-root-old-b-sig-a", "both-root-old-b", "both-root-old-b"},
"root-old-b": {"self", "root-old-b-reissued", "cross-root-old-b-sig-a", "cross-root-old-a-sig-b", "both-root-old-a", "both-root-old-a"},
"root-old-b-reissued": {"self", "root-old-b", "cross-root-old-b-sig-a", "cross-root-old-a-sig-b", "both-root-old-a", "both-root-old-a"},
"cross-root-old-b-sig-a": {"self", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "all-root-old"},
"cross-root-old-a-sig-b": {"self", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "all-root-old"},
},
Aliases: map[string]string{
"root-new-a-or-cross": "root-new-a,cross-root-new-a-sig-b",
"root-new-b-or-cross": "root-new-b,cross-root-new-b-sig-a",
"both-root-new": "root-new-a,root-new-b",
"any-root-new": "root-new-a,cross-root-new-a-sig-b,root-new-b,cross-root-new-b-sig-a",
"both-root-old-a": "root-old-a,root-old-a-reissued",
"both-root-old-b": "root-old-b,root-old-b-reissued",
"all-root-old": "root-old-a,root-old-a-reissued,root-old-b,root-old-b-reissued,cross-root-old-b-sig-a,cross-root-old-a-sig-b",
},
},
// Finally, generate an intermediate to link new->old. We
// link root-new-a into root-old-a.
CBGenerateIntermediate{
Key: "key-root-old-a",
Existing: true,
Name: "cross-root-old-a-sig-root-new-a",
CommonName: "root-old-a",
Parent: "root-new-a",
},
CBValidateChain{
Chains: map[string][]string{
// New stuff should be unchanged.
"root-new-a": {"self", "cross-root-new-a-sig-b", "root-new-b-or-cross", "root-new-b-or-cross"},
"root-new-b": {"self", "cross-root-new-b-sig-a", "root-new-a-or-cross", "root-new-a-or-cross"},
"cross-root-new-b-sig-a": {"self", "any-root-new", "any-root-new", "any-root-new"},
"cross-root-new-a-sig-b": {"self", "any-root-new", "any-root-new", "any-root-new"},
// Old stuff
"root-old-a": {"self", "root-old-a-reissued", "cross-root-old-a-sig-b", "cross-root-old-b-sig-a", "both-root-old-b", "both-root-old-b", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"},
"root-old-a-reissued": {"self", "root-old-a", "cross-root-old-a-sig-b", "cross-root-old-b-sig-a", "both-root-old-b", "both-root-old-b", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"},
"root-old-b": {"self", "root-old-b-reissued", "cross-root-old-b-sig-a", "cross-root-old-a-sig-b", "both-root-old-a", "both-root-old-a", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"},
"root-old-b-reissued": {"self", "root-old-b", "cross-root-old-b-sig-a", "cross-root-old-a-sig-b", "both-root-old-a", "both-root-old-a", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"},
"cross-root-old-b-sig-a": {"self", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"},
"cross-root-old-a-sig-b": {"self", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "all-root-old", "cross-root-old-a-sig-root-new-a", "any-root-new", "any-root-new", "any-root-new", "any-root-new"},
// Link
"cross-root-old-a-sig-root-new-a": {"self", "root-new-a-or-cross", "any-root-new", "any-root-new", "any-root-new"},
},
Aliases: map[string]string{
"root-new-a-or-cross": "root-new-a,cross-root-new-a-sig-b",
"root-new-b-or-cross": "root-new-b,cross-root-new-b-sig-a",
"both-root-new": "root-new-a,root-new-b",
"any-root-new": "root-new-a,cross-root-new-a-sig-b,root-new-b,cross-root-new-b-sig-a",
"both-root-old-a": "root-old-a,root-old-a-reissued",
"both-root-old-b": "root-old-b,root-old-b-reissued",
"all-root-old": "root-old-a,root-old-a-reissued,root-old-b,root-old-b-reissued,cross-root-old-b-sig-a,cross-root-old-a-sig-b",
},
},
CBIssueLeaf{Issuer: "root-new-a"},
CBIssueLeaf{Issuer: "root-new-b"},
CBIssueLeaf{Issuer: "cross-root-new-b-sig-a"},
CBIssueLeaf{Issuer: "cross-root-new-a-sig-b"},
CBIssueLeaf{Issuer: "root-old-a"},
CBIssueLeaf{Issuer: "root-old-a-reissued"},
CBIssueLeaf{Issuer: "root-old-b"},
CBIssueLeaf{Issuer: "root-old-b-reissued"},
CBIssueLeaf{Issuer: "cross-root-old-b-sig-a"},
CBIssueLeaf{Issuer: "cross-root-old-a-sig-b"},
CBIssueLeaf{Issuer: "cross-root-old-a-sig-root-new-a"},
},
},
{
// Test a dual-root of trust chaining example with different
// lengths of chains.
Steps: []CBTestStep{
CBGenerateRoot{
Key: "key-root-new",
Name: "root-new",
},
CBGenerateIntermediate{
Key: "key-inter-new",
Name: "inter-new",
Parent: "root-new",
},
CBGenerateRoot{
Key: "key-root-old",
Name: "root-old",
},
CBGenerateIntermediate{
Key: "key-inter-old-a",
Name: "inter-old-a",
Parent: "root-old",
},
CBGenerateIntermediate{
Key: "key-inter-old-b",
Name: "inter-old-b",
Parent: "inter-old-a",
},
// Now generate a cross-signed intermediate to merge these
// two chains.
CBGenerateIntermediate{
Key: "key-cross-old-new",
Name: "cross-old-new-signed-new",
CommonName: "cross-old-new",
Parent: "inter-new",
},
CBGenerateIntermediate{
Key: "key-cross-old-new",
Existing: true,
Name: "cross-old-new-signed-old",
CommonName: "cross-old-new",
Parent: "inter-old-b",
},
CBGenerateIntermediate{
Key: "key-leaf-inter",
Name: "leaf-inter",
Parent: "cross-old-new-signed-new",
},
CBValidateChain{
Chains: map[string][]string{
"root-new": {"self"},
"inter-new": {"self", "root-new"},
"cross-old-new-signed-new": {"self", "inter-new", "root-new"},
"root-old": {"self"},
"inter-old-a": {"self", "root-old"},
"inter-old-b": {"self", "inter-old-a", "root-old"},
"cross-old-new-signed-old": {"self", "inter-old-b", "inter-old-a", "root-old"},
"leaf-inter": {"self", "either-cross", "one-intermediate", "other-inter-or-root", "everything-else", "everything-else", "everything-else", "everything-else"},
},
Aliases: map[string]string{
"either-cross": "cross-old-new-signed-new,cross-old-new-signed-old",
"one-intermediate": "inter-new,inter-old-b",
"other-inter-or-root": "root-new,inter-old-a",
"everything-else": "cross-old-new-signed-new,cross-old-new-signed-old,inter-new,inter-old-b,root-new,inter-old-a,root-old",
},
},
CBIssueLeaf{Issuer: "root-new"},
CBIssueLeaf{Issuer: "inter-new"},
CBIssueLeaf{Issuer: "root-old"},
CBIssueLeaf{Issuer: "inter-old-a"},
CBIssueLeaf{Issuer: "inter-old-b"},
CBIssueLeaf{Issuer: "cross-old-new-signed-new"},
CBIssueLeaf{Issuer: "cross-old-new-signed-old"},
CBIssueLeaf{Issuer: "leaf-inter"},
},
},
{
// Test just a single root.
Steps: []CBTestStep{
CBGenerateRoot{
Key: "key-root",
Name: "root",
},
CBValidateChain{
Chains: map[string][]string{
"root": {"self"},
},
},
CBIssueLeaf{Issuer: "root"},
},
},
{
// Test root + intermediate.
Steps: []CBTestStep{
CBGenerateRoot{
Key: "key-root",
Name: "root",
},
CBGenerateIntermediate{
Key: "key-inter",
Name: "inter",
Parent: "root",
},
CBValidateChain{
Chains: map[string][]string{
"root": {"self"},
"inter": {"self", "root"},
},
},
CBIssueLeaf{Issuer: "root"},
CBIssueLeaf{Issuer: "inter"},
},
},
{
// Test root + intermediate, twice (simulating rotation without
// chaining).
Steps: []CBTestStep{
CBGenerateRoot{
Key: "key-root-a",
Name: "root-a",
},
CBGenerateIntermediate{
Key: "key-inter-a",
Name: "inter-a",
Parent: "root-a",
},
CBGenerateRoot{
Key: "key-root-b",
Name: "root-b",
},
CBGenerateIntermediate{
Key: "key-inter-b",
Name: "inter-b",
Parent: "root-b",
},
CBValidateChain{
Chains: map[string][]string{
"root-a": {"self"},
"inter-a": {"self", "root-a"},
"root-b": {"self"},
"inter-b": {"self", "root-b"},
},
},
CBIssueLeaf{Issuer: "root-a"},
CBIssueLeaf{Issuer: "inter-a"},
CBIssueLeaf{Issuer: "root-b"},
CBIssueLeaf{Issuer: "inter-b"},
},
},
{
// Test root + intermediate, twice, chained a->b.
Steps: []CBTestStep{
CBGenerateRoot{
Key: "key-root-a",
Name: "root-a",
},
CBGenerateIntermediate{
Key: "key-inter-a",
Name: "inter-a",
Parent: "root-a",
},
CBGenerateRoot{
Key: "key-root-b",
Name: "root-b",
},
CBGenerateIntermediate{
Key: "key-inter-b",
Name: "inter-b",
Parent: "root-b",
},
CBGenerateIntermediate{
Key: "key-root-b",
Existing: true,
Name: "cross-a-b",
CommonName: "root-b",
Parent: "root-a",
},
CBValidateChain{
Chains: map[string][]string{
"root-a": {"self"},
"inter-a": {"self", "root-a"},
"root-b": {"self", "cross-a-b", "root-a"},
"inter-b": {"self", "root-b", "cross-a-b", "root-a"},
"cross-a-b": {"self", "root-a"},
},
},
CBIssueLeaf{Issuer: "root-a"},
CBIssueLeaf{Issuer: "inter-a"},
CBIssueLeaf{Issuer: "root-b"},
CBIssueLeaf{Issuer: "inter-b"},
CBIssueLeaf{Issuer: "cross-a-b"},
},
},
}
func Test_CAChainBuilding(t *testing.T) {
t.Parallel()
for testIndex, testCase := range chainBuildingTestCases {
b, s := CreateBackendWithStorage(t)
knownKeys := make(map[string]string)
knownCerts := make(map[string]string)
for stepIndex, testStep := range testCase.Steps {
t.Logf("Running %v / %v", testIndex, stepIndex)
testStep.Run(t, b, s, knownKeys, knownCerts)
}
t.Logf("Checking stable ordering of chains...")
ensureStableOrderingOfChains(t, b, s, knownKeys, knownCerts)
}
}
func BenchmarkChainBuilding(benchies *testing.B) {
for testIndex, testCase := range chainBuildingTestCases {
name := "test-case-" + strconv.Itoa(testIndex)
benchies.Run(name, func(bench *testing.B) {
// Stop the timer as we setup the infra and certs.
bench.StopTimer()
bench.ResetTimer()
b, s := CreateBackendWithStorage(bench)
knownKeys := make(map[string]string)
knownCerts := make(map[string]string)
for _, testStep := range testCase.Steps {
testStep.Run(bench, b, s, knownKeys, knownCerts)
}
// Run the benchmark.
ctx := context.Background()
sc := b.makeStorageContext(ctx, s)
bench.StartTimer()
for n := 0; n < bench.N; n++ {
sc.rebuildIssuersChains(nil)
}
})
}
}