@openwop/openwop-conformance 2.40.0 → 2.40.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -23,9 +23,14 @@
23
23
  * test.
24
24
  *
25
25
  * **Gate:** an advertised `auth.lanes[]` member with `revocation: "exp-only"`
26
- * (`inapplicable`, naming that, otherwise) AND `OPENWOP_TEST_OIDC_ISSUER_URL` naming a
27
- * synthetic issuer the operator has configured that lane to trust (`blocked` otherwise —
28
- * without a token the host would ever accept, "it refused a string" witnesses nothing).
26
+ * (`inapplicable`, naming that, otherwise) whose `issuers[]` lists
27
+ * `OPENWOP_TEST_OIDC_ISSUER_URL`, the synthetic issuer the suite holds the key for. A lane
28
+ * that does not list it records `inapplicable` naming the issuers it does trust: the
29
+ * harness is a suite instrument the host claims by advertising it (RFC 0168 §C.1's reading
30
+ * for the seams profile; lib/harness-issuer.ts), and a production host must never trust a
31
+ * test issuer. Corrected in 2.40.1; this was `blocked`, which denied certification to
32
+ * every honest production bundle. A claimed harness that cannot be served is `blocked` —
33
+ * without a token the host would ever accept, "it refused a string" witnesses nothing.
29
34
  * A window under 120 s is `blocked`: the control token needs room inside it.
30
35
  *
31
36
  * **Each refusal leg is skewed so it isolates ONE bound.** A token minted at `now` with
@@ -54,13 +59,14 @@
54
59
 
55
60
  import { afterAll, describe, it, expect } from 'vitest';
56
61
  import { randomBytes } from 'node:crypto';
57
- import { createServer, type Server } from 'node:http';
62
+ import type { Server } from 'node:http';
58
63
  import { driver } from '../lib/driver.js';
59
64
  import { softSkip } from '../lib/soft-skip.js';
60
65
  import { req } from '../lib/requirement-ids.js';
61
66
  import { readErrorCode } from '../lib/error-envelope.js';
62
67
  import { v2Discovery, familyAdvertised } from '../lib/v2.js';
63
- import { createSyntheticOIDCIssuer, issuerListenPort, type SyntheticOIDCIssuer } from '../lib/oidc-issuer.js';
68
+ import { createSyntheticOIDCIssuer, type SyntheticOIDCIssuer } from '../lib/oidc-issuer.js';
69
+ import { harnessClaimed, serveHarnessIssuer } from '../lib/harness-issuer.js';
64
70
 
65
71
  export const HOST_CALLBACK_NOT_REQUIRED =
66
72
  'the suite stands up the synthetic OIDC issuer and the host fetches its JWKS; no request returns to the suite\'s own API, so no host-reachable callback is needed';
@@ -113,23 +119,20 @@ async function gate(): Promise<Gate | { readonly kind: 'inapplicable' | 'blocked
113
119
  if (window < 2 * MARGIN) {
114
120
  return { kind: 'blocked', reason: `lane ${String(lane['lane'])} advertises a ${window}s window; the control token needs ${MARGIN}s of room inside it and the sabotage tokens ${MARGIN}s outside, so a window under ${2 * MARGIN}s cannot be probed without the two cases overlapping` };
115
121
  }
116
- const url = process.env['OPENWOP_TEST_OIDC_ISSUER_URL']?.trim();
117
- if (!url) {
118
- return { kind: 'blocked', reason: 'OPENWOP_TEST_OIDC_ISSUER_URL is not set — without an issuer the host is configured to trust, every minted token is refused for the wrong reason and the control leg (which is what makes the refusals non-vacuous) cannot run' };
119
- }
122
+ // The harness is an instrument the host claims by listing it in THIS lane's issuers[]
123
+ // (RFC 0168 §C.1's reading; lib/harness-issuer.ts). A production host lists its real
124
+ // IdP and must never list a test issuer, so it records `inapplicable` naming the issuers
125
+ // it does trust. Before 2.40.1 this was `blocked`, which denied certification to every
126
+ // honest production bundle advertising an exp-only lane.
127
+ const claim = harnessClaimed(lane, process.env['OPENWOP_TEST_OIDC_ISSUER_URL']);
128
+ if (!claim.ok) return { kind: claim.kind, reason: claim.reason };
129
+ const url = claim.url;
120
130
  const audience = process.env['OPENWOP_TEST_OIDC_AUDIENCE']?.trim() ?? 'openwop-conformance';
121
131
  if (issuer === null) {
122
132
  const made = createSyntheticOIDCIssuer({ issuer: url, audience, algorithm: 'RS256' });
123
- const srv = createServer((r, res) => {
124
- if (r.url === '/.well-known/jwks.json') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(made.jwksJson); return; }
125
- if (r.url === '/.well-known/openid-configuration') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(made.discoveryJson); return; }
126
- res.writeHead(404); res.end();
127
- });
128
- await new Promise<void>((resolve, reject) => {
129
- srv.once('error', reject);
130
- srv.listen(issuerListenPort(url), '127.0.0.1', () => resolve());
131
- });
132
- server = srv;
133
+ try { server = await serveHarnessIssuer(made, url); } catch (e) {
134
+ return { kind: 'blocked', reason: `lane ${String(lane['lane'])} lists the harness issuer, but it could not be served for ${url}: ${(e as Error).message}` };
135
+ }
133
136
  issuer = made;
