@openwop/openwop-conformance 2.40.2 → 2.40.3
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 +7 -0
- package/README.md +1 -1
- package/dist/lib/certification-bundle-verify.js +24 -2
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +37 -8
- package/schemas/CORPUS-STAMP.json +11 -11
- package/src/lib/certification-bundle-verify.ts +23 -1
- package/src/scenarios/certification-bundle-redaction.test.ts +30 -7
- package/src/scenarios/v2-revocation-honored.test.ts +11 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.40.3] — 2026-09-26 — a cookie-borne lane can be revoke-witnessed, and a secret-named setting no longer corrupts the discovery digest
|
|
4
|
+
|
|
5
|
+
- **`v2-revocation-honored` presents a cookie-lane credential as a cookie.** When the mint seam answers `presentation: { kind: "cookie", name }`, the probe sends `Cookie: <name>=<credential>` instead of a bearer token. The seams contract's `mintLaneCredential` response gains that optional member. Bearer stays the default when `presentation` is absent. The cookie branch is unexercised until a host serves it; openwop-app's `session` lane is the first expected.
|
|
6
|
+
- **RFC 0215 gap G1 closes in the contract peer.** `@openwop/spec-artifacts`' `spec/v1/gaps.json` records MyndHyve's measurement (openwop#1595), and `spec/v2/core/webhooks.md`'s `Stable` banner cites RFC 0215 now that it is `Accepted`. No scenario change.
|
|
7
|
+
- **A setting that names a secret is not scrubbed as one.** `evidenceSecretsFromEnv` sweeps every `OPENWOP_*` value whose NAME mentions a key/token/secret/password, and `scrubEvidence` then rewrites that value everywhere in the bundle. openwop-app's RFC 0201 setting `OPENWOP_WEBHOOK_SECRET_ROTATION_OVERLAP_S=60` was swept, the "60" inside `discovery.sha256` was rewritten, the bundle failed `^[0-9a-f]{64}$`, and its deploy certify refused to ship. Values guessed from names are now swept only when credential-shaped (at least 8 characters and not a bare integer); a credential the emitter hands over explicitly is scrubbed whatever its shape. Sabotage: without the floor the new redaction leg fails.
|
|
8
|
+
- **Version moved ahead of publication.** `@openwop/openwop-conformance` and its exact-pinned peer `@openwop/spec-artifacts` move to `2.40.3` because `2.40.2` is tagged. Not tagged, not published.
|
|
9
|
+
|
|
3
10
|
## [2.40.2] — 2026-09-26 — production bundles stop carrying harness-issuer blocked rows, and a colocated companion is marked
|
|
4
11
|
|
|
5
12
|
- **RFC 0216: `--as-colocated-companion`, and a companion witnesses only the harness-trust-anchor rows.** The flag marks a v3 bundle `host.deployment: "colocated-companion"`. It is refused without `--certify` and bundle version 3. `witnessDigest` takes the marker into the preimage when present (`{ rows, relaxations?, deployment }`), so a signed companion cannot shed it: `--verify` then refuses with `witness-digest`. New coherence scenario `v2-colocated-companion` (7 legs, one id each) drives `scripts/check-harness-trust-anchors.mjs` and `scripts/check-companion-pairing.mjs` on tmpdir fixtures signed by the suite's own `signBundleV3`. Five sabotages were each run red.
|
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
# --legacy-peer-deps is REQUIRED, not optional: the exact peer pin is what npm's
|
|
12
12
|
# default resolver refuses. npm 10.9 fails outright with
|
|
13
13
|
# "Cannot read properties of null (reading 'edgesOut')" — use npm >= 11.
|
|
14
|
-
npm install --legacy-peer-deps @openwop/openwop-conformance@2.40.
|
|
14
|
+
npm install --legacy-peer-deps @openwop/openwop-conformance@2.40.3 @openwop/spec-artifacts@2.40.3
|
|
15
15
|
# or run without install:
|
|
16
16
|
npx @openwop/openwop-conformance --base-url https://api.example.com --api-key hk_test_...
|
|
17
17
|
```
|
|
@@ -267,6 +267,25 @@ export function scrubEvidence(value, secrets) {
|
|
|
267
267
|
* are excluded, and a caller may name values that MUST stay visible (`except`)
|
|
268
268
|
* regardless of which variable carried them.
|
|
269
269
|
*/
|
|
270
|
+
/**
|
|
271
|
+
* Whether an environment VALUE can be a credential at all (suite 2.40.3).
|
|
272
|
+
*
|
|
273
|
+
* The name filter above matches settings ABOUT secrets as well as secrets:
|
|
274
|
+
* openwop-app's `OPENWOP_WEBHOOK_SECRET_ROTATION_OVERLAP_S=60` (RFC 0201's
|
|
275
|
+
* rotation overlap) was swept in, and `scrubEvidence` then rewrote every "60"
|
|
276
|
+
* in the bundle — including the one inside `discovery.sha256`, which stopped
|
|
277
|
+
* matching `^[0-9a-f]{64}$` and failed the host's own deploy certify. A short
|
|
278
|
+
* value occurs by chance inside digests, counts and timestamps, so scrubbing it
|
|
279
|
+
* corrupts the evidence without protecting anything: no credential is a bare
|
|
280
|
+
* integer or under 8 characters (NIST SP 800-63B's floor for a memorized
|
|
281
|
+
* secret). The floor applies only to values GUESSED from variable names; a
|
|
282
|
+
* credential the emitter hands over explicitly (`extra`) is always scrubbed,
|
|
283
|
+
* whatever its shape.
|
|
284
|
+
*/
|
|
285
|
+
export function credentialShaped(v) {
|
|
286
|
+
const t = v.trim();
|
|
287
|
+
return t.length >= 8 && !/^\d+$/.test(t);
|
|
288
|
+
}
|
|
270
289
|
export function evidenceSecretsFromEnv(env, extra = [], except = []) {
|
|
271
290
|
const keep = new Set(except.filter((s) => s !== undefined && s.trim() !== ''));
|
|
272
291
|
const out = new Set();
|
|
@@ -277,8 +296,11 @@ export function evidenceSecretsFromEnv(env, extra = [], except = []) {
|
|
|
277
296
|
continue;
|
|
278
297
|
if (/_ID$/.test(k))
|
|
279
298
|
continue; // a key's identifier is published, not secret
|
|
280
|
-
if (v
|
|
281
|
-
|
|
299
|
+
if (v === undefined || v.trim() === '' || keep.has(v))
|
|
300
|
+
continue;
|
|
301
|
+
if (!credentialShaped(v))
|
|
302
|
+
continue; // a setting that names a secret, not a secret
|
|
303
|
+
out.add(v);
|
|
282
304
|
}
|
|
283
305
|
for (const s of extra)
|
|
284
306
|
if (s !== undefined && s.trim() !== '' && !keep.has(s))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.40.
|
|
3
|
+
"version": "2.40.3",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@openwop/spec-artifacts": "file:../spec-artifacts"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@openwop/spec-artifacts": "2.40.
|
|
59
|
+
"@openwop/spec-artifacts": "2.40.3"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
"generatedFrom": "src/scenarios/*.test.ts",
|
|
4
4
|
"counts": {
|
|
5
5
|
"files": 636,
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
6
|
+
"tests": 2438,
|
|
7
|
+
"withStableId": 2438,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
9
|
+
"explicitIds": 2332
|
|
10
10
|
},
|
|
11
11
|
"records": [
|
|
12
12
|
{
|
|
@@ -7170,9 +7170,38 @@
|
|
|
7170
7170
|
]
|
|
7171
7171
|
},
|
|
7172
7172
|
{
|
|
7173
|
-
"id": "openwop.it.certification-bundle-redaction.evidencesecretsfromenv-never-
|
|
7173
|
+
"id": "openwop.it.certification-bundle-redaction.evidencesecretsfromenv-never-treats-a-setting-about-a-secret-as-one-a-bare-integ",
|
|
7174
7174
|
"file": "certification-bundle-redaction.test.ts",
|
|
7175
7175
|
"line": 157,
|
|
7176
|
+
"title": "evidenceSecretsFromEnv never treats a setting ABOUT a secret as one: a bare integer or a value under 8 characters is not swept from the environment",
|
|
7177
|
+
"explicitId": "openwop.it.certification-bundle-redaction.setting-about-a-secret-not-swept",
|
|
7178
|
+
"citations": [
|
|
7179
|
+
{
|
|
7180
|
+
"section": "RFC 0148 §C",
|
|
7181
|
+
"requirement": "a bare-integer setting whose NAME mentions a secret (a rotation overlap, a TTL) MUST NOT be swept as a credential"
|
|
7182
|
+
},
|
|
7183
|
+
{
|
|
7184
|
+
"section": "RFC 0148 §C",
|
|
7185
|
+
"requirement": "a value under 8 characters is not credential-shaped and is not swept from the environment"
|
|
7186
|
+
},
|
|
7187
|
+
{
|
|
7188
|
+
"section": "RFC 0148 §C",
|
|
7189
|
+
"requirement": "a credential-shaped value is still swept"
|
|
7190
|
+
},
|
|
7191
|
+
{
|
|
7192
|
+
"section": "RFC 0148 §C",
|
|
7193
|
+
"requirement": "a credential the emitter hands over explicitly is scrubbed whatever its shape"
|
|
7194
|
+
},
|
|
7195
|
+
{
|
|
7196
|
+
"section": "RFC 0148 §C",
|
|
7197
|
+
"requirement": "scrubbing MUST NOT corrupt a digest the bundle carries"
|
|
7198
|
+
}
|
|
7199
|
+
]
|
|
7200
|
+
},
|
|
7201
|
+
{
|
|
7202
|
+
"id": "openwop.it.certification-bundle-redaction.evidencesecretsfromenv-never-scrubs-a-key-identifier-id-variables-are-excluded-a",
|
|
7203
|
+
"file": "certification-bundle-redaction.test.ts",
|
|
7204
|
+
"line": 180,
|
|
7176
7205
|
"title": "evidenceSecretsFromEnv never scrubs a key IDENTIFIER: *_ID variables are excluded and `except` values stay visible whichever variable carried them",
|
|
7177
7206
|
"explicitId": "openwop.it.certification-bundle-redaction.key-identifier-never-scrubbed",
|
|
7178
7207
|
"citations": [
|
|
@@ -7189,7 +7218,7 @@
|
|
|
7189
7218
|
{
|
|
7190
7219
|
"id": "openwop.it.certification-bundle-redaction.verifybundlev2-rejects-a-bundle-carrying-the-conformance-canary-anywhere-and-acc",
|
|
7191
7220
|
"file": "certification-bundle-redaction.test.ts",
|
|
7192
|
-
"line":
|
|
7221
|
+
"line": 200,
|
|
7193
7222
|
"title": "verifyBundleV2 REJECTS a bundle carrying the conformance canary anywhere, and accepts the scrubbed twin",
|
|
7194
7223
|
"explicitId": "openwop.it.certification-bundle-redaction.verifybundlev2-rejects-a-bundle-carrying-the-conformance-canary-anywhere-and-acc",
|
|
7195
7224
|
"citations": [
|
|
@@ -7202,7 +7231,7 @@
|
|
|
7202
7231
|
{
|
|
7203
7232
|
"id": "openwop.it.certification-bundle-redaction.the-canary-in-a-discovery-document-key-is-still-a-rejection-keys-are-evidence",
|
|
7204
7233
|
"file": "certification-bundle-redaction.test.ts",
|
|
7205
|
-
"line":
|
|
7234
|
+
"line": 215,
|
|
7206
7235
|
"title": "the canary in a discovery-document KEY is still a rejection (keys are evidence)",
|
|
7207
7236
|
"explicitId": "openwop.it.certification-bundle-redaction.the-canary-in-a-discovery-document-key-is-still-a-rejection-keys-are-evidence",
|
|
7208
7237
|
"citations": [
|
|
@@ -7215,7 +7244,7 @@
|
|
|
7215
7244
|
{
|
|
7216
7245
|
"id": "openwop.it.certification-bundle-redaction.the-target-configuration-digest-is-a-digest-the-api-key-is-not-a-substring-of-an",
|
|
7217
7246
|
"file": "certification-bundle-redaction.test.ts",
|
|
7218
|
-
"line":
|
|
7247
|
+
"line": 221,
|
|
7219
7248
|
"title": "the target-configuration digest is a digest: the API key is not a substring of any string in the document",
|
|
7220
7249
|
"explicitId": "openwop.it.certification-bundle-redaction.the-target-configuration-digest-is-a-digest-the-api-key-is-not-a-substring-of-an",
|
|
7221
7250
|
"citations": [
|
|
@@ -32080,7 +32109,7 @@
|
|
|
32080
32109
|
{
|
|
32081
32110
|
"id": "openwop.it.v2-revocation-honored.a-revoked-next-request-credential-is-refused-on-the-next-request-with-credential",
|
|
32082
32111
|
"file": "v2-revocation-honored.test.ts",
|
|
32083
|
-
"line":
|
|
32112
|
+
"line": 40,
|
|
32084
32113
|
"title": "a revoked next-request credential is refused on the next request with credential_revoked",
|
|
32085
32114
|
"explicitId": "openwop.requirement.0170.revocation-honored",
|
|
32086
32115
|
"citations": [
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
|
|
3
3
|
"package": "@openwop/spec-artifacts",
|
|
4
|
-
"version": "2.40.
|
|
5
|
-
"corpusTag": "v2.40.
|
|
4
|
+
"version": "2.40.3",
|
|
5
|
+
"corpusTag": "v2.40.3",
|
|
6
6
|
"files": {
|
|
7
7
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
8
8
|
"api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
|
|
9
9
|
"api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
|
|
10
10
|
"api/openapi.yaml": "6366b5514be71098e9ea41fc81ff52bc39db1a547b353c0b344cc2a2bf5370aa",
|
|
11
11
|
"api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
|
|
12
|
-
"api/seams-v2.yaml": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "96b5b49ba8aab1595b547d568ece7c4fb9fae9d7bdde29aff7c39f20ef31b120",
|
|
13
|
+
"api/v2/asyncapi.yaml": "cc4d94df8eb1e168f30f8c12279bc65cf34c948a5ed95cc062288728fc605618",
|
|
14
|
+
"api/v2/openapi.yaml": "5029e637cd61c900b169cb3f79e61e48b3ee89a6c3c11f3262158bf2fa910125",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -204,13 +204,13 @@
|
|
|
204
204
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
205
205
|
"spec/v1/alias-detectors.json": "2401fcb1c18cdd688c018b3d716ae6bca85c5e356220ee2b793bd9d81872412d",
|
|
206
206
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
207
|
-
"spec/v1/core-standard-manifest.json": "
|
|
207
|
+
"spec/v1/core-standard-manifest.json": "c8d01314ab747038fb9b3ef17552d336f61211591b4af07379e65f65a6e55454",
|
|
208
208
|
"spec/v1/deprecations.json": "2d03f4729810280147ea08c630ea65434f2dc5370567d0aac41c3595f8337d40",
|
|
209
209
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
210
210
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
211
211
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
212
212
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
213
|
-
"spec/v1/gaps.json": "
|
|
213
|
+
"spec/v1/gaps.json": "41e5bbd50515eb08923a29a79dfeb41d045f2b28dec9b2af3473c18db148c7be",
|
|
214
214
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
215
215
|
"spec/v1/migrations.json": "8839497d6a4830b53fa7867014a8336f0d725ef2346352e54fb580b7947897af",
|
|
216
216
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
@@ -240,7 +240,7 @@
|
|
|
240
240
|
"spec/v2/core/security-defaults.md": "500471a8db7af9b776ac40b4a9a278c9f3d45e1c1dead880114e7fadc7bee37d",
|
|
241
241
|
"spec/v2/core/tool-catalog.md": "35f1a3fd509db0fc0490ddf400686fe94dc4b20c98064d5ebe776da516153e60",
|
|
242
242
|
"spec/v2/core/versioning.md": "0aded71d090c8358c3ce17763c120cfc1cc42531a5eaa1b8a2a7016ed9594069",
|
|
243
|
-
"spec/v2/core/webhooks.md": "
|
|
243
|
+
"spec/v2/core/webhooks.md": "2a44b100c42dd841fe2b8f87179a981be6c0dd26608ce1b14e077318c4ac8201",
|
|
244
244
|
"spec/v2/core/workflow-chain-packs.md": "ee45d3fede3bb6f0cc6abcf0d6c929c7cd213a0fbe862b738a998f929c4d1102",
|
|
245
245
|
"spec/v2/corrections.json": "cc74ffde74384b0f261a4f625871d922e8661d654d647a8534e21036595cb280",
|
|
246
246
|
"spec/v2/corrections.schema.json": "4ac595b9a6d7f66d53de03fe0edbe0dc58de46426932387bfdc0ceb3cf949a3e",
|
|
@@ -294,9 +294,9 @@
|
|
|
294
294
|
"spec/v2/path-manifest.json": "ae9b56d03a701061fd48ad0b73cb0373493f6a4547da92763b422295b5e19535",
|
|
295
295
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
296
296
|
"spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
|
|
297
|
-
"spec/v2/release.json": "
|
|
297
|
+
"spec/v2/release.json": "4c84997343779f46a54f9c4caf3d02714abc563f618b25729ca4b52468a23d39",
|
|
298
298
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
299
|
-
"spec/v2/surface-baseline.json": "
|
|
299
|
+
"spec/v2/surface-baseline.json": "2f1da906609307ec885cb04c36d7e688cd3124933759c88dc9ae3fa0ee9ec88b"
|
|
300
300
|
},
|
|
301
|
-
"corpusCommit": "
|
|
301
|
+
"corpusCommit": "5d3d440cac757fb2b6c08452f64785eaeda086d3"
|
|
302
302
|
}
|
|
@@ -354,6 +354,26 @@ export function scrubEvidence<T>(value: T, secrets: readonly string[]): ScrubRes
|
|
|
354
354
|
* are excluded, and a caller may name values that MUST stay visible (`except`)
|
|
355
355
|
* regardless of which variable carried them.
|
|
356
356
|
*/
|
|
357
|
+
/**
|
|
358
|
+
* Whether an environment VALUE can be a credential at all (suite 2.40.3).
|
|
359
|
+
*
|
|
360
|
+
* The name filter above matches settings ABOUT secrets as well as secrets:
|
|
361
|
+
* openwop-app's `OPENWOP_WEBHOOK_SECRET_ROTATION_OVERLAP_S=60` (RFC 0201's
|
|
362
|
+
* rotation overlap) was swept in, and `scrubEvidence` then rewrote every "60"
|
|
363
|
+
* in the bundle — including the one inside `discovery.sha256`, which stopped
|
|
364
|
+
* matching `^[0-9a-f]{64}$` and failed the host's own deploy certify. A short
|
|
365
|
+
* value occurs by chance inside digests, counts and timestamps, so scrubbing it
|
|
366
|
+
* corrupts the evidence without protecting anything: no credential is a bare
|
|
367
|
+
* integer or under 8 characters (NIST SP 800-63B's floor for a memorized
|
|
368
|
+
* secret). The floor applies only to values GUESSED from variable names; a
|
|
369
|
+
* credential the emitter hands over explicitly (`extra`) is always scrubbed,
|
|
370
|
+
* whatever its shape.
|
|
371
|
+
*/
|
|
372
|
+
export function credentialShaped(v: string): boolean {
|
|
373
|
+
const t = v.trim();
|
|
374
|
+
return t.length >= 8 && !/^\d+$/.test(t);
|
|
375
|
+
}
|
|
376
|
+
|
|
357
377
|
export function evidenceSecretsFromEnv(
|
|
358
378
|
env: NodeJS.ProcessEnv,
|
|
359
379
|
extra: readonly (string | undefined)[] = [],
|
|
@@ -365,7 +385,9 @@ export function evidenceSecretsFromEnv(
|
|
|
365
385
|
if (!k.startsWith('OPENWOP_')) continue;
|
|
366
386
|
if (!/(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/.test(k)) continue;
|
|
367
387
|
if (/_ID$/.test(k)) continue; // a key's identifier is published, not secret
|
|
368
|
-
if (v
|
|
388
|
+
if (v === undefined || v.trim() === '' || keep.has(v)) continue;
|
|
389
|
+
if (!credentialShaped(v)) continue; // a setting that names a secret, not a secret
|
|
390
|
+
out.add(v);
|
|
369
391
|
}
|
|
370
392
|
for (const s of extra) if (s !== undefined && s.trim() !== '' && !keep.has(s)) out.add(s);
|
|
371
393
|
out.add(CONFORMANCE_SECRET_CANARY);
|
|
@@ -141,17 +141,40 @@ describe('RFC 0148 §C — certification-bundle-redaction: secret canaries never
|
|
|
141
141
|
|
|
142
142
|
it('evidenceSecretsFromEnv selects OPENWOP_* key/token/secret/password variables, the handed credential, and ALWAYS the canary', () => {
|
|
143
143
|
const env = {
|
|
144
|
-
OPENWOP_API_KEY: 'k1',
|
|
145
|
-
OPENWOP_MESSAGING_BRIDGE_TOKEN: 't1',
|
|
146
|
-
OPENWOP_DB_PASSWORD: 'p1',
|
|
147
|
-
OPENWOP_CLIENT_SECRET: 's1',
|
|
144
|
+
OPENWOP_API_KEY: 'k1-api-key-value',
|
|
145
|
+
OPENWOP_MESSAGING_BRIDGE_TOKEN: 't1-bridge-token',
|
|
146
|
+
OPENWOP_DB_PASSWORD: 'p1-db-password',
|
|
147
|
+
OPENWOP_CLIENT_SECRET: 's1-client-secret',
|
|
148
148
|
OPENWOP_BASE_URL: 'https://example.invalid', // not a secret
|
|
149
149
|
OPENWOP_REQUIRE_BEHAVIOR: 'true',
|
|
150
150
|
PATH: '/usr/bin',
|
|
151
151
|
SOME_OTHER_TOKEN: 'not-ours',
|
|
152
152
|
} as NodeJS.ProcessEnv;
|
|
153
153
|
const secrets = evidenceSecretsFromEnv(env, [API_KEY, undefined, '']);
|
|
154
|
-
expect(secrets.sort(), req('openwop.it.certification-bundle-redaction.evidencesecretsfromenv-selects-openwop-key-token-secret-password-variables-the-h', 'RFC 0148 §C', 'evidenceSecretsFromEnv selects OPENWOP_* key/token/secret/password variables, the handed credential, and ALWAYS the canary')).toEqual([API_KEY, 'k1', 'p1', 's1', 't1', CONFORMANCE_SECRET_CANARY].sort());
|
|
154
|
+
expect(secrets.sort(), req('openwop.it.certification-bundle-redaction.evidencesecretsfromenv-selects-openwop-key-token-secret-password-variables-the-h', 'RFC 0148 §C', 'evidenceSecretsFromEnv selects OPENWOP_* key/token/secret/password variables, the handed credential, and ALWAYS the canary')).toEqual([API_KEY, 'k1-api-key-value', 'p1-db-password', 's1-client-secret', 't1-bridge-token', CONFORMANCE_SECRET_CANARY].sort());
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('evidenceSecretsFromEnv never treats a setting ABOUT a secret as one: a bare integer or a value under 8 characters is not swept from the environment', () => {
|
|
158
|
+
// Suite 2.40.2: openwop-app set OPENWOP_WEBHOOK_SECRET_ROTATION_OVERLAP_S=60,
|
|
159
|
+
// the name matched SECRET, and scrubEvidence rewrote the "60" inside
|
|
160
|
+
// discovery.sha256 — the bundle failed ^[0-9a-f]{64}$ and main could not deploy.
|
|
161
|
+
const digest = '4b8d99c002a061a0255e8224ee683162007febadd251b2d171b47c0849695315';
|
|
162
|
+
const env = {
|
|
163
|
+
OPENWOP_WEBHOOK_SECRET_ROTATION_OVERLAP_S: '60',
|
|
164
|
+
OPENWOP_TOKEN_TTL_SECONDS_KEY: '86400000',
|
|
165
|
+
OPENWOP_SHORT_KEY: 'abc1234',
|
|
166
|
+
OPENWOP_REAL_SECRET: 'whsec_c2VjcmV0LXZhbHVl',
|
|
167
|
+
} as NodeJS.ProcessEnv;
|
|
168
|
+
const secrets = evidenceSecretsFromEnv(env, ['k9']);
|
|
169
|
+
const R = 'openwop.it.certification-bundle-redaction.setting-about-a-secret-not-swept';
|
|
170
|
+
expect(secrets, req(R, 'RFC 0148 §C', 'a bare-integer setting whose NAME mentions a secret (a rotation overlap, a TTL) MUST NOT be swept as a credential')).not.toContain('60');
|
|
171
|
+
expect(secrets).not.toContain('86400000');
|
|
172
|
+
expect(secrets, req(R, 'RFC 0148 §C', 'a value under 8 characters is not credential-shaped and is not swept from the environment')).not.toContain('abc1234');
|
|
173
|
+
expect(secrets, req(R, 'RFC 0148 §C', 'a credential-shaped value is still swept')).toContain('whsec_c2VjcmV0LXZhbHVl');
|
|
174
|
+
expect(secrets, req(R, 'RFC 0148 §C', 'a credential the emitter hands over explicitly is scrubbed whatever its shape')).toContain('k9');
|
|
175
|
+
const { value } = scrubEvidence({ discovery: { sha256: digest } }, secrets);
|
|
176
|
+
expect(value.discovery.sha256, req(R, 'RFC 0148 §C', 'scrubbing MUST NOT corrupt a digest the bundle carries')).toBe(digest);
|
|
177
|
+
expect(value.discovery.sha256).toMatch(/^[0-9a-f]{64}$/);
|
|
155
178
|
});
|
|
156
179
|
|
|
157
180
|
it('evidenceSecretsFromEnv never scrubs a key IDENTIFIER: *_ID variables are excluded and `except` values stay visible whichever variable carried them', () => {
|
|
@@ -162,14 +185,14 @@ describe('RFC 0148 §C — certification-bundle-redaction: secret canaries never
|
|
|
162
185
|
OPENWOP_BUNDLE_SIGNING_KEY: '-----BEGIN PRIVATE KEY-----\nsecret\n-----END PRIVATE KEY-----',
|
|
163
186
|
OPENWOP_BUNDLE_SIGNING_KEY_ID: 'myndhyve-bundle-2026-09',
|
|
164
187
|
OPENWOP_BUNDLE_VERIFIER_KEY_ID: 'steward-verifier-1',
|
|
165
|
-
OPENWOP_API_KEY: 'k1',
|
|
188
|
+
OPENWOP_API_KEY: 'k1-api-key-value',
|
|
166
189
|
OPENWOP_ODDLY_NAMED_KEY: 'published-key-id-in-a-secret-shaped-variable',
|
|
167
190
|
} as NodeJS.ProcessEnv;
|
|
168
191
|
const secrets = evidenceSecretsFromEnv(env, [], ['published-key-id-in-a-secret-shaped-variable']);
|
|
169
192
|
expect(secrets, req('openwop.it.certification-bundle-redaction.key-identifier-never-scrubbed', 'RFC 0168 §E.2', 'the keyId a host publishes in discovery.document.signingKeys[] MUST survive evidence scrubbing — a *_ID variable is an identifier, not a secret')).not.toContain('myndhyve-bundle-2026-09');
|
|
170
193
|
expect(secrets).not.toContain('steward-verifier-1');
|
|
171
194
|
expect(secrets, req('openwop.it.certification-bundle-redaction.key-identifier-never-scrubbed', 'RFC 0168 §E.2', 'a value the emitter names as `except` (the keyId it is about to publish) is never scrubbed, whichever variable carried it')).not.toContain('published-key-id-in-a-secret-shaped-variable');
|
|
172
|
-
expect(secrets).toContain('k1');
|
|
195
|
+
expect(secrets).toContain('k1-api-key-value');
|
|
173
196
|
expect(secrets.some((s) => s.includes('BEGIN PRIVATE KEY'))).toBe(true);
|
|
174
197
|
expect(secrets).toContain(CONFORMANCE_SECRET_CANARY);
|
|
175
198
|
});
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
* drives `POST /conformance/seams/sample/auth/credential/{mint,revoke}` (the
|
|
12
12
|
* v1-shaped address `/v1/host/sample/auth/credential/…` through `seamPath()`)
|
|
13
13
|
* with `{ lane }` / `{ lane, credential }` and records `blocked` when the host
|
|
14
|
-
* does not mount it.
|
|
14
|
+
* does not mount it. A mint answer carrying `presentation: { kind: "cookie", name }`
|
|
15
|
+
* (2.40.3) is presented as `Cookie: <name>=<credential>`, so a cookie-borne
|
|
16
|
+
* `session` lane is witnessed as its production credential travels; bearer otherwise. Windowed lanes (`exp-and-recheck`, `short-lived`, `rebind`)
|
|
15
17
|
* have no wire-observable "next request" and record `inapplicable`.
|
|
16
18
|
*/
|
|
17
19
|
|
|
@@ -48,17 +50,22 @@ describe('v2 revocation-honored (RFC 0170 §B.3 — seam-gated)', () => {
|
|
|
48
50
|
const minted = await http(() => driver.post(MINT, { lane }));
|
|
49
51
|
if (minted === null) return softSkip('blocked', `${MINT} unreachable (fetch failed)`);
|
|
50
52
|
if (minted.status === 404 || minted.status === 403) return seamAbsent(`${MINT} not mounted (${minted.status}) — the host advertises the seams profile but does not serve the RFC 0170 §B.3 credential mint/revoke seams (api/seams-v2.yaml mintLaneCredential / revokeLaneCredential; host-sample-test-seams.md)`);
|
|
51
|
-
const
|
|
53
|
+
const body = minted.json as { credential?: unknown; presentation?: { kind?: unknown; name?: unknown } } | undefined;
|
|
54
|
+
const credential = body?.credential;
|
|
55
|
+
// 2.40.3 — a cookie-borne lane (a `session` lane) is presented the way its
|
|
56
|
+
// production credential is, `Cookie: <name>=<credential>`. Bearer otherwise.
|
|
57
|
+
const cookieName = body?.presentation?.kind === 'cookie' && typeof body.presentation.name === 'string' ? body.presentation.name : null;
|
|
58
|
+
const present = (c: unknown): Record<string, string> => (cookieName ? { Cookie: `${cookieName}=${String(c)}` } : { Authorization: `Bearer ${String(c)}` });
|
|
52
59
|
expect(typeof credential, req('openwop.requirement.0170.revocation-honored', DOC, `the mint seam MUST answer { credential } for lane ${lane}`)).toBe('string');
|
|
53
60
|
|
|
54
61
|
// Positive control — the minted credential authenticates before revocation.
|
|
55
|
-
const before = await http(() => driver.get('/runs/openwop-conformance-tenant/revocationprobe0123456789', { authenticated: false, headers:
|
|
62
|
+
const before = await http(() => driver.get('/runs/openwop-conformance-tenant/revocationprobe0123456789', { authenticated: false, headers: present(credential) }));
|
|
56
63
|
expect(before !== null && before.status !== 401, req('openwop.requirement.0170.revocation-honored', DOC, `a freshly minted ${lane} credential MUST authenticate before revocation (got ${before?.status ?? 'no response'})`)).toBe(true);
|
|
57
64
|
|
|
58
65
|
const revoked = await http(() => driver.post(REVOKE, { lane, credential }));
|
|
59
66
|
expect(revoked !== null && revoked.status < 400, req('openwop.requirement.0170.revocation-honored', DOC, `the revoke seam MUST accept the credential it minted for lane ${lane} (got ${revoked?.status ?? 'no response'})`)).toBe(true);
|
|
60
67
|
|
|
61
|
-
const after = await http(() => driver.get('/runs/openwop-conformance-tenant/revocationprobe0123456789', { authenticated: false, headers:
|
|
68
|
+
const after = await http(() => driver.get('/runs/openwop-conformance-tenant/revocationprobe0123456789', { authenticated: false, headers: present(credential) }));
|
|
62
69
|
expect(after?.status, req('openwop.requirement.0170.revocation-honored', DOC, `lane ${lane} MUST refuse a revoked credential on the next request with 401`)).toBe(401);
|
|
63
70
|
expect(readErrorCode(after?.json), req('openwop.requirement.0170.revocation-honored', DOC, `the refusal code MUST be credential_revoked (lane ${lane})`)).toBe('credential_revoked');
|
|
64
71
|
}
|