From ef0e991437e225d88963759db44fd79f03380102 Mon Sep 17 00:00:00 2001 From: kalsprite Date: Sun, 14 Jun 2026 00:16:41 -0700 Subject: [PATCH] relax roots --- core/crypto/x509/verify.odin | 43 +++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/core/crypto/x509/verify.odin b/core/crypto/x509/verify.odin index 80a413902..ca667d790 100644 --- a/core/crypto/x509/verify.odin +++ b/core/crypto/x509/verify.odin @@ -221,12 +221,12 @@ _MAX_SIG_CHECKS :: 100 // opts.required_eku is set the leaf AND every intermediate must permit // that purpose (e.g. an email-only sub-CA cannot issue a TLS server leaf). // -// The trust anchor is checked like any issuer (validity, CA / -// keyCertSign, pathLenConstraint, no uninterpreted critical extension), -// except that its own self-signature is not re-verified, since it is -// trusted a priori. An expired or malformed root is therefore rejected; -// resilience to that comes from the search trying every other available -// anchor and intermediate. +// The trust anchor is treated as trusted input, as in Go and OpenSSL: it +// must be valid at opts.current_time and is name-chained + signature-checked +// as the issuer below it, but its CA authorization (basicConstraints / +// keyCertSign / pathLenConstraint) and its own self-signature are NOT +// re-checked. An expired anchor is still rejected; resilience to that comes +// from the search trying every other available anchor and intermediate. // @(require_results) verify_chain :: proc( @@ -330,11 +330,13 @@ _build_to_anchor :: proc( // Non-self-issued intermediates already beneath the next issuer, for the pathLenConstraint check (RFC 5280 section 6.1.4) below := _non_self_issued_below(acc) - // Prefer terminating at a trust anchor. The anchor must have issued `cert` (name chaining + signature) and, - // like any CA, be fit to act as an issuer: valid at `now`, a CA with keyCertSign, within its pathLenConstraint, - // no uninterpreted critical extension. Its own self-signature is NOT re-verified; it is trusted a priori. + // Prefer terminating at a trust anchor. The anchor must have issued `cert` + // (name chaining + signature) and be valid at `now`, but as TRUSTED INPUT + // its CA authorization (basicConstraints / keyCertSign / pathLenConstraint) + // and its own self-signature are NOT re-checked, matching how Go and + // OpenSSL treat roots. See _anchor_usable. for root in opts.roots { - if !_is_issuer_of(root, cert) || !_issuer_usable(root, opts.current_time, below) { + if !_is_issuer_of(root, cert) || !_anchor_usable(root, opts.current_time) { continue } if budget^ <= 0 { @@ -449,6 +451,27 @@ _issuer_usable :: proc(issuer: ^Certificate, now: time.Time, below: int) -> bool return true } +// The anchor is trusted input, so unlike _issuer_usable its CA authorization +// (basicConstraints / keyCertSign / pathLenConstraint) and self-signature are +// NOT re-checked, matching Go and OpenSSL's treatment of roots. Still required: +// valid at `now` (Go and OpenSSL both enforce this; resilience to an expired +// anchor comes from the search trying other anchors/intermediates), no +// uninterpreted critical extension, and no name constraints (which we cannot +// enforce, so refuse rather than ignore). +@(private) +_anchor_usable :: proc(anchor: ^Certificate, now: time.Time) -> bool { + if anchor.unhandled_critical { + return false + } + if _has_extension(anchor, _OID_EXT_NAME_CONSTRAINTS) { + return false + } + if _check_validity(anchor, now) != .None { + return false + } + return true +} + // Reports whether `cert` allows the given Extended Key Usage purpose: // no EKU extension means unrestricted, anyExtendedKeyUsage // permits everything, otherwise the purpose must be listed. Applied to