fix mTLS: trusted_leaf_certs requires trusted_ca_certs for chain validation

Caddy's trusted_leaf_certs is an additional check on top of CA chain
validation, not a replacement. Without trusted_ca_certs, Go's TLS
rejects the client cert before the leaf check runs, causing 'unknown ca'.

Updated buildClientAuthentication to always include the CA cert in
trusted_ca_certs for chain validation, and additionally set
trusted_leaf_certs for managed CAs to enforce revocation. When all
issued certs for a CA are revoked, the CA is excluded from
trusted_ca_certs entirely so chain validation fails for any cert from it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
fuomag9
2026-03-06 21:18:33 +01:00
parent 9fa57bcf28
commit 90fa11ae3c

View File

@@ -1333,33 +1333,44 @@ function buildClientAuthentication(
}
if (caCertIds.size === 0) return null;
// For CAs that have never issued tracked certs: trust any cert signed by that CA.
// For CAs that have issued tracked certs: pin to only the active (non-revoked) leaf
// certs so revocation is enforced. trusted_leaf_certs bypasses chain validation and
// accepts only those exact certificates — do NOT also put the CA in trusted_ca_certs
// or that would allow any cert signed by it (defeating revocation).
// trusted_leaf_certs is an ADDITIONAL check on top of CA chain validation
// (Caddy rejects without trusted_ca_certs even when trusted_leaf_certs is set).
//
// Strategy:
// - Unmanaged CA (no tracked issued certs): trust any cert signed by that CA
// → CA cert in trusted_ca_certs only.
// - Managed CA with active certs: trust only those specific leaf certs
// → CA cert in trusted_ca_certs (for chain validation) +
// active leaf certs in trusted_leaf_certs (revocation enforcement).
// - Managed CA with ALL certs revoked: exclude it from trusted_ca_certs so
// chain validation fails — no cert from it will be accepted.
const trustedCaCerts: string[] = [];
const trustedLeafCerts: string[] = [];
for (const id of caCertIds) {
const ca = caCertMap.get(id);
if (!ca) continue;
if (cAsWithAnyIssuedCerts.has(id)) {
// Managed CA: only trust active leaf certs (revoked ones are absent from the map)
// Managed CA: enforce revocation via leaf pinning
const activeLeafCerts = issuedClientCertMap.get(id) ?? [];
if (activeLeafCerts.length === 0) {
// All certs revoked: exclude CA so chain validation fails for its certs
continue;
}
trustedCaCerts.push(pemToBase64Der(ca.certificatePem));
for (const certPem of activeLeafCerts) {
trustedLeafCerts.push(pemToBase64Der(certPem));
}
} else {
// Unmanaged CA: trust any cert in the chain
const ca = caCertMap.get(id);
if (!ca) continue;
trustedCaCerts.push(pemToBase64Der(ca.certificatePem));
}
}
if (trustedCaCerts.length === 0 && trustedLeafCerts.length === 0) return null;
if (trustedCaCerts.length === 0) return null;
const result: Record<string, unknown> = { mode: "require_and_verify" };
if (trustedCaCerts.length > 0) result.trusted_ca_certs = trustedCaCerts;
const result: Record<string, unknown> = { mode: "require_and_verify", trusted_ca_certs: trustedCaCerts };
if (trustedLeafCerts.length > 0) result.trusted_leaf_certs = trustedLeafCerts;
return result;
}