A passed challenge keeps the original finding

Nine signed-record cases show which address a challenge clearance binds to and which original facts survive escalation.

6 min read
RSS

A visitor can pass a Heretic challenge and still have verdict: "contradicted" in the resulting clearance. The original probe and the later challenge describe different observations. An application can admit the visitor after escalation without changing what the probe found.

We tested that distinction through the token endpoint handler and the reference Cloudflare Worker gate. Nine synthetic cases covered phone-only challenges, desktop hand-offs, changed addresses, denied outcomes and missing records. Each produced a valid signed clearance. Only three reached the protected origin.

Two addresses in one signed record

Our original probe used 192.0.2.1. The phone used 198.51.100.2; a desktop hand-off selected 203.0.113.4. These are documentation addresses in a local fixture.

For the phone-only pass, the relevant fields of the verified payload were:

json
{
  "verdict": "contradicted",
  "concealed": true,
  "challenged": true,
  "outcome": "passed",
  "probe": { "client_ip": "192.0.2.1" },
  "challenge": {
    "device": { "client_ip": "198.51.100.2" }
  }
}

This excerpt omits the other payload fields. The complete record also identifies the original probe and challenge. The signature covers both observations. Passing the challenge does not replace the original probe address or change its verdict.

In a desktop hand-off, challenge.device identifies the selected desktop. In phone-only mode it identifies the ceremony device. A gate that always compares against the original probe would reject a visitor who completed the challenge on the new path. A gate that always compares against the phone would select the wrong address for the desktop hand-off.

The reference gate makes the selection explicit. This function runs after signature and token validation:

js
export function allowed(claims, request) {
  const h = claims.heretic;
  const device = h.challenged ? h.challenge?.device : h.probe;
  const ip = request.headers.get('cf-connecting-ip');
  if (!ip || !device?.client_ip || device.client_ip !== ip) return false;
  return h.challenged ? h.outcome === 'passed' : h.verdict === 'uncontradicted';
}

After escalation, this policy requires a matching selected-device address and a passed outcome. It leaves the original verdict available to a tenant that wants additional restrictions. Address equality here is a request-binding check; it does not establish a unique person or device.

Following each clearance to the origin

We supplied synthetic terminal challenge records to the real token endpoint handler through its store injection point. The handler generated the clearance with an ephemeral signing key. The packaged verifier checked it, then a local workerd instance received a protected GET request carrying the clearance cookie.

Every outbound fetch was intercepted. Key lookup returned the fixture's public key. A fetch to the protected origin incremented a counter and returned a fixed response. No customer service, production database or physical phone participated.

Terminal record and request address Gate response Origin calls
Passed phone, selected phone address 200 1
Passed phone, original probe address 403 0
Passed phone, another address 403 0
Passed desktop hand-off, selected desktop address 200 1
Passed desktop hand-off, phone address 403 0
Contradicted phone ceremony, matching address 403 0
Not-completed phone ceremony, matching address 403 0
Passed ceremony, missing selected device 403 0
Passed phone, missing original probe snapshot 200 1

The token endpoint returned HTTP 200 for all nine terminal records. That means it returned a record, not that the customer gate admitted the request. All nine clearance signatures verified. The separate short-lived proof appeared only for the passed outcomes.

Every case retained the original contradicted verdict and concealed: true. Eight retained the supplied probe snapshot unchanged. The ninth had no original snapshot to retain.

The missing-snapshot case

The last row is easy to miss when implementing a challenge callback. Its clearance had probe: null, concealed: true and outcome: "passed". The selected phone address matched the request. The reference policy admitted it.

That follows directly from the branch above: once challenged is true, admission checks the selected device and outcome. It does not require an original probe snapshot. The signed record exposes the absence, so a tenant can require a snapshot if its policy needs one.

The neighboring case failed. A passed outcome with no selected device returned 403 and made no origin call. The address comparison could not be satisfied.

These cases help define an application's escalation policy. Requiring evidence of a later completed challenge and requiring a retained original probe are separate conditions. Neither should be inferred from the presence of a signature.

What the fixture assumes

The fixture starts with terminal records. It tests how the endpoint signs them and how the gate consumes them. It cannot establish whether a person or an automated client can earn a passed challenge, how much doing so costs, or which authenticator produced a credential. Those questions require a different experiment beginning before challenge completion.

The test also supplies CF-Connecting-IP as a runtime input. A deployment must establish where that value comes from. Cloudflare documents different behavior for direct traffic and Worker subrequests, including a fixed address for cross-zone subrequests. Copying this gate behind another proxy requires checking that request path; accepting an arbitrary client header would defeat the address comparison. See Cloudflare's header documentation.

For an integration test, keep the original probe address, selected challenge address and live request address distinct. Verify the signed fields first, then count protected-origin calls for each address and outcome. Include a missing original snapshot and a missing selected device. Those two rows exercise different requirements even when both carry a passed outcome.

Reproducing the gate results

The reproduction archive contains the public reference gate, verifier, a signing fixture and the nine recorded claim blocks. With Node 22.22.2 or later, extract it and run:

sh
npm ci --no-audit --no-fund
npm test

The script creates a fresh test key and updates the JWT envelope times so the records can be checked after their original expiry. It preserves each recorded Heretic claim block, submits a protected request to local workerd, and asserts the status and origin-call count from the table. A clean archive extraction reproduced all nine rows.

This download exercises the public gate. The token endpoint results remain recorded evidence from the earlier experiment; private endpoint code is not included. The README distinguishes the two runs, and the manifest lists file hashes for the included sources and observations.

Next article A canvas hash can change without a readback hook An opaque control separates ordinary alpha conversion from a deliberate one-byte canvas readback alteration.

The probe runs on your own browser.

The public demo returns the verdict and its named findings.