Skip to content

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

Merged
kriszyp merged 1 commit into
v5.2from
cherry-pick/v5.2/pr-2457
Sep 25, 2026

Conversation

@kriszyp

@kriszyp kriszyp commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

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, applies failureMode (rejected under fail-closed, allowed under fail-open) with a warn log instead of a silent allow.

Why now: every v5.2 Integration Tests run since Node 24.21.0 became the resolved node:24 is 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 through socket.getPeerCertificate(true) the way Node 26.8.x does (nodejs/node#65579). On unmodified v5.2 that makes verifyCertificate() return valid: true for a revoked client certificate regardless of failureMode. The Docker image builds from node:24, so this is a security gap in the next patch release, not only a test failure.

For the human reviewer

  1. Planning-gate framing overruled. The planning round returned 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 and createTLSSelector never calls server.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 to security/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.
  2. The only v5.2-specific lines are the keys.ts publish point. Main's createTLSSelector builds 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 rebuilds caCerts in place, so publishTrustedAuthorities(caCerts.values()) sits after the zero-certificate retry guard, the first point where the pass has committed its CA set, with the same liveReload gate 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-only keys.ts change is pulled in. Look hardest here.
  3. Design concerns the review raised are 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, byte-identical to main, and intentionally not changed here. The independent round (Codex, Gemini, Cursor Composer, Harper-domain adjudication) found nothing in the conflict resolution. Its two majors are on 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 design: (a) a client leaf issued by an intermediate that Harper holds only the root for is refused under fail-closed on 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 to tls.certificateAuthority); (b) harper-pro's replication listener trusts system roots plus hdb_nodes CAs that core never publishes, so with replication.mtls.certificateVerification enabled 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.
  4. Pre-existing, unrelated to 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: raw-TCP MQTT catches the verification error and still admits the socket without a user (server/mqtt.ts:139-142 on v5.2, same on main). Recorded as a finding, not touched.
  5. server/DESIGN.md keeps v5.2's cheat-sheet rows and adds only #2457's row.

Changes

Verification

Node version used locally: v24.21.0 (the version today's v5.2 CI resolves), with the worktree's npm ci against v5.2's lockfile and dist/ rebuilt.

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-1

Review-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

…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
@kriszyp kriszyp added this to the v5.2 milestone Sep 25, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread security/certificateVerification/index.ts
@kriszyp
kriszyp marked this pull request as ready for review September 25, 2026 20:03
@kriszyp
kriszyp merged commit 47c6029 into v5.2 Sep 25, 2026
50 of 53 checks passed
@kriszyp
kriszyp deleted the cherry-pick/v5.2/pr-2457 branch September 25, 2026 20:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant