Cross-signing and bridge-CA support¶
Cross-signing lets an intermediate (or root) public key be certified by more than one issuer, so the same subordinate key holds several valid certificates and a relying party can be served whichever chain terminates at a trust anchor it already holds. This complements the dual-chain rotation overlap work: rotation issues a second certificate for a new key under the same parent, whereas cross-signing issues a second certificate for the same key under a different issuer.
Two topologies motivate it:
- Bridge CA — two enterprise PKIs cross-certify each other's roots so leaves in one domain validate under the other's trust anchor.
- Root transition — a new root is cross-signed by the old root, so relying parties that still trust only the old root keep building a chain to the new one until they have distributed the new anchor.
The model¶
A cross-signed certificate is a CA certificate that carries the subject's exact distinguished name and public key (so it is a byte-compatible drop-in issuer) but is signed by a different issuer's HSM-backed key. Because the DN and Subject Public Key — and therefore the Subject Key Identifier — are preserved, any leaf already signed by that subordinate key chains equally through either certificate:
root1 ──issues──▶ intermediate ──▶ leaf (native chain)
root2 ──cross-signs──▶ intermediate (alternate chain, same key + DN)
The subordinate's private key is never involved — only its public half is re-certified. The subject may be supplied three ways:
| Source | Meaning |
|---|---|
local-ca |
A CA already in this deployment (its HSM key is untouched) |
certificate |
An externally supplied CA certificate (bridge import) |
csr |
An externally supplied CA CSR (self-signature verified) |
Cross-sign relationships are persisted in the store and tenant-scoped through their issuer CA; cross-signing a subject CA from another tenant is refused. Every certificate for one subject key shares a Subject Key Identifier, which is the join key for alternate-chain selection.
CLI (secsy-ca)¶
| Command | Purpose |
|---|---|
cross-sign |
Cross-sign a subject key under an issuer CA's HSM key |
list-cross-signs |
List a CA's cross-sign relationships, or its publishable alt-chains |
Cross-sign a local intermediate under a second root:
Cross-sign an external root (bridge import) or a CSR:
secsy-ca cross-sign -issuer our-root -cert partner-root.pem -validity-days 730
secsy-ca cross-sign -issuer our-root -csr partner.csr -validity-days 730
Inspect the alternate chains available for a subject CA:
-path-len defaults to preserving the subject's constraint (-2); use -1 for
unconstrained or a non-negative value to override. The cross-signed lifetime
reuses the subject's span when -validity-days is omitted (required for a CSR)
and is always clamped to the issuer's own expiry.
HTTP API¶
Creating and listing cross-signs are management operations (ca:manage); the
alternate chains they publish are public, like the CRL/OCSP/chain endpoints.
POST /api/ca/{id}/cross-signs → CrossSignResult (ca:manage)
GET /api/ca/{id}/cross-signs → { cross_signs } (ca:manage)
GET /api/ca/{id}/chains → { chains } (public)
GET /api/ca/{id}/cross-signs/{csid}/chain → application/x-pem-file (public)
{id} on POST /cross-signs is the issuer CA whose key signs. GET /chains
returns every publishable trust path for a subject CA — its native
parent-lineage chain plus one chain per active cross-sign — so an operator (or an
automated relying party) can select whichever chain it needs.
Tests¶
internal/ca/crosssign_openssl_test.go is the acceptance test: it cross-signs one
intermediate under two independent roots, issues a leaf, and verifies both
resulting chains build and validate with Go's x509 verifier and with
openssl verify. It runs against the software provider and SoftHSM (skipped when
SoftHSM is not configured). internal/ca/crosssign_test.go covers the external
certificate, CSR, and root-transition paths plus argument validation.