@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.
- package/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/cli.js +18 -2
- package/dist/lib/certification-bundle-v3.js +12 -4
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirement-aliases.json +3 -1
- package/requirements.json +265 -65
- package/schemas/CORPUS-STAMP.json +15 -13
- package/src/cli.ts +20 -2
- package/src/lib/certification-bundle-v3.ts +15 -5
- package/src/lib/harness-issuer.ts +101 -0
- package/src/lib/v2-retired-members.ts +119 -0
- package/src/scenarios/v2-a2ui-v09-surface.test.ts +15 -0
- package/src/scenarios/v2-artifact-a2a-shape.test.ts +14 -0
- package/src/scenarios/v2-capability-maturity-bounded.test.ts +62 -1
- package/src/scenarios/v2-lane-exp-only-bound.test.ts +22 -19
- package/src/scenarios/v2-oidc-id-token-audience.test.ts +24 -23
|
@@ -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)
|
|
27
|
-
* synthetic issuer the
|
|
28
|
-
*
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
|
14
|
-
* synthetic issuer the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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 {
|
|
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,
|
|
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
|
|
54
|
-
async function harness(audience: string): Promise<{ url: string; issuer: SyntheticOIDCIssuer } |
|
|
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
|
-
|
|
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 ===
|
|
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
|
|
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) });
|