4f6c6ac317
* Starter PKI CA Storage API (#14796) * Simple starting PKI storage api for CA rotation * Add key and issuer storage apis * Add listKeys and listIssuers storage implementations * Add simple keys and issuers configuration storage api methods * Handle resolving key, issuer references The API context will usually have a user-specified reference to the key. This is either the literal string "default" to select the default key, an identifier of the key, or a slug name for the key. Here, we wish to resolve this reference to an actual identifier that can be understood by storage. Also adds the missing Name field to keys. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add method to fetch an issuer's cert bundle This adds a method to construct a certutil.CertBundle from the specified issuer identifier, optionally loading its corresponding key for signing. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Refactor certutil PrivateKey PEM handling This refactors the parsing of PrivateKeys from PEM blobs into shared methods (ParsePEMKey, ParseDERKey) that can be reused by the existing Bundle parsing logic (ParsePEMBundle) or independently in the new issuers/key-based PKI storage code. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add importKey, importCert to PKI storage importKey is generally preferable to the low-level writeKey for adding new entries. This takes only the contents of the private key (as a string -- so a PEM bundle or a managed key handle) and checks if it already exists in the storage. If it does, it returns the existing key instance. Otherwise, we create a new one. In the process, we detect any issuers using this key and link them back to the new key entry. The same holds for importCert over importKey, with the note that keys are not modified when importing certificates. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add tests for importing issuers, keys This adds tests for importing keys and issuers into the new storage layout, ensuring that identifiers are correctly inferred and linked. Note that directly writing entries to storage (writeKey/writeissuer) will take KeyID links from the parent entry and should not be used for import; only existing entries should be updated with this info. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Implement PKI storage migration. - Hook into the backend::initialize function, calling the migration on a primary only. - Migrate an existing certificate bundle to the new issuers and key layout * Make fetchCAInfo aware of new storage layout This allows fetchCAInfo to fetch a specified issuer, via a reference parameter provided by the user. We pass that into the storage layer and have it return a cert bundle for us. Finally, we need to validate that it truly has the key desired. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Begin /issuers API endpoints This implements the fetch operations around issuers in the PKI Secrets Engine. We implement the following operations: - LIST /issuers - returns a list of known issuers' IDs and names. - GET /issuer/:ref - returns a JSON blob with information about this issuer. - POST /issuer/:ref - allows configuring information about issuers, presently just its name. - DELETE /issuer/:ref - allows deleting the specified issuer. - GET /issuer/:ref/{der,pem} - returns a raw API response with just the DER (or PEM) of the issuer's certificate. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add import to PKI Issuers API This adds the two core import code paths to the API: /issuers/import/cert and /issuers/import/bundle. The former differs from the latter in that the latter allows the import of keys. This allows operators to restrict importing of keys to privileged roles, while allowing more operators permission to import additional certificates (not used for signing, but instead for path/chain building). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add /issuer/:ref/sign-intermediate endpoint This endpoint allows existing issuers to be used to sign intermediate CA certificates. In the process, we've updated the existing /root/sign-intermediate endpoint to be equivalent to a call to /issuer/default/sign-intermediate. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add /issuer/:ref/sign-self-issued endpoint This endpoint allows existing issuers to be used to sign self-signed certificates. In the process, we've updated the existing /root/sign-self-issued endpoint to be equivalent to a call to /issuer/default/sign-self-issued. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add /issuer/:ref/sign-verbatim endpoint This endpoint allows existing issuers to be used to directly sign CSRs. In the process, we've updated the existing /sign-verbatim endpoint to be equivalent to a call to /issuer/:ref/sign-verbatim. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Allow configuration of default issuers Using the new updateDefaultIssuerId(...) from the storage migration PR allows for easy implementation of configuring the default issuer. We restrict callers from setting blank defaults and setting default to default. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix fetching default issuers After setting a default issuer, one should be able to use the old /ca, /ca_chain, and /cert/{ca,ca_chain} endpoints to fetch the default issuer (and its chain). Update the fetchCertBySerial helper to no longer support fetching the ca and prefer fetchCAInfo for that instead (as we've already updated that to support fetching the new issuer location). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add /issuer/:ref/{sign,issue}/:role This updates the /sign and /issue endpoints, allowing them to take the default issuer (if none is provided by a role) and adding issuer-specific versions of them. Note that at this point in time, the behavior isn't yet ideal (as /sign/:role allows adding the ref=... parameter to override the default issuer); a later change adding role-based issuer specification will fix this incorrect behavior. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add support root issuer generation * Add support for issuer generate intermediate end-point * Update issuer and key arguments to consistent values - Update all new API endpoints to use the new agreed upon argument names. - issuer_ref & key_ref to refer to existing - issuer_name & key_name for new definitions - Update returned values to always user issuer_id and key_id * Add utility methods to fetch common ref and name arguments - Add utility methods to fetch the issuer_name, issuer_ref, key_name and key_ref arguments from data fields. - Centralize the logic to clean up these inputs and apply various validations to all of them. * Rename common PKI backend handlers - Use the buildPath convention for the function name instead of common... * Move setting PKI defaults from writeCaBundle to proper import{keys,issuer} methods - PR feedback, move setting up the default configuration references within the import methods instead of within the writeCaBundle method. This should now cover all use cases of us setting up the defaults properly. * Introduce constants for issuer_ref, rename isKeyDefaultSet... * Fix legacy PKI sign-verbatim api path - Addresses some test failures due to an incorrect refactoring of a legacy api path /sign-verbatim within PKI * Use import code to handle intermediate, config/ca The existing bundle import code will satisfy the intermediate import; use it instead of the old ca_bundle import logic. Additionally, update /config/ca to use the new import code as well. While testing, a panic was discovered: > reflect.Value.SetMapIndex: value of type string is not assignable to type pki.keyId This was caused by returning a map with type issuerId->keyId; instead switch to returning string->string maps so the audit log can properly HMAC them. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Clarify error message on missing defaults When the default issuer and key are missing (and haven't yet been specified), we should clarify that error message. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Update test semantics for new changes This makes two minor changes to the existing test suite: 1. Importing partial bundles should now succeed, where they'd previously error. 2. fetchCertBySerial no longer handles CA certificates. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add support for deleting all keys, issuers The old DELETE /root code must now delete all keys and issuers for backwards compatibility. We strongly suggest calling individual delete methods (DELETE /key/:key_ref or DELETE /issuer/:issuer_ref) instead, for finer control. In the process, we detect whether the deleted key/issuers was set as the default. This will allow us to warn (from the single key/deletion issuer code) whether or not the default was deleted (while allowing the operation to succeed). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Introduce defaultRef constant within PKI - Replace hardcoded "default" references with a constant to easily identify various usages. - Use the addIssuerRefField function instead of redefining the field in various locations. * Rework PKI test TestBackend_Root_Idempotency - Validate that generate/root calls are no longer idempotent, but the bundle importing does not generate new keys/issuers - As before make sure that the delete root api resets everything - Address a bug within the storage that we bombed when we had multiple different key types within storage. * Assign Name=current to migrated key and issuer - Detail I missed from the RFC was to assign the Name field as "current" for migrated key and issuer. * Build CRL upon PKI intermediary set-signed api called - Add a call to buildCRL if we created an issuer within pathImportIssuers - Augment existing FullCAChain to verify we have a proper CRL post set-signed api call - Remove a code block writing out "ca" storage entry that is no longer used. * Identify which certificate or key failed When importing complex chains, we should identify in which certificate or key the failure occurred. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * PKI migration writes out empty migration log entry - Since the elements of the struct were not exported we serialized an empty migration log to disk and would re-run the migration * Add chain-building logic to PKI issuers path With the one-entry-per-issuer approach, CA Chains become implicitly constructed from the pool of issuers. This roughly matches the existing expectations from /config/ca (wherein a chain could be provided) and /intemediate/set-signed (where a chain may be provided). However, in both of those cases, we simply accepted a chain. Here, we need to be able to reconstruct the chain from parts on disk. However, with potential rotation of roots, we need to be aware of disparate chains. Simply concating together all issuers isn't sufficient. Thus we need to be able to parse a certificate's Issuer and Subject field and reconstruct valid (and potentially parallel) parent<->child mappings. This attempts to handle roots, intermediates, cross-signed intermediates, cross-signed roots, and rotated keys (wherein one might not have a valid signature due to changed key material with the same subject). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Return CA Chain when fetching issuers This returns the CA Chain attribute of an issuer, showing its computed chain based on other issuers in the database, when fetching a specific issuer. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add testing for chain building Using the issuance infrastructure, we generate new certificates (either roots or intermediates), positing that this is roughly equivalent to importing an external bundle (minus error handling during partial imports). This allows us to incrementally construct complex chains, creating reissuance cliques and cross-signing cycles. By using ECDSA certificates, we avoid high signature verification and key generation times. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Allow manual construction of issuer chain Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix handling of duplicate names With the new issuer field (manual_chain), we can no longer err when a name already exists: we might be updating the existing issuer (with the same name), but changing its manual_chain field. Detect this error and correctly handle it. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add tests for manual chain building We break the clique, instead building these chains manually, ensuring that the remaining chains do not change and only the modified certs change. We then reset them (back to implicit chain building) and ensure we get the same results as earlier. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add stricter verification of issuers PEM format This ensures each issuer is only a single certificate entry (as validated by count and parsing) without any trailing data. We further ensure that each certificate PEM has leading and trailing spaces removed with only a single trailing new line remaining. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Fix full chain building Don't set the legacy IssuingCA field on the certificate bundle, as we prefer the CAChain field over it. Additionally, building the full chain could result in duplicate certificates when the CAChain included the leaf certificate itself. When building the full chain, ensure we don't include the bundle's certificate twice. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add stricter tests for full chain construction We wish to ensure that each desired certificate in the chain is only present once. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Rename PKI types to avoid constant variable name collisions keyId -> keyID issuerId -> issuerID key -> keyEntry issuer -> issuerEntry keyConfig -> keyConfigEntry issuerConfig -> issuerConfigEntry * Update CRL handling for multiple issuers When building CRLs, we've gotta make sure certs issued by that issuer land up on that issuer's CRL and not some other CRL. If no CRL is found (matching a cert), we'll place it on the default CRL. However, in the event of equivalent issuers (those with the same subject AND the same key material) -- perhaps due to reissuance -- we'll only create a single (unified) CRL for them. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Allow fetching updated CRL locations This updates fetchCertBySerial to support querying the default issuer's CRL. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Remove legacy CRL storage location test case Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Update to CRLv2 Format to copy RawIssuer When using the older Certificate.CreateCRL(...) call, Go's x509 library copies the parsed pkix.Name version of the CRL Issuer's Subject field. For certain constructed CAs, this fails since pkix.Name is not suitable for round-tripping. This also builds a CRLv1 (per RFC 5280) CRL. In updating to the newer x509.CreateRevocationList(...) call, we can construct the CRL in the CRLv2 format and correctly copy the issuer's name. However, this requires holding an additional field per-CRL, the CRLNumber field, which is required in Go's implementation of CRLv2 (though OPTIONAL in the spec). We store this on the new LocalCRLConfigEntry object, per-CRL. Co-authored-by: Alexander Scheel <alex.scheel@hashicorp.com> Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add comment regarding CRL non-assignment in GOTO In previous versions of Vault, it was possible to sign an empty CRL (when the CRL was disabled and a force-rebuild was requested). Add a comment about this case. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Allow fetching the specified issuer's CRL We add a new API endpoint to fetch the specified issuer's CRL directly (rather than the default issuer's CRL at /crl and /certs/crl). We also add a new test to validate the CRL in a multi-root scenario and ensure it is signed with the correct keys. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add new PKI key prefix to seal wrapped storage (#15126) * Refactor common backend initialization within backend_test - Leverage an existing helper method within the PKI backend tests to setup a PKI backend with storage. * Add ability to read legacy cert bundle if the migration has not occurred on secondaries. - Track the migration state forbidding an issuer/key writing api call if we have not migrated - For operations that just need to read the CA bundle, use the same tracking variable to switch between reading the legacy bundle or use the new key/issuer storage. - Add an invalidation function that will listen for updates to our log path to refresh the state on secondary clusters. * Always write migration entry to trigger secondary clusters to wake up - Some PR feedback and handle a case in which the primary cluster does not have a CA bundle within storage but somehow a secondary does. * Update CA Chain to report entire chain This merges the ca_chain JSON field (of the /certs/ca_chain path) with the regular certificate field, returning the root of trust always. This also affects the non-JSON (raw) endpoints as well. We return the default issuer's chain here, rather than all known issuers (as that may not form a strict chain). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Allow explicit issuer override on roles When a role is used to generate a certificate (such as with the sign/ and issue/ legacy paths or the legacy sign-verbatim/ paths), we prefer that issuer to the one on the request. This allows operators to set an issuer (other than default) for requests to be issued against, effectively making the change no different from the users' perspective as it is "just" a different role name. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add tests for role-based issuer selection Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Expand NotAfter limit enforcement behavior Vault previously strictly enforced NotAfter/ttl values on certificate requests, erring if the requested TTL extended past the NotAfter date of the issuer. In the event of issuing an intermediate, this behavior was ignored, instead permitting the issuance. Users generally do not think to check their issuer's NotAfter date when requesting a certificate; thus this behavior was generally surprising. Per RFC 5280 however, issuers need to maintain status information throughout the life cycle of the issued cert. If this leaf cert were to be issued for a longer duration than the parent issuer, the CA must still maintain revocation information past its expiration. Thus, we add an option to the issuer to change the desired behavior: - err, to err out, - permit, to permit the longer NotAfter date, or - truncate, to silently truncate the expiration to the issuer's NotAfter date. Since expiration of certificates in the system's trust store are not generally validated (when validating an arbitrary leaf, e.g., during TLS validation), permit should generally only be used in that case. However, browsers usually validate intermediate's validity periods, and thus truncate should likely be used (as with permit, the leaf's chain will not validate towards the end of the issuance period). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add tests for expanded issuance behaviors Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add warning on keyless default issuer (#15178) Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Update PKI to new Operations framework (#15180) The backend Framework has updated Callbacks (used extensively in PKI) to become deprecated; Operations takes their place and clarifies forwarding of requests. We switch to the new format everywhere, updating some bad assumptions about forwarding along the way. Anywhere writes are handled (that should be propagated to all nodes in all clusters), we choose to forward the request all the way up to the performance primary cluster's primary node. This holds for issuers/keys, roles, and configs (such as CRL config, which is globally set for all clusters despite all clusters having their own separate CRL). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Kitography/vault 5474 rebase (#15150) * These parts work (put in signature so that backend wouldn't break, but missing fields, desc, etc.) * Import and Generate API calls w/ needed additions to SDK. * make fmt * Add Help/Sync Text, fix some of internal/exported/kms code. * Fix PEM/DER Encoding issue. * make fmt * Standardize keyIdParam, keyNameParam, keyTypeParam * Add error response if key to be deleted is in use. * replaces all instances of "default" in code with defaultRef * Updates from Callbacks to Operations Function with explicit forwarding. * Fixes a panic with names not being updated everywhere. * add a logged error in addition to warning on deleting default key. * Normalize whitespace upon importing keys. Authored-by: Alexander Scheel <alexander.m.scheel@gmail.com> * Fix isKeyInUse functionality. * Fixes tests associated with newline at end of key pem. * Add alternative proposal PKI aliased paths (#15211) * Add aliased path for root/rotate/:exported This adds a user-friendly path name for generating a rotated root. We automatically choose the name "next" for the newly generated root at this path if it doesn't already exist. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add aliased path for intermediate/cross-sign This allows cross-signatures to work. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add path for replacing the current root This updates default to point to the value of the issuer with name "next" rather than its current value. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Remove plural issuers/ in signing paths These paths use a single issuer and thus shouldn't include the plural issuers/ as a path prefix, instead using the singular issuer/ path prefix. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Only warn if default issuer was imported When the default issuer was not (re-)imported, we'd fail to find it, causing an extraneous warning about missing keys, even though this issuer indeed had a key. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add missing issuer sign/issue paths Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Clean up various warnings within the PKI package (#15230) * Rebuild CRLs on secondary performance clusters post migration and on new/updated issuers - Hook into the backend invalidation function so that secondaries are notified of new/updated issuer or migrations occuring on the primary cluster. Upon notification schedule a CRL rebuild to take place upon the next process to read/update the CRL or within the periodic function if no request comes in. * Schedule rebuilding PKI CRLs on active nodes only - Address an issue that we were scheduling the rebuilding of a CRL on standby nodes, which would not be able to write to storage. - Fix an issue with standby nodes not correctly determining that a migration previously occurred. * Return legacy CRL storage path when no migration has occurred. * Handle issuer, keys locking (#15227) * Handle locking of issuers during writes We need a write lock around writes to ensure serialization of modifications. We use a single lock for both issuer and key updates, in part because certain operations (like deletion) will potentially affect both. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add missing b.useLegacyBundleCaStorage guards Several locations needed to guard against early usage of the new issuers endpoint pre-migration. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Address PKI to properly support managed keys (#15256) * Address codebase for managed key fixes * Add proper public key comparison for better managed key support to importKeys * Remove redundant public key fetching within PKI importKeys * Correctly handle rebuilding remaining chains When deleting a specific issuer, we might impact the chains. From a consistency perspective, we need to ensure the remaining chains are correct and don't refer to the since-deleted issuer, so trigger a full rebuild here. We don't need to call this in the delete-the-world (DELETE /root) code path, as there shouldn't be any remaining issuers or chains to build. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Remove legacy CRL bundle on world deletion When calling DELETE /root, we should remove the legacy CRL bundle, since we're deleting the legacy CA issuer bundle as well. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Remove deleted issuers' CRL entries Since CRLs are no longer resolvable after deletion (due to missing issuer ID, which will cause resolution to fail regardless of if an ID or a name/default reference was used), we should delete these CRLs from storage to avoid leaking them. In the event that this issuer comes back (with key material), we can simply rebuild the CRL at that time (from the remaining revoked storage entries). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add unauthed JSON fetching of CRLs, Issuers (#15253) Default to fetching JSON CRL for consistency This makes the bare issuer-specific CRL fetching endpoint return the JSON-wrapped CRL by default, moving the DER CRL to a specific endpoint. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Add JSON-specific endpoint for fetching issuers Unlike the unqualified /issuer/:ref endpoint (which also returns JSON), we have a separate /issuer/:ref/json endpoint to return _only_ the PEM-encoded certificate and the chain, mirroring the existing /cert/ca endpoint but for a specific issuer. This allows us to make the endpoint unauthenticated, whereas the bare endpoint would remain authenticated and usually privileged. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> Add tests for raw JSON endpoints Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add unauthenticated issuers endpoints to PKI table This adds the unauthenticated issuers endpoints? - LIST /issuers, - Fetching _just_ the issuer certificates (in JSON/DER/PEM form), and - Fetching the CRL of this issuer (in JSON/DER/PEM form). Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add issuer usage restrictions bitset This allows issuers to have usage restrictions, limiting whether they can be used to issue certificates or if they can generate CRLs. This allows certain issuers to not generate a CRL (if the global config is with the CRL enabled) or allows the issuer to not issue new certificates (but potentially letting the CRL generation continue). Setting both fields to false effectively forms a soft delete capability. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * PKI Pod rotation Add Base Changelog (#15283) * PKI Pod rotation changelog. * Use feature release-note formatting of changelog. Co-authored-by: Steven Clark <steven.clark@hashicorp.com> Co-authored-by: Kit Haines <kit.haines@hashicorp.com> Co-authored-by: kitography <khaines@mit.edu>
1306 lines
45 KiB
Go
1306 lines
45 KiB
Go
package pki
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"sort"
|
|
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
)
|
|
|
|
func prettyIssuer(issuerIdEntryMap map[issuerID]*issuerEntry, issuer issuerID) string {
|
|
if entry, ok := issuerIdEntryMap[issuer]; ok && len(entry.Name) > 0 {
|
|
return "[id:" + string(issuer) + "/name:" + entry.Name + "]"
|
|
}
|
|
|
|
return "[" + string(issuer) + "]"
|
|
}
|
|
|
|
func rebuildIssuersChains(ctx context.Context, s logical.Storage, referenceCert *issuerEntry /* optional */) error {
|
|
// This function rebuilds the CAChain field of all known issuers. This
|
|
// function should usually be invoked when a new issuer is added to the
|
|
// pool of issuers.
|
|
//
|
|
// In addition to the context and storage, we take an optional
|
|
// referenceCert parameter -- an issuer certificate that we should write
|
|
// to storage once done, but which might not be persisted yet (either due
|
|
// to new values on it or due to it not yet existing in the list). This is
|
|
// helpful when calling e.g., importIssuer(...) (from storage.go), to allow
|
|
// the newly imported issuer to have its CAChain field computed, but
|
|
// without writing and re-reading it from storage (potentially failing in
|
|
// the process if chain building failed).
|
|
//
|
|
// Our contract guarantees that, if referenceCert is provided, we'll write
|
|
// it to storage. Further, we guarantee that (given the issuers haven't
|
|
// changed), the results will be stable on multiple calls to rebuild the
|
|
// chain.
|
|
//
|
|
// Note that at no point in time do we fetch the private keys associated
|
|
// with any issuers. It is sufficient to merely look at the issuers
|
|
// themselves.
|
|
//
|
|
// To begin, we fetch all known issuers from disk.
|
|
issuers, err := listIssuers(ctx, s)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to list issuers to build chain: %v", err)
|
|
}
|
|
|
|
// Fast path: no issuers means we can set the reference cert's value, if
|
|
// provided, to itself.
|
|
if len(issuers) == 0 {
|
|
if referenceCert == nil {
|
|
// Nothing to do; no reference cert was provided.
|
|
return nil
|
|
}
|
|
|
|
// Otherwise, the only entry in the chain (that we know about) is the
|
|
// certificate itself.
|
|
referenceCert.CAChain = []string{referenceCert.Certificate}
|
|
return writeIssuer(ctx, s, referenceCert)
|
|
}
|
|
|
|
// Our provided reference cert might not be in the list of issuers. In
|
|
// that case, add it manually.
|
|
if referenceCert != nil {
|
|
missing := true
|
|
for _, issuer := range issuers {
|
|
if issuer == referenceCert.ID {
|
|
missing = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if missing {
|
|
issuers = append(issuers, referenceCert.ID)
|
|
}
|
|
}
|
|
|
|
// Now call a stable sorting algorithm here. We want to ensure the results
|
|
// are the same across multiple calls to rebuildIssuersChains with the same
|
|
// input data.
|
|
sort.SliceStable(issuers, func(i, j int) bool {
|
|
return issuers[i] < issuers[j]
|
|
})
|
|
|
|
// We expect each of these maps to be the size of the number of issuers
|
|
// we have (as we're mapping from issuers to other values).
|
|
//
|
|
// The first caches the storage entry for the issuer, the second caches
|
|
// the parsed *x509.Certificate of the issuer itself, and the third and
|
|
// fourth maps that certificate back to the other issuers with that
|
|
// subject (note the keyword _other_: we'll exclude self-loops here) --
|
|
// either via a parent or child relationship.
|
|
issuerIdEntryMap := make(map[issuerID]*issuerEntry, len(issuers))
|
|
issuerIdCertMap := make(map[issuerID]*x509.Certificate, len(issuers))
|
|
issuerIdParentsMap := make(map[issuerID][]issuerID, len(issuers))
|
|
issuerIdChildrenMap := make(map[issuerID][]issuerID, len(issuers))
|
|
|
|
// For every known issuer, we map that subject back to the id of issuers
|
|
// containing that subject. This lets us build our issuerID -> parents
|
|
// mapping efficiently. Worst case we'll have a single linear chain where
|
|
// every entry has a distinct subject.
|
|
subjectIssuerIdsMap := make(map[string][]issuerID, len(issuers))
|
|
|
|
// First, read every issuer entry from storage. We'll propagate entries
|
|
// to three of the maps here: all but issuerIdParentsMap and
|
|
// issuerIdChildrenMap, which we'll do in a second pass.
|
|
for _, identifier := range issuers {
|
|
var stored *issuerEntry
|
|
|
|
// When the reference issuer is provided and matches this identifier,
|
|
// prefer the updated reference copy instead.
|
|
if referenceCert != nil && identifier == referenceCert.ID {
|
|
stored = referenceCert
|
|
} else {
|
|
// Otherwise, fetch it from disk.
|
|
stored, err = fetchIssuerById(ctx, s, identifier)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to fetch issuer %v to build chain: %v", identifier, err)
|
|
}
|
|
}
|
|
|
|
if stored == nil || len(stored.Certificate) == 0 {
|
|
return fmt.Errorf("bad issuer while building chain: missing certificate entry: %v", identifier)
|
|
}
|
|
|
|
issuerIdEntryMap[identifier] = stored
|
|
cert, err := stored.GetCertificate()
|
|
if err != nil {
|
|
return fmt.Errorf("unable to parse issuer %v to certificate to build chain: %v", identifier, err)
|
|
}
|
|
|
|
issuerIdCertMap[identifier] = cert
|
|
subjectIssuerIdsMap[string(cert.RawSubject)] = append(subjectIssuerIdsMap[string(cert.RawSubject)], identifier)
|
|
}
|
|
|
|
// Now that we have the subj->issuer map built, we can build the parent
|
|
// and child mappings. We iterate over all issuers and build it one step
|
|
// at a time.
|
|
//
|
|
// This is worst case O(n^2) because all of the issuers could have the
|
|
// same name and be self-signed certs with different keys. That makes the
|
|
// chain building (below) fast as they've all got empty parents/children
|
|
// maps.
|
|
//
|
|
// Note that the order of iteration is stable. Why? We've built
|
|
// subjectIssuerIdsMap from the (above) sorted issuers by appending the
|
|
// next entry to the present list; since they're already sorted, that
|
|
// lookup will also be sorted. Thus, each of these iterations are also
|
|
// in sorted order, so the resulting map entries (of ids) are also sorted.
|
|
// Thus, the graph structure is in sorted order and thus the toposort
|
|
// below will be stable.
|
|
for _, child := range issuers {
|
|
// Fetch the certificate as we'll need it later.
|
|
childCert := issuerIdCertMap[child]
|
|
|
|
parentSubject := string(issuerIdCertMap[child].RawIssuer)
|
|
parentCerts, ok := subjectIssuerIdsMap[parentSubject]
|
|
if !ok {
|
|
// When the issuer isn't known to Vault, the lookup by the issuer
|
|
// will be empty. This most commonly occurs when intermediates are
|
|
// directly added (via intermediate/set-signed) without providing
|
|
// the root.
|
|
continue
|
|
}
|
|
|
|
// Now, iterate over all possible parents and assign the child/parent
|
|
// relationship.
|
|
for _, parent := range parentCerts {
|
|
// Skip self-references to the exact same certificate.
|
|
if child == parent {
|
|
continue
|
|
}
|
|
|
|
// While we could use Subject/Authority Key Identifier (SKI/AKI)
|
|
// as a heuristic for whether or not this relationship is valid,
|
|
// this is insufficient as otherwise valid CA certificates could
|
|
// elide this information. That means its best to actually validate
|
|
// the signature (e.g., call child.CheckSignatureFrom(parent))
|
|
// instead.
|
|
parentCert := issuerIdCertMap[parent]
|
|
if err := childCert.CheckSignatureFrom(parentCert); err != nil {
|
|
// We cannot return an error here as it could be that this
|
|
// signature is entirely valid -- but just for a different
|
|
// key. Instead, skip adding the parent->child and
|
|
// child->parent link.
|
|
continue
|
|
}
|
|
|
|
// Otherwise, we can append it to the map, allowing us to walk the
|
|
// issuer->parent mapping.
|
|
issuerIdParentsMap[child] = append(issuerIdParentsMap[child], parent)
|
|
|
|
// Also cross-add the child relationship step at the same time.
|
|
issuerIdChildrenMap[parent] = append(issuerIdChildrenMap[parent], child)
|
|
}
|
|
}
|
|
|
|
// Finally, we consult RFC 8446 Section 4.4.2 for creating an algorithm for
|
|
// building the chain:
|
|
//
|
|
// > ... The sender's certificate MUST come in the first
|
|
// > CertificateEntry in the list. Each following certificate SHOULD
|
|
// > directly certify the one immediately preceding it. Because
|
|
// > certificate validation requires that trust anchors be distributed
|
|
// > independently, a certificate that specifies a trust anchor MAY be
|
|
// > omitted from the chain, provided that supported peers are known to
|
|
// > possess any omitted certificates.
|
|
// >
|
|
// > Note: Prior to TLS 1.3, "certificate_list" ordering required each
|
|
// > certificate to certify the one immediately preceding it; however,
|
|
// > some implementations allowed some flexibility. Servers sometimes
|
|
// > send both a current and deprecated intermediate for transitional
|
|
// > purposes, and others are simply configured incorrectly, but these
|
|
// > cases can nonetheless be validated properly. For maximum
|
|
// > compatibility, all implementations SHOULD be prepared to handle
|
|
// > potentially extraneous certificates and arbitrary orderings from any
|
|
// > TLS version, with the exception of the end-entity certificate which
|
|
// > MUST be first.
|
|
//
|
|
// So, we take this to mean we should build chains via DFS: each issuer is
|
|
// explored until an empty parent pointer (i.e., self-loop) is reached and
|
|
// then the last most recently seen duplicate parent link is then explored.
|
|
//
|
|
// However, we don't actually need to do a DFS (per issuer) here. We can
|
|
// simply invert the (pseudo-)directed graph, i.e., topologically sort it.
|
|
// Some number of certs (roots without cross-signing) lack parent issuers.
|
|
// These are already "done" from the PoV of chain building. We can thus
|
|
// iterating through the parent mapping to find entries without parents to
|
|
// start the sort. After processing, we can add all children and visit them
|
|
// if all parents have been processed.
|
|
//
|
|
// Note though, that while topographical sorting is equivalent to the DFS,
|
|
// we have to take care to make it a pseudo-DAG. This means handling the
|
|
// most common 2-star (2-clique) sub-graphs of reissued certificates,
|
|
// manually building their chain prior to starting the topographical sort.
|
|
//
|
|
// This thus runs in O(|V| + |E|) -> O(n^2) in the number of issuers.
|
|
processedIssuers := make(map[issuerID]bool, len(issuers))
|
|
toVisit := make([]issuerID, 0, len(issuers))
|
|
|
|
// Handle any explicitly constructed certificate chains. Here, we don't
|
|
// validate much what the user provides; if they provide since-deleted
|
|
// refs, skip them; if they duplicate entries, add them multiple times.
|
|
// The other chain building logic will be able to deduplicate them when
|
|
// used as parents to other certificates.
|
|
for _, candidate := range issuers {
|
|
entry := issuerIdEntryMap[candidate]
|
|
if len(entry.ManualChain) == 0 {
|
|
continue
|
|
}
|
|
|
|
entry.CAChain = nil
|
|
for _, parentId := range entry.ManualChain {
|
|
parentEntry := issuerIdEntryMap[parentId]
|
|
if parentEntry == nil {
|
|
continue
|
|
}
|
|
|
|
entry.CAChain = append(entry.CAChain, parentEntry.Certificate)
|
|
}
|
|
|
|
// Mark this node as processed and add its children.
|
|
processedIssuers[candidate] = true
|
|
children, ok := issuerIdChildrenMap[candidate]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, child := range children {
|
|
toVisit = append(toVisit, child)
|
|
}
|
|
}
|
|
|
|
// Setup the toVisit queue.
|
|
for _, candidate := range issuers {
|
|
parentCerts, ok := issuerIdParentsMap[candidate]
|
|
if ok && len(parentCerts) > 0 {
|
|
// Assumption: no self-loops in the parent mapping, so if there's
|
|
// a non-empty parent mapping it means we can skip this node as
|
|
// it can't be processed yet.
|
|
continue
|
|
}
|
|
|
|
// Because this candidate has no known parent issuers; update the
|
|
// list.
|
|
toVisit = append(toVisit, candidate)
|
|
}
|
|
|
|
// If the queue is empty (and we know we have issuers), trigger the
|
|
// clique/cycle detection logic so we aren't starved for nodes.
|
|
if len(toVisit) == 0 {
|
|
toVisit, err = processAnyCliqueOrCycle(issuers, processedIssuers, toVisit, issuerIdEntryMap, issuerIdCertMap, issuerIdParentsMap, issuerIdChildrenMap, subjectIssuerIdsMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Now actually build the CAChain entries... Use a safety mechanism to
|
|
// ensure we don't accidentally infinite-loop (if we introduce a bug).
|
|
maxVisitCount := len(issuers)*len(issuers)*len(issuers) + 100
|
|
for len(toVisit) > 0 && maxVisitCount >= 0 {
|
|
var issuer issuerID
|
|
issuer, toVisit = toVisit[0], toVisit[1:]
|
|
|
|
// If (and only if) we're presently starved for next nodes to visit,
|
|
// attempt to resolve cliques and cycles again to fix that. This is
|
|
// because all-cycles cycle detection is at least as costly as
|
|
// traversing the entire graph a couple of times.
|
|
//
|
|
// Additionally, we do this immediately after popping a node from the
|
|
// queue as we wish to ensure we never become starved for nodes.
|
|
if len(toVisit) == 0 {
|
|
toVisit, err = processAnyCliqueOrCycle(issuers, processedIssuers, toVisit, issuerIdEntryMap, issuerIdCertMap, issuerIdParentsMap, issuerIdChildrenMap, subjectIssuerIdsMap)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Self-loops and cross-signing might lead to this node already being
|
|
// processed; skip it on the second pass.
|
|
if processed, ok := processedIssuers[issuer]; ok && processed {
|
|
continue
|
|
}
|
|
|
|
// Check our parent certs now; if they are all processed, we can
|
|
// process this node. Otherwise, we'll re-add this to the queue
|
|
// when the last parent is processed (and we re-add its children).
|
|
parentCerts, ok := issuerIdParentsMap[issuer]
|
|
if ok && len(parentCerts) > 0 {
|
|
// For each parent, validate that we've processed it.
|
|
mustSkip := false
|
|
for _, parentCert := range parentCerts {
|
|
if processed, ok := processedIssuers[parentCert]; !ok || !processed {
|
|
mustSkip = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if mustSkip {
|
|
// Skip this node for now, we'll come back to it later.
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Now we can build the chain. Start with the current cert...
|
|
entry := issuerIdEntryMap[issuer]
|
|
entry.CAChain = []string{entry.Certificate}
|
|
|
|
// ...and add all parents into it. Note that we have to tell if
|
|
// that parent was already visited or not.
|
|
if ok && len(parentCerts) > 0 {
|
|
includedParentCerts := make(map[string]bool, len(parentCerts)+1)
|
|
includedParentCerts[entry.Certificate] = true
|
|
for _, parentCert := range parentCerts {
|
|
// See discussion of the algorithm above as to why this is
|
|
// in the correct order. However, note that we do need to
|
|
// exclude duplicate certs, hence the map above.
|
|
//
|
|
// Assumption: issuerIdEntryMap and issuerIdParentsMap is well
|
|
// constructed.
|
|
parent := issuerIdEntryMap[parentCert]
|
|
for _, parentChainCert := range parent.CAChain {
|
|
addToChainIfNotExisting(includedParentCerts, entry, parentChainCert)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now, mark this node as processed and go and visit all of its
|
|
// children.
|
|
processedIssuers[issuer] = true
|
|
|
|
childrenCerts, ok := issuerIdChildrenMap[issuer]
|
|
if ok && len(childrenCerts) > 0 {
|
|
toVisit = append(toVisit, childrenCerts...)
|
|
}
|
|
}
|
|
|
|
// Assumption: no nodes left unprocessed. They should've either been
|
|
// reached through the parent->child addition or they should've been
|
|
// self-loops.
|
|
var msg string
|
|
for _, issuer := range issuers {
|
|
if visited, ok := processedIssuers[issuer]; !ok || !visited {
|
|
pretty := prettyIssuer(issuerIdEntryMap, issuer)
|
|
msg += fmt.Sprintf("[failed to build chain correctly: unprocessed issuer %v: ok: %v; visited: %v]\n", pretty, ok, visited)
|
|
}
|
|
}
|
|
if len(msg) > 0 {
|
|
return fmt.Errorf(msg)
|
|
}
|
|
|
|
// Finally, write all issuers to disk.
|
|
for _, issuer := range issuers {
|
|
entry := issuerIdEntryMap[issuer]
|
|
|
|
err := writeIssuer(ctx, s, entry)
|
|
if err != nil {
|
|
pretty := prettyIssuer(issuerIdEntryMap, issuer)
|
|
return fmt.Errorf("failed to persist issuer (%v) chain to disk: %v", pretty, err)
|
|
}
|
|
}
|
|
|
|
// Everything worked \o/
|
|
return nil
|
|
}
|
|
|
|
func addToChainIfNotExisting(includedParentCerts map[string]bool, entry *issuerEntry, certToAdd string) {
|
|
included, ok := includedParentCerts[certToAdd]
|
|
if ok && included {
|
|
return
|
|
}
|
|
|
|
entry.CAChain = append(entry.CAChain, certToAdd)
|
|
includedParentCerts[certToAdd] = true
|
|
}
|
|
|
|
func processAnyCliqueOrCycle(
|
|
issuers []issuerID,
|
|
processedIssuers map[issuerID]bool,
|
|
toVisit []issuerID,
|
|
issuerIdEntryMap map[issuerID]*issuerEntry,
|
|
issuerIdCertMap map[issuerID]*x509.Certificate,
|
|
issuerIdParentsMap map[issuerID][]issuerID,
|
|
issuerIdChildrenMap map[issuerID][]issuerID,
|
|
subjectIssuerIdsMap map[string][]issuerID,
|
|
) ([]issuerID /* toVisit */, error) {
|
|
// Topological sort really only works on directed acyclic graphs (DAGs).
|
|
// But a pool of arbitrary (issuer) certificates are actually neither!
|
|
// This pool could contain both cliques and cycles. Because this could
|
|
// block chain construction, we need to handle these cases.
|
|
//
|
|
// Within the helper for rebuildIssuersChains, we realize that we might
|
|
// have certain pathological cases where cliques and cycles might _mix_.
|
|
// This warrants handling them outside of the topo-sort code, effectively
|
|
// acting as a node-collapsing technique (turning many nodes into one).
|
|
// In reality, we just special-case this and handle the processing of
|
|
// these nodes manually, fixing their CAChain value and then skipping
|
|
// them.
|
|
//
|
|
// Since clique detection is (in this case) cheap (at worst O(n) on the
|
|
// size of the graph), we favor it over the cycle detection logic. The
|
|
// order (in the case of mixed cliques+cycles) doesn't matter, as the
|
|
// discovery of the clique will lead to the cycle. We additionally find
|
|
// all (unprocessed) cliques first, so our cycle detection code can avoid
|
|
// falling into cliques.
|
|
//
|
|
// We need to be able to handle cliques adjacent to cycles. This is
|
|
// necessary because a cross-signed cert (with same subject and key as
|
|
// the clique, but different issuer) could be part of a cycle; this cycle
|
|
// loop forms a parent chain (that topo-sort can't resolve) -- AND the
|
|
// clique itself mixes with this, so resolving one or the other isn't
|
|
// sufficient (as the reissued clique plus the cross-signed cert
|
|
// effectively acts as a single node in the cycle). Oh, and there might
|
|
// be multiple cycles. :-)
|
|
//
|
|
// We also might just have cycles, separately from reissued cliques.
|
|
//
|
|
// The nice thing about both cliques and cycles is that, as long as you
|
|
// deduplicate your certs, all issuers in the collection (including the
|
|
// mixed collection) have the same chain entries, just in different
|
|
// orders (preferring the cycle and appending the remaining clique
|
|
// entries afterwards).
|
|
|
|
// To begin, cache all cliques that we know about.
|
|
allCliques, issuerIdCliqueMap, allCliqueNodes, err := findAllCliques(processedIssuers, issuerIdCertMap, subjectIssuerIdsMap, issuers)
|
|
if err != nil {
|
|
// Found a clique that is too large; exit with an error.
|
|
return nil, err
|
|
}
|
|
|
|
for _, issuer := range issuers {
|
|
// Skip anything that's already been processed.
|
|
if processed, ok := processedIssuers[issuer]; ok && processed {
|
|
continue
|
|
}
|
|
|
|
// This first branch is finding cliques. However, finding a clique is
|
|
// not sufficient as discussed above -- we also need to find any
|
|
// incident cycle as this cycle is a parent and child to the clique,
|
|
// which means the cycle nodes _must_ include the clique _and_ the
|
|
// clique must include the cycle (in the CA Chain computation).
|
|
// However, its not sufficient to just do one and then the other:
|
|
// we need the closure of all cliques (and their incident cycles).
|
|
// Finally -- it isn't enough to consider this chain in isolation
|
|
// either. We need to consider _all_ parents and ensure they've been
|
|
// processed before processing this closure.
|
|
var cliques [][]issuerID
|
|
var cycles [][]issuerID
|
|
closure := make(map[issuerID]bool)
|
|
|
|
var cliquesToProcess []issuerID
|
|
cliquesToProcess = append(cliquesToProcess, issuer)
|
|
|
|
for len(cliquesToProcess) > 0 {
|
|
var node issuerID
|
|
node, cliquesToProcess = cliquesToProcess[0], cliquesToProcess[1:]
|
|
|
|
// Skip potential clique nodes which have already been processed
|
|
// (either by the topo-sort or by this clique-finding code).
|
|
if processed, ok := processedIssuers[node]; ok && processed {
|
|
continue
|
|
}
|
|
if nodeInClosure, ok := closure[node]; ok && nodeInClosure {
|
|
continue
|
|
}
|
|
|
|
// Check if we have a clique for this node from our computed
|
|
// collection of cliques.
|
|
cliqueId, ok := issuerIdCliqueMap[node]
|
|
if !ok {
|
|
continue
|
|
}
|
|
cliqueNodes := allCliques[cliqueId]
|
|
|
|
// Add our discovered clique. Note that we avoid duplicate cliques by
|
|
// the skip logic above. Additionally, we know that cliqueNodes must
|
|
// be unique and not duplicated with any existing nodes so we can add
|
|
// all nodes to closure.
|
|
cliques = append(cliques, cliqueNodes)
|
|
for _, node := range cliqueNodes {
|
|
closure[node] = true
|
|
}
|
|
|
|
// Try and expand the clique to see if there's common cycles around
|
|
// it. We exclude _all_ clique nodes from the expansion path, because
|
|
// it will unnecessarily bloat the detected cycles AND we know that
|
|
// we'll find them again from the neighborhood search.
|
|
//
|
|
// Additionally, note that, detection of cycles should be independent
|
|
// of cliques: cliques form under reissuance, and cycles form via
|
|
// cross-signing chains; the latter ensures that any cliques can be
|
|
// strictly bypassed from cycles (but the chain construction later
|
|
// ensures we pull in the cliques into the cycles).
|
|
foundCycles, err := findCyclesNearClique(processedIssuers, issuerIdChildrenMap, allCliqueNodes)
|
|
if err != nil {
|
|
// Cycle is too large.
|
|
return toVisit, err
|
|
}
|
|
|
|
// Assumption: each cycle in foundCycles is in canonical order (see note
|
|
// below about canonical ordering). Deduplicate these against already
|
|
// existing cycles and add them to the closure nodes.
|
|
for _, cycle := range foundCycles {
|
|
cycles = appendCycleIfNotExisting(cycles, cycle)
|
|
|
|
// Now, for each cycle node, we need to find all adjacent cliques.
|
|
// We do this by finding each child of the cycle and adding it to
|
|
// the queue. If these nodes aren't on cliques, we'll skip them
|
|
// fairly quickly since the cliques were pre-computed.
|
|
for _, cycleNode := range cycle {
|
|
children, ok := issuerIdChildrenMap[cycleNode]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, child := range children {
|
|
cliquesToProcess = append(cliquesToProcess, child)
|
|
}
|
|
|
|
// While we're here, add this cycle node to the closure.
|
|
closure[cycleNode] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Before we begin, we need to compute the _parents_ of the nodes in
|
|
// these cliques and cycles and ensure they've all been processed (if
|
|
// they're not already part of the closure).
|
|
parents, ok := computeParentsFromClosure(processedIssuers, issuerIdParentsMap, closure)
|
|
if !ok {
|
|
// At least one parent wasn't processed; skip this cliques and
|
|
// cycles group for now until they have all been processed.
|
|
continue
|
|
}
|
|
|
|
// Ok, we've computed the closure. Now we can build CA nodes and mark
|
|
// everything as processed, growing the toVisit queue in the process.
|
|
// For every node we've found...
|
|
for node := range closure {
|
|
// Skip anything that's already been processed.
|
|
if processed, ok := processedIssuers[node]; ok && processed {
|
|
continue
|
|
}
|
|
|
|
// Before we begin, mark this node as processed (so we can continue
|
|
// later) and add children to toVisit.
|
|
processedIssuers[node] = true
|
|
childrenCerts, ok := issuerIdChildrenMap[node]
|
|
if ok && len(childrenCerts) > 0 {
|
|
toVisit = append(toVisit, childrenCerts...)
|
|
}
|
|
|
|
// It can either be part of a clique or a cycle. We wish to add
|
|
// the nodes of whatever grouping
|
|
foundNode := false
|
|
for _, clique := range cliques {
|
|
inClique := false
|
|
for _, cliqueNode := range clique {
|
|
if cliqueNode == node {
|
|
inClique = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if inClique {
|
|
foundNode = true
|
|
|
|
// Compute this node's CAChain. Note order doesn't matter
|
|
// (within the clique), but we'll preserve the relative
|
|
// order of associated cycles.
|
|
entry := issuerIdEntryMap[node]
|
|
entry.CAChain = []string{entry.Certificate}
|
|
|
|
includedParentCerts := make(map[string]bool, len(closure)+1)
|
|
includedParentCerts[entry.Certificate] = true
|
|
|
|
// First add nodes from this clique, then all cycles, and then
|
|
// all other cliques.
|
|
addNodeCertsToEntry(issuerIdEntryMap, issuerIdChildrenMap, includedParentCerts, entry, clique)
|
|
addNodeCertsToEntry(issuerIdEntryMap, issuerIdChildrenMap, includedParentCerts, entry, cycles...)
|
|
addNodeCertsToEntry(issuerIdEntryMap, issuerIdChildrenMap, includedParentCerts, entry, cliques...)
|
|
addParentChainsToEntry(issuerIdEntryMap, includedParentCerts, entry, parents)
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
// Otherwise, it must be part of a cycle.
|
|
for _, cycle := range cycles {
|
|
inCycle := false
|
|
offsetInCycle := 0
|
|
for index, cycleNode := range cycle {
|
|
if cycleNode == node {
|
|
inCycle = true
|
|
offsetInCycle = index
|
|
break
|
|
}
|
|
}
|
|
|
|
if inCycle {
|
|
foundNode = true
|
|
|
|
// Compute this node's CAChain. Note that order within cycles
|
|
// matters, but we'll preserve the relative order.
|
|
entry := issuerIdEntryMap[node]
|
|
entry.CAChain = []string{entry.Certificate}
|
|
|
|
includedParentCerts := make(map[string]bool, len(closure)+1)
|
|
includedParentCerts[entry.Certificate] = true
|
|
|
|
// First add nodes from this cycle, then all cliques, then all
|
|
// other cycles, and finally from parents.
|
|
orderedCycle := append(cycle[offsetInCycle:], cycle[0:offsetInCycle]...)
|
|
addNodeCertsToEntry(issuerIdEntryMap, issuerIdChildrenMap, includedParentCerts, entry, orderedCycle)
|
|
addNodeCertsToEntry(issuerIdEntryMap, issuerIdChildrenMap, includedParentCerts, entry, cliques...)
|
|
addNodeCertsToEntry(issuerIdEntryMap, issuerIdChildrenMap, includedParentCerts, entry, cycles...)
|
|
addParentChainsToEntry(issuerIdEntryMap, includedParentCerts, entry, parents)
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if !foundNode {
|
|
// Unable to find node; return an error. This shouldn't happen
|
|
// generally.
|
|
pretty := prettyIssuer(issuerIdEntryMap, issuer)
|
|
return nil, fmt.Errorf("unable to find node (%v) in closure (%v) but not in cycles (%v) or cliques (%v)", pretty, closure, cycles, cliques)
|
|
}
|
|
}
|
|
}
|
|
|
|
// We might also have cycles without having associated cliques. We assume
|
|
// that any cliques (if they existed and were relevant for the remaining
|
|
// cycles) were processed at this point. However, we might still have
|
|
// unprocessed cliques (and related cycles) at this point _if_ an
|
|
// unrelated cycle is the parent to that clique+cycle group.
|
|
for _, issuer := range issuers {
|
|
// Skip this node if it is already processed.
|
|
if processed, ok := processedIssuers[issuer]; ok && processed {
|
|
continue
|
|
}
|
|
|
|
// Cliques should've been processed by now, if they were necessary
|
|
// for processable cycles, so ignore them from here to avoid
|
|
// bloating our search paths.
|
|
cycles, err := findAllCyclesWithNode(processedIssuers, issuerIdChildrenMap, issuer, allCliqueNodes)
|
|
if err != nil {
|
|
// To large of cycle.
|
|
return nil, err
|
|
}
|
|
|
|
closure := make(map[issuerID]bool)
|
|
for _, cycle := range cycles {
|
|
for _, node := range cycle {
|
|
closure[node] = true
|
|
}
|
|
}
|
|
|
|
// Before we begin, we need to compute the _parents_ of the nodes in
|
|
// these cycles and ensure they've all been processed (if they're not
|
|
// part of the closure).
|
|
parents, ok := computeParentsFromClosure(processedIssuers, issuerIdParentsMap, closure)
|
|
if !ok {
|
|
// At least one parent wasn't processed; skip this cycle
|
|
// group for now until they have all been processed.
|
|
continue
|
|
}
|
|
|
|
// Finally, for all detected cycles, build the CAChain for nodes in
|
|
// cycles. Since they all share a common parent, they must all contain
|
|
// each other.
|
|
for _, cycle := range cycles {
|
|
// For each node in each cycle
|
|
for nodeIndex, node := range cycle {
|
|
// If the node is processed already, skip it.
|
|
if processed, ok := processedIssuers[node]; ok && processed {
|
|
continue
|
|
}
|
|
|
|
// Otherwise, build its CAChain.
|
|
entry := issuerIdEntryMap[node]
|
|
entry.CAChain = []string{entry.Certificate}
|
|
|
|
// No indication as to size of chain here
|
|
includedParentCerts := make(map[string]bool)
|
|
includedParentCerts[entry.Certificate] = true
|
|
|
|
// First add nodes from this cycle, then all other cycles, and
|
|
// finally from parents.
|
|
orderedCycle := append(cycle[nodeIndex:], cycle[0:nodeIndex]...)
|
|
addNodeCertsToEntry(issuerIdEntryMap, issuerIdChildrenMap, includedParentCerts, entry, orderedCycle)
|
|
addNodeCertsToEntry(issuerIdEntryMap, issuerIdChildrenMap, includedParentCerts, entry, cycles...)
|
|
addParentChainsToEntry(issuerIdEntryMap, includedParentCerts, entry, parents)
|
|
|
|
// Finally, mark the node as processed and add the remaining
|
|
// children to toVisit.
|
|
processedIssuers[node] = true
|
|
childrenCerts, ok := issuerIdChildrenMap[node]
|
|
if ok && len(childrenCerts) > 0 {
|
|
toVisit = append(toVisit, childrenCerts...)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return toVisit, nil
|
|
}
|
|
|
|
func findAllCliques(
|
|
processedIssuers map[issuerID]bool,
|
|
issuerIdCertMap map[issuerID]*x509.Certificate,
|
|
subjectIssuerIdsMap map[string][]issuerID,
|
|
issuers []issuerID,
|
|
) ([][]issuerID, map[issuerID]int, []issuerID, error) {
|
|
var allCliques [][]issuerID
|
|
issuerIdCliqueMap := make(map[issuerID]int)
|
|
var allCliqueNodes []issuerID
|
|
|
|
for _, node := range issuers {
|
|
// Check if the node has already been visited...
|
|
if processed, ok := processedIssuers[node]; ok && processed {
|
|
// ...if so it might have had a manually constructed chain; skip
|
|
// it for clique detection.
|
|
continue
|
|
}
|
|
if _, ok := issuerIdCliqueMap[node]; ok {
|
|
// ...if so it must be on another clique; skip the clique finding
|
|
// so we don't get duplicated cliques.
|
|
continue
|
|
}
|
|
|
|
// See if this is a node on a clique and find that clique.
|
|
cliqueNodes, err := isOnReissuedClique(processedIssuers, issuerIdCertMap, subjectIssuerIdsMap, node)
|
|
if err != nil {
|
|
// Clique is too large.
|
|
return nil, nil, nil, err
|
|
}
|
|
|
|
// Skip nodes which really aren't a clique.
|
|
if len(cliqueNodes) <= 1 {
|
|
continue
|
|
}
|
|
|
|
// Add this clique and update the mapping. A given node can only be in one
|
|
// clique.
|
|
cliqueId := len(allCliques)
|
|
allCliques = append(allCliques, cliqueNodes)
|
|
allCliqueNodes = append(allCliqueNodes, cliqueNodes...)
|
|
for _, cliqueNode := range cliqueNodes {
|
|
issuerIdCliqueMap[cliqueNode] = cliqueId
|
|
}
|
|
}
|
|
|
|
return allCliques, issuerIdCliqueMap, allCliqueNodes, nil
|
|
}
|
|
|
|
func isOnReissuedClique(
|
|
processedIssuers map[issuerID]bool,
|
|
issuerIdCertMap map[issuerID]*x509.Certificate,
|
|
subjectIssuerIdsMap map[string][]issuerID,
|
|
node issuerID,
|
|
) ([]issuerID, error) {
|
|
// Finding max cliques in arbitrary graphs is a nearly pathological
|
|
// problem, usually left to the realm of SAT solvers and NP-Complete
|
|
// theoretical.
|
|
//
|
|
// We're not dealing with arbitrary graphs though. We're dealing with
|
|
// a highly regular, highly structured constructed graph.
|
|
//
|
|
// Reissued cliques form in certificate chains when two conditions hold:
|
|
//
|
|
// 1. The Subject of the certificate matches the Issuer.
|
|
// 2. The underlying public key is the same, resulting in the signature
|
|
// validating for any pair of certs.
|
|
//
|
|
// This follows from the definition of a reissued certificate (same key
|
|
// material, subject, and issuer but with a different serial number and
|
|
// a different validity period). The structure means that the graph is
|
|
// highly regular: given a partial or self-clique, if any candidate node
|
|
// can satisfy this relation with any node of the existing clique, it must
|
|
// mean it must form a larger clique and satisfy this relationship with
|
|
// all other nodes in the existing clique.
|
|
//
|
|
// (Aside: this is not the only type of clique, but it is the only type
|
|
// of 3+ node clique. A 2-star is emitted from certain graphs, but we
|
|
// chose to handle that case in the cycle detection code rather than
|
|
// under this reissued clique detection code).
|
|
//
|
|
// What does this mean for our algorithm? A simple greedy search is
|
|
// sufficient. If we index our certificates by subject -> issuerID
|
|
// (and cache its value across calls, which we've already done for
|
|
// building the parent/child relationship), we can find all other issuers
|
|
// with the same public key and subject as the existing node fairly
|
|
// easily.
|
|
//
|
|
// However, we should also set some reasonable bounds on clique size.
|
|
// Let's limit it to 6 nodes.
|
|
maxCliqueSize := 6
|
|
|
|
// Per assumptions of how we've built the graph, these map lookups should
|
|
// both exist.
|
|
cert := issuerIdCertMap[node]
|
|
subject := string(cert.RawSubject)
|
|
issuer := string(cert.RawIssuer)
|
|
candidates := subjectIssuerIdsMap[subject]
|
|
|
|
// If the given node doesn't have the same subject and issuer, it isn't
|
|
// a valid clique node.
|
|
if subject != issuer {
|
|
return nil, nil
|
|
}
|
|
|
|
// We have two choices here for validating that the two keys are the same:
|
|
// perform a cheap ASN.1 encoding comparison of the public keys, which
|
|
// _should_ be the same but may not be, or perform a more costly (but
|
|
// which should definitely be correct) signature verification. We prefer
|
|
// cheap and call it good enough.
|
|
spki := cert.RawSubjectPublicKeyInfo
|
|
|
|
// We know candidates has everything satisfying _half_ of the first
|
|
// condition (the subject half), so validate they match the other half
|
|
// (the issuer half) and the second condition. For node (which is
|
|
// included in candidates), the condition should vacuously hold.
|
|
var clique []issuerID
|
|
for _, candidate := range candidates {
|
|
// Skip already processed nodes, even if they could be clique
|
|
// candidates. We'll treat them as any other (already processed)
|
|
// external parent in that scenario.
|
|
if processed, ok := processedIssuers[candidate]; ok && processed {
|
|
continue
|
|
}
|
|
|
|
candidateCert := issuerIdCertMap[candidate]
|
|
hasRightKey := bytes.Equal(candidateCert.RawSubjectPublicKeyInfo, spki)
|
|
hasMatchingIssuer := string(candidateCert.RawIssuer) == issuer
|
|
|
|
if hasRightKey && hasMatchingIssuer {
|
|
clique = append(clique, candidate)
|
|
}
|
|
}
|
|
|
|
// Clique is invalid if it contains zero or one nodes.
|
|
if len(clique) <= 1 {
|
|
return nil, nil
|
|
}
|
|
|
|
// Validate it is within the acceptable clique size.
|
|
if len(clique) > maxCliqueSize {
|
|
return clique, fmt.Errorf("error building issuer chains: excessively reissued certificate: %v entries", len(clique))
|
|
}
|
|
|
|
// Must be a valid clique.
|
|
return clique, nil
|
|
}
|
|
|
|
func containsIssuer(collection []issuerID, target issuerID) bool {
|
|
if len(collection) == 0 {
|
|
return false
|
|
}
|
|
|
|
for _, needle := range collection {
|
|
if needle == target {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func appendCycleIfNotExisting(knownCycles [][]issuerID, candidate []issuerID) [][]issuerID {
|
|
// There's two ways to do cycle detection: canonicalize the cycles,
|
|
// rewriting them to have the least (or max) element first or just
|
|
// brute force the detection.
|
|
//
|
|
// Canonicalizing them is faster and easier to write (just compare
|
|
// canonical forms) so do that instead.
|
|
canonicalized := canonicalizeCycle(candidate)
|
|
|
|
found := false
|
|
for _, existing := range knownCycles {
|
|
if len(existing) != len(canonicalized) {
|
|
continue
|
|
}
|
|
|
|
equivalent := true
|
|
for index, node := range canonicalized {
|
|
if node != existing[index] {
|
|
equivalent = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if equivalent {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return append(knownCycles, canonicalized)
|
|
}
|
|
|
|
return knownCycles
|
|
}
|
|
|
|
func canonicalizeCycle(cycle []issuerID) []issuerID {
|
|
// Find the minimum value and put it at the head, keeping the relative
|
|
// ordering the same.
|
|
minIndex := 0
|
|
for index, entry := range cycle {
|
|
if entry < cycle[minIndex] {
|
|
minIndex = index
|
|
}
|
|
}
|
|
|
|
ret := append(cycle[minIndex:], cycle[0:minIndex]...)
|
|
if len(ret) != len(cycle) {
|
|
panic("ABORT")
|
|
}
|
|
|
|
return ret
|
|
}
|
|
|
|
func findCyclesNearClique(
|
|
processedIssuers map[issuerID]bool,
|
|
issuerIdChildrenMap map[issuerID][]issuerID,
|
|
cliqueNodes []issuerID,
|
|
) ([][]issuerID, error) {
|
|
// When we have a reissued clique, we need to find all cycles next to it.
|
|
// Presumably, because they all have non-empty parents, they should not
|
|
// have been visited yet. We further know that (because we're exploring
|
|
// the children path), any processed check would be unnecessary as all
|
|
// children shouldn't have been processed yet (since their parents aren't
|
|
// either).
|
|
//
|
|
// So, we can explore each of the children of any one clique node and
|
|
// find all cycles using that node, until we come back to the starting
|
|
// node, excluding the clique and other cycles.
|
|
cliqueNode := cliqueNodes[0]
|
|
|
|
// Copy the clique nodes as excluded nodes; we'll avoid exploring cycles
|
|
// which have parents that have been already explored.
|
|
excludeNodes := cliqueNodes[:]
|
|
var knownCycles [][]issuerID
|
|
|
|
// We know the node has at least one child, since the clique is non-empty.
|
|
for _, child := range issuerIdChildrenMap[cliqueNode] {
|
|
// Skip children that are part of the clique.
|
|
if containsIssuer(excludeNodes, child) {
|
|
continue
|
|
}
|
|
|
|
// Find cycles containing this node.
|
|
newCycles, err := findAllCyclesWithNode(processedIssuers, issuerIdChildrenMap, child, excludeNodes)
|
|
if err != nil {
|
|
// Found too large of a cycle
|
|
return nil, err
|
|
}
|
|
|
|
// Add all cycles into the known cycles list.
|
|
for _, cycle := range newCycles {
|
|
knownCycles = appendCycleIfNotExisting(knownCycles, cycle)
|
|
}
|
|
|
|
// Exclude only the current child. Adding everything in the cycles
|
|
// results might prevent discovery of other valid cycles.
|
|
excludeNodes = append(excludeNodes, child)
|
|
}
|
|
|
|
return knownCycles, nil
|
|
}
|
|
|
|
func findAllCyclesWithNode(
|
|
processedIssuers map[issuerID]bool,
|
|
issuerIdChildrenMap map[issuerID][]issuerID,
|
|
source issuerID,
|
|
exclude []issuerID,
|
|
) ([][]issuerID, error) {
|
|
// We wish to find all cycles involving this particular node and report
|
|
// the corresponding paths. This is a full-graph traversal (excluding
|
|
// certain paths) as we're not just checking if a cycle occurred, but
|
|
// instead returning all of cycles with that node.
|
|
//
|
|
// Set some limit on max cycle size.
|
|
maxCycleSize := 8
|
|
|
|
// Whether we've visited any given node.
|
|
cycleVisited := make(map[issuerID]bool)
|
|
visitCounts := make(map[issuerID]int)
|
|
parentCounts := make(map[issuerID]map[issuerID]bool)
|
|
|
|
// Paths to the specified node. Some of these might be cycles.
|
|
pathsTo := make(map[issuerID][][]issuerID)
|
|
|
|
// Nodes to visit.
|
|
var visitQueue []issuerID
|
|
|
|
// Add the source node to start. In order to set up the paths to a
|
|
// given node, we seed pathsTo with the single path involving just
|
|
// this node
|
|
visitQueue = append(visitQueue, source)
|
|
pathsTo[source] = [][]issuerID{{source}}
|
|
|
|
// Begin building paths.
|
|
//
|
|
// Loop invariant:
|
|
// pathTo[x] contains valid paths to reach this node, from source.
|
|
for len(visitQueue) > 0 {
|
|
var current issuerID
|
|
current, visitQueue = visitQueue[0], visitQueue[1:]
|
|
|
|
// If we've already processed this node, we have a cycle. Skip this
|
|
// node for now; we'll build cycles later.
|
|
if processed, ok := cycleVisited[current]; ok && processed {
|
|
continue
|
|
}
|
|
|
|
// Mark this node as visited for next time.
|
|
cycleVisited[current] = true
|
|
if _, ok := visitCounts[current]; !ok {
|
|
visitCounts[current] = 0
|
|
}
|
|
visitCounts[current] += 1
|
|
|
|
// For every child of this node...
|
|
children, ok := issuerIdChildrenMap[current]
|
|
if !ok {
|
|
// Node has no children, nothing else we can do.
|
|
continue
|
|
}
|
|
|
|
for _, child := range children {
|
|
// Ensure we can visit this child; exclude processedIssuers and
|
|
// exclude lists.
|
|
if childProcessed, ok := processedIssuers[child]; ok && childProcessed {
|
|
continue
|
|
}
|
|
|
|
skipNode := false
|
|
for _, excluded := range exclude {
|
|
if excluded == child {
|
|
skipNode = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if skipNode {
|
|
continue
|
|
}
|
|
|
|
// Track this parent->child relationship to know when to exit.
|
|
setOfParents, ok := parentCounts[child]
|
|
if !ok {
|
|
setOfParents = make(map[issuerID]bool)
|
|
parentCounts[child] = setOfParents
|
|
}
|
|
_, existingParent := setOfParents[current]
|
|
setOfParents[current] = true
|
|
|
|
// Since we know that we can visit this node, we should now build
|
|
// all destination paths using this node, from our current node.
|
|
//
|
|
// Since these are all starting at a single path from source,
|
|
// if we have any cycles back to source, we'll find them here.
|
|
//
|
|
// Only add this if it is a net-new path that doesn't repeat
|
|
// (either internally -- indicating an internal cycle -- or
|
|
// externally with an existing path).
|
|
addedPath := false
|
|
if _, ok := pathsTo[child]; !ok {
|
|
pathsTo[child] = make([][]issuerID, 0)
|
|
}
|
|
for _, path := range pathsTo[current] {
|
|
if child != source {
|
|
// We only care about source->source cycles. If this
|
|
// cycles, but isn't a source->source cycle, don't add
|
|
// this path.
|
|
foundSelf := false
|
|
for _, node := range path {
|
|
if child == node {
|
|
foundSelf = true
|
|
break
|
|
}
|
|
}
|
|
if foundSelf {
|
|
// Skip this path.
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Make sure to deep copy the path.
|
|
newPath := make([]issuerID, 0, len(path)+1)
|
|
newPath = append(newPath, path...)
|
|
newPath = append(newPath, child)
|
|
|
|
isSamePath := false
|
|
for _, childPath := range pathsTo[child] {
|
|
if len(childPath) != len(newPath) {
|
|
continue
|
|
}
|
|
|
|
isSamePath = true
|
|
for index, node := range childPath {
|
|
if newPath[index] != node {
|
|
isSamePath = false
|
|
break
|
|
}
|
|
}
|
|
|
|
if isSamePath {
|
|
break
|
|
}
|
|
}
|
|
|
|
if !isSamePath {
|
|
pathsTo[child] = append(pathsTo[child], newPath)
|
|
addedPath = true
|
|
}
|
|
}
|
|
|
|
// Visit this child next.
|
|
visitQueue = append(visitQueue, child)
|
|
|
|
// If there's a new parent or we found a new path, then we should
|
|
// revisit this child, to update _its_ children and see if there's
|
|
// another new path. Eventually the paths will stabilize and we'll
|
|
// end up with no new parents or paths.
|
|
if !existingParent || addedPath {
|
|
cycleVisited[child] = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Ok, we've now exited from our loop. Any cycles would've been detected
|
|
// and their paths recorded in pathsTo. Now we can iterate over these
|
|
// (starting a source), clean them up and validate them.
|
|
var cycles [][]issuerID
|
|
for _, cycle := range pathsTo[source] {
|
|
// Skip the trivial cycle.
|
|
if len(cycle) == 1 && cycle[0] == source {
|
|
continue
|
|
}
|
|
|
|
// Validate cycle starts and ends with source.
|
|
if cycle[0] != source {
|
|
return nil, fmt.Errorf("cycle (%v) unexpectedly starts with node %v; expected to start with %v", cycle, cycle[0], source)
|
|
}
|
|
|
|
// If the cycle doesn't start/end with the source,
|
|
// skip it.
|
|
if cycle[len(cycle)-1] != source {
|
|
continue
|
|
}
|
|
|
|
truncatedCycle := cycle[0 : len(cycle)-1]
|
|
if len(truncatedCycle) >= maxCycleSize {
|
|
return nil, fmt.Errorf("cycle (%v) exceeds max size: %v > %v", cycle, len(cycle), maxCycleSize)
|
|
}
|
|
|
|
// Now one last thing: our cycle was built via parent->child
|
|
// traversal, but we want child->parent ordered cycles. So,
|
|
// just reverse it.
|
|
reversed := reversedCycle(truncatedCycle)
|
|
cycles = appendCycleIfNotExisting(cycles, reversed)
|
|
}
|
|
|
|
return cycles, nil
|
|
}
|
|
|
|
func reversedCycle(cycle []issuerID) []issuerID {
|
|
var result []issuerID
|
|
for index := len(cycle) - 1; index >= 0; index-- {
|
|
result = append(result, cycle[index])
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func computeParentsFromClosure(
|
|
processedIssuers map[issuerID]bool,
|
|
issuerIdParentsMap map[issuerID][]issuerID,
|
|
closure map[issuerID]bool,
|
|
) (map[issuerID]bool, bool) {
|
|
parents := make(map[issuerID]bool)
|
|
for node := range closure {
|
|
nodeParents, ok := issuerIdParentsMap[node]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
for _, parent := range nodeParents {
|
|
if nodeInClosure, ok := closure[parent]; ok && nodeInClosure {
|
|
continue
|
|
}
|
|
|
|
parents[parent] = true
|
|
if processed, ok := processedIssuers[parent]; ok && processed {
|
|
continue
|
|
}
|
|
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
return parents, true
|
|
}
|
|
|
|
func addNodeCertsToEntry(
|
|
issuerIdEntryMap map[issuerID]*issuerEntry,
|
|
issuerIdChildrenMap map[issuerID][]issuerID,
|
|
includedParentCerts map[string]bool,
|
|
entry *issuerEntry,
|
|
issuersCollection ...[]issuerID,
|
|
) {
|
|
for _, collection := range issuersCollection {
|
|
// Find a starting point into this collection such that it verifies
|
|
// something in the existing collection.
|
|
offset := 0
|
|
for index, issuer := range collection {
|
|
children, ok := issuerIdChildrenMap[issuer]
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
foundChild := false
|
|
for _, child := range children {
|
|
childEntry := issuerIdEntryMap[child]
|
|
if inChain, ok := includedParentCerts[childEntry.Certificate]; ok && inChain {
|
|
foundChild = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if foundChild {
|
|
offset = index
|
|
break
|
|
}
|
|
}
|
|
|
|
// Assumption: collection is in child -> parent order. For cliques,
|
|
// this is trivially true because everyone can validate each other,
|
|
// but for cycles we have to ensure that in findAllCyclesWithNode.
|
|
// This allows us to build the chain in the correct order.
|
|
for _, issuer := range append(collection[offset:], collection[0:offset]...) {
|
|
nodeEntry := issuerIdEntryMap[issuer]
|
|
addToChainIfNotExisting(includedParentCerts, entry, nodeEntry.Certificate)
|
|
}
|
|
}
|
|
}
|
|
|
|
func addParentChainsToEntry(
|
|
issuerIdEntryMap map[issuerID]*issuerEntry,
|
|
includedParentCerts map[string]bool,
|
|
entry *issuerEntry,
|
|
parents map[issuerID]bool,
|
|
) {
|
|
for parent := range parents {
|
|
nodeEntry := issuerIdEntryMap[parent]
|
|
for _, cert := range nodeEntry.CAChain {
|
|
addToChainIfNotExisting(includedParentCerts, entry, cert)
|
|
}
|
|
}
|
|
}
|