Repository navigation
cherry-pick: Reject mTLS clients whose revocation status cannot be checked under fail-closed, and recover the issuer from Harper's trusted CAs when the socket chain lacks it (conflicts → v5.2) (#2457 → v5.2) - #2827
Conversation
…ail-closed, and recover the issuer from Harper's trusted CAs when the socket chain lacks it (#2457 → v5.2) verifyCertificate() returned valid:true with status no-issuer-cert whenever socket.getPeerCertificate(true) exposed no issuer, regardless of failureMode, so revocation checking silently switched itself off. On v5.2 that branch is now the normal case on Node 24.21.0 (the same peer-chain change as nodejs/node#65579 on 26.8.x) and, on every Node version, on resumed TLS sessions: Node only completes chains from the listener context's store while Harper's client CAs live on the SNI contexts built by createTLSSelector. - keys.ts publishes the live CA set to certificateVerification/trustedIssuers.ts on every TLS rebuild; verifyCertificate() resolves a missing issuer from it (subject match plus signature verification, cached per leaf and per CA generation). - An issuer that cannot be resolved applies failureMode: rejected under fail-closed, allowed under fail-open, logged at warn once per certificate. - Both revocation methods explicitly disabled returns 'disabled' before the chain is examined, so a disabled control never rejects. - Integration tests cover a revoked certificate on a resumed TLS session for OCSP and CRL. Conflict resolution for v5.2: createTLSSelector on v5.2 rebuilds `caCerts` in place (no candidate-map swap), so the publish call sits after the zero-certificate retry guard, where the pass has committed its CA set. server/DESIGN.md keeps v5.2's cheat-sheet rows and adds only #2457's row. (cherry picked from commit 6a42ded) Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Dispatch-Task: harper-backport-2457-v5.2 Claude-Session: https://claude.ai/code/session_012XqVnuTJcws6TJZqmRMcBv
There was a problem hiding this comment.
Code Review
This pull request introduces client certificate issuer resolution from trusted certificate authorities to support mTLS revocation checking (CRL/OCSP) on resumed TLS sessions and Node versions where the socket omits the issuer from the peer chain. It adds a new trustedIssuers module to publish and resolve trusted authorities, updates the certificate verification logic to handle missing issuers according to the configured failure mode, and includes comprehensive integration and unit tests. Feedback on the changes suggests using optional chaining when checking config.crl.enabled and config.ocsp.enabled to prevent potential TypeError runtime crashes if these optional configuration properties are undefined.
Cherry-pick of #2457 onto
v5.2: mTLS revocation checking no longer fails open when the client certificate's issuer is missing from the socket chain.verifyCertificate()recovers the issuer from Harper's own configured certificate authorities and, when it cannot, appliesfailureMode(rejected underfail-closed, allowed underfail-open) with awarnlog instead of a silent allow.Why now: every v5.2 Integration Tests run since Node 24.21.0 became the resolved
node:24is red on the same 4 OCSP/CRL tests on the Node 24 and uWS legs (Node 22 and Bun pass), because 24.21.0 stops exposing the issuer throughsocket.getPeerCertificate(true)the way Node 26.8.x does (nodejs/node#65579). On unmodifiedv5.2that makesverifyCertificate()returnvalid: truefor a revoked client certificate regardless offailureMode. The Docker image builds fromnode:24, so this is a security gap in the next patch release, not only a test failure.For the human reviewer
Framing-Verdict: option-set-too-narrow (0b6afdf5f1d1), asking for a TLS-layer alternative (client CAs on the listener's default trust context, or the handshake-learned issuer retained) and a listener-scoped resolver to be evaluated before accepting the process-wide CA snapshot on a release branch. Overruled on two facts: both alternatives were evaluated and rejected in Reject mTLS clients whose revocation status cannot be checked under fail-closed, and recover the issuer from Harper's trusted CAs when the socket chain lacks it #2457's own decision ledger before it merged (the listener context is fixed at creation andcreateTLSSelectornever callsserver.setSecureContext()while the CA set hot-reloads; a handshake memo needs a cross-thread store because a resumed session lands on any worker), and a release-branch cherry-pick that diverges from main's design forks the line (later main fixes tosecurity/certificateVerification/would conflict on v5.2, and the 5.2→5.3 upgrade would change mTLS admission semantics). The task brief settles it the same way: a backport of a reviewed and merged fix, no new design.keys.tspublish point. Main'screateTLSSelectorbuilds candidate maps and swaps them in one step; Reject mTLS clients whose revocation status cannot be checked under fail-closed, and recover the issuer from Harper's trusted CAs when the socket chain lacks it #2457 published the CA set right after that swap. v5.2 clears and rebuildscaCertsin place, sopublishTrustedAuthorities(caCerts.values())sits after the zero-certificate retry guard, the first point where the pass has committed its CA set, with the sameliveReloadgate as main (a one-shot client selector's pass may read zero authority rows and must not empty the listener's published set). No other main-onlykeys.tschange is pulled in. Look hardest here.fail-closedon a resumed session (Reject mTLS clients whose revocation status cannot be checked under fail-closed, and recover the issuer from Harper's trusted CAs when the socket chain lacks it #2457's ledger item 1: operator fix is adding the intermediate totls.certificateAuthority); (b) harper-pro's replication listener trusts system roots plushdb_nodesCAs that core never publishes, so withreplication.mtls.certificateVerificationenabled a peer certificate from a public CA resolves no issuer on Node 24.21.0 and is refused. Both hold on main today and are recorded as follow-ups in the dispatch findings; fixing them on v5.2 first would fork the line.server/mqtt.ts:139-142on v5.2, same on main). Recorded as a finding, not touched.server/DESIGN.mdkeeps v5.2's cheat-sheet rows and adds only #2457's row.Changes
security/certificateVerification/index.ts: explicitly disabled CRL+OCSP returnsdisabledbefore the chain is examined; a missing issuer is resolved from the published authorities; an unresolvable one goes throughunresolvedIssuerResult, which appliesfailureModeand warns once per certificate.security/certificateVerification/trustedIssuers.ts(new):publishTrustedAuthoritiesparses the CA PEM set (skipping unparseable entries, keeping the cache when the set is byte-identical) andresolveTrustedIssuerfinds the authority that signed a leaf (subject match plus signature check, never self-issued), cached per leaf fingerprint.security/certificateVerification/types.ts: optionalfingerprint256onPeerCertificate, the cache key.security/keys.ts: publishes the live CA set on every listener TLS rebuild (item 2 above).server/DESIGN.md: the cheat-sheet row explaining why revocation checking resolves the issuer itself.Verification
Node version used locally: v24.21.0 (the version today's v5.2 CI resolves), with the worktree's
npm ciagainst v5.2's lockfile anddist/rebuilt.origin/v5.2(0895ce059) built in a side tree,npm run test:integration -- integrationTests/security/ocsp-verification.test.ts integrationTests/security/crl-verification.test.ts: 8 pass / 4 fail, exactly the CI set (should reject revoked certificate with OCSP/CRL check,should reject connection in fail-closed mode when OCSP/CRL times out;Expected 401 for revoked cert, got: 404).npx mocha unitTests/security/certificateVerification/*.test.js: 191 passing (index.test.jsupdated expectations, newtrustedIssuers.test.jsandverifyCertificate.test.js).npm run build,npm run lint:required,prettier --checkon the changed files: clean.6a42ded92on main (git diff 6a42ded92 -- <the other eight files>is empty).Refs #2457
Refs #2380
Complexity: easy
Signed: Claude Fable 5.1
🤖 Generated with Claude Code
https://claude.ai/code/session_012XqVnuTJcws6TJZqmRMcBv
Origin — the dispatch brief this PR was written from
Backport harper#2457 ("Reject mTLS clients whose revocation status cannot be checked under fail-closed, and recover the issuer from Harper's trusted CAs when the socket chain lacks it") onto the v5.2 release branch as a patch PR with base v5.2. This blocks the v5.2.14 patch release cut. Use the patch-pr skill conventions: branch from origin/v5.2 named cherry-pick/v5.2/pr-2457, cherry-pick the squash-merge commit 6a42ded from main (PR commits were c5443f8 + b1490fa), resolve the conflicts, open a draft PR against v5.2 titled like the other v5.2 cherry-picks (e.g. "cherry-pick: Reject mTLS clients ... (conflicts → v5.2) (#2457 → v5.2)"), and set milestone v5.2.
Acceptance
Dispatch: task
harper-backport-2457-v5.2· queued by kriszyp-session · ran by claude/fable/high · worker kzyp-xps-1Review-Coverage: authored=claude; ran=cursor-composer,gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-kimi,cursor-muse; rounds=1; full=1 @ a184743
Human-Review-Need: 4 (decisions: process-wide-issuer-set, directly-trusted-self-signed-client) @ a184743