134
137
  }
135
138
  return { issuer, url, window, lane: String(lane['lane']) };
@@ -10,10 +10,14 @@
10
10
  * ID-token case explicitly as admissible under the audience MUST `identity.md` §2.1
11
11
  * already carries, and refuses everything else.
12
12
  *
13
- * **Gate:** an advertised `oidc` lane AND `OPENWOP_TEST_OIDC_ISSUER_URL` naming a
14
- * synthetic issuer the host trusts as that lane's trust root. Without the harness issuer
15
- * the suite cannot mint a token the host would ever accept, so the leg is `blocked` — an
16
- * assertion that a random string is refused would witness nothing.
13
+ * **Gate:** an advertised `oidc` lane whose `issuers[]` lists `OPENWOP_TEST_OIDC_ISSUER_URL`,
14
+ * the synthetic issuer the suite holds the key for. A lane that does not list it (every
15
+ * production host, which must not trust a test issuer) records `inapplicable`, naming the
16
+ * issuers it does trust — the harness is a suite instrument the host claims by advertising
17
+ * it, RFC 0168 §C.1's reading for the seams profile (lib/harness-issuer.ts; corrected in
18
+ * 2.40.1, when this was `blocked` and denied certification to every production bundle).
19
+ * A claimed harness that cannot be served, or whose same-audience control is refused, is
20
+ * `blocked`: an assertion that a random string is refused would witness nothing.
17
21
  *
18
22
  * `HOST_CALLBACK_NOT_REQUIRED`: the suite stands the issuer up itself and the host reaches
19
23
  * it for JWKS; nothing calls back into the suite's own API.
@@ -29,13 +33,14 @@
29
33
 
30
34
  import { afterAll, describe, it, expect } from 'vitest';
31
35
  import { randomBytes } from 'node:crypto';
32
- import { createServer, type Server } from 'node:http';
36
+ import type { Server } from 'node:http';
33
37
  import { driver } from '../lib/driver.js';
34
38
  import { softSkip } from '../lib/soft-skip.js';
35
39
  import { req } from '../lib/requirement-ids.js';
36
40
  import { readErrorCode } from '../lib/error-envelope.js';
37
- import { createSyntheticOIDCIssuer, issuerListenPort, type SyntheticOIDCIssuer } from '../lib/oidc-issuer.js';
41
+ import { createSyntheticOIDCIssuer, type SyntheticOIDCIssuer } from '../lib/oidc-issuer.js';
38
42
  import { prmGate } from '../lib/protected-resource.js';
43
+ import { harnessClaimed, serveHarnessIssuer } from '../lib/harness-issuer.js';
39
44
 
40
45
  export const HOST_CALLBACK_NOT_REQUIRED =
41
46
  'the suite stands up the synthetic OIDC issuer and the host fetches its JWKS; no request returns to the suite\'s own API, so no host-reachable callback is needed';
@@ -50,22 +55,11 @@ afterAll(async () => {
50
55
  issuer = null;
51
56
  });
52
57
 
53
- /** Stand the issuer up on the URL the operator told the host to trust. */
54
- async function harness(audience: string): Promise<{ url: string; issuer: SyntheticOIDCIssuer } | null> {
55
- const url = process.env['OPENWOP_TEST_OIDC_ISSUER_URL']?.trim();
56
- if (!url) return null;
58
+ /** Stand the issuer up on the URL the host's oidc lane lists (the claim is checked by the caller). */
59
+ async function harness(url: string, audience: string): Promise<{ url: string; issuer: SyntheticOIDCIssuer } | string> {
57
60
  if (issuer !== null) return { url, issuer };
58
61
  const made = createSyntheticOIDCIssuer({ issuer: url, audience, algorithm: 'RS256' });
59
- const srv = createServer((r, res) => {
60
- if (r.url === '/.well-known/jwks.json') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(made.jwksJson); return; }
61
- if (r.url === '/.well-known/openid-configuration') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(made.discoveryJson); return; }
62
- res.writeHead(404); res.end();
63
- });
64
- await new Promise<void>((resolve, reject) => {
65
- srv.once('error', reject);
66
- srv.listen(issuerListenPort(url), '127.0.0.1', () => resolve());
67
- });
68
- server = srv;
62
+ try { server = await serveHarnessIssuer(made, url); } catch (e) { return `the harness issuer could not be served for ${url}: ${(e as Error).message}`; }
69
63
  issuer = made;
70
64
  return { url, issuer: made };
71
65
  }
@@ -76,9 +70,16 @@ describe('RFC 0200 §D — v2-oidc-id-token-audience (gated on an oidc lane and
76
70
  if (!g.ok) return softSkip(g.kind, g.reason);
77
71
  const oidc = g.lanes.find((l) => l.lane === 'oidc');
78
72
  if (oidc === undefined) return softSkip('inapplicable', 'no oidc lane advertised — §D names the ID-token case ON that lane and binds no other');
73
+ // The harness is an instrument the host claims by listing it in the lane's issuers[]
74
+ // (RFC 0168 §C.1's reading; lib/harness-issuer.ts). A production host lists its real
75
+ // IdP and must never list a test issuer, so that host records `inapplicable` with the
76
+ // issuers it does trust, not `blocked` — before 2.40.1 this was `blocked` and denied
77
+ // certification to every honest production bundle with an oidc lane.
78
+ const claim = harnessClaimed(oidc as unknown as Record<string, unknown>, process.env['OPENWOP_TEST_OIDC_ISSUER_URL']);
79
+ if (!claim.ok) return softSkip(claim.kind, claim.reason);
79
80
  const audience = process.env['OPENWOP_TEST_OIDC_AUDIENCE']?.trim() ?? 'openwop-conformance';
80
- const h = await harness(audience);
81
- if (h === null) return softSkip('blocked', 'OPENWOP_TEST_OIDC_ISSUER_URL is not set — without an issuer the host trusts, no minted token could ever be accepted and the admissible half of §D cannot run');
81
+ const h = await harness(claim.url, audience);
82
+ if (typeof h === 'string') return softSkip('blocked', `the oidc lane lists the harness issuer, but ${h}`);
82
83
 
83
84
  // An ID token, not an access token: `nonce` and `auth_time` are what make it one.
84
85
  const good = h.issuer.mint({ sub: `conformance-${randomBytes(6).toString('hex')}`, nonce: randomBytes(8).toString('hex'), auth_time: Math.floor(Date.now() / 1000) });
@@ -90,7 +91,7 @@ describe('RFC 0200 §D — v2-oidc-id-token-audience (gated on an oidc lane and
90
91
  // acceptance DOES establish, once it holds, is that the refusal below is caused by the
91
92
  // audience and by nothing else: same issuer, same key, same token shape.
92
93
  if (probe.status === 401) {
93
- return softSkip('blocked', `a same-audience ID token from ${h.url} was refused (401 ${readErrorCode(probe.json) ?? ''}) — either the host does not trust the harness issuer as its oidc lane trust root, or OPENWOP_TEST_OIDC_AUDIENCE does not name the audience it is configured with; with no accepted token the foreign-audience refusal would prove nothing`);
94
+ return softSkip('blocked', `a same-audience ID token from ${h.url} was refused (401 ${readErrorCode(probe.json) ?? ''}) although the oidc lane lists that issuer — either the host's trust configuration does not match its advertisement, or OPENWOP_TEST_OIDC_AUDIENCE does not name the audience it is configured with; with no accepted token the foreign-audience refusal would prove nothing`);
94
95
  }
95
96
 
96
97
  const foreign = h.issuer.mint({ sub: `conformance-${randomBytes(6).toString('hex')}`, aud: `other-client-${randomBytes(6).toString('hex')}`, nonce: randomBytes(8).toString('hex'), auth_time: Math.floor(Date.now() / 1000) });