@openwop/openwop-conformance 2.0.10 → 2.0.11

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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # `@openwop/openwop-conformance` Changelog
2
2
 
3
+ ## [2.0.11] — 2026-09-10 — the emitter scrubbed the key id it was required to publish
4
+
5
+ `--certify` fix. `evidenceSecretsFromEnv` selected every `OPENWOP_*` variable
6
+ whose name matched `/(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/` and replaced its
7
+ value throughout the bundle after `discovery.sha256` was taken.
8
+ `OPENWOP_BUNDLE_SIGNING_KEY_ID` — the variable `--help` offers — matches on
9
+ `KEY`, and its value is the identifier RFC 0168 §E.2 requires
10
+ `discovery.document.signingKeys[].keyId` to publish. The embedded document lost
11
+ its keyId, the digest kept it, and the emitter rejected its own output
12
+ (`discovery.document ≠ discovery.sha256`). Three MyndHyve cuts on 2.0.8 and
13
+ 2.0.9 failed on this alone; the same host passed the day before with the id
14
+ on `--signing-key-id`.
15
+
16
+ - `*_ID` variable names are excluded from the classifier — an identifier of a
17
+ key is not the key.
18
+ - The emitter passes the keyId it is about to publish as `except`, so it
19
+ survives whichever variable carried it.
20
+ - On a self-verification failure the rejected bundle is written to
21
+ `<out>.rejected.json` and named on stderr, instead of being discarded — the
22
+ diagnostic a host had to patch a scratch copy of this CLI to obtain.
23
+ - New redaction leg `openwop.it.certification-bundle-redaction.key-identifier-never-scrubbed`.
24
+
25
+ No scenario logic changes.
26
+
3
27
  ## [2.0.10] — 2026-09-10 — the bare-id leg
4
28
 
5
29
  One new leg in `v2-id-grammar` (`openwop.requirement.0170.id-grammar.bare-id`):
package/dist/cli.js CHANGED
@@ -643,13 +643,20 @@ async function runCertify(args, baseUrl, apiKey) {
643
643
  }
644
644
  v3.verifierSignature = verifierSign(unsigned, vk, vkId);
645
645
  }
646
- const secrets3 = evidenceSecretsFromEnv(process.env, [apiKey, signingKeyPem]);
646
+ // The published keyId is never a secret, whichever variable carried it
647
+ // (evidenceSecretsFromEnv: the `_ID` exclusion is the rule, `except` is
648
+ // the belt for a host that names its env var differently).
649
+ const secrets3 = evidenceSecretsFromEnv(process.env, [apiKey, signingKeyPem], [keyId]);
647
650
  const scrubbed3 = scrubEvidence(v3, secrets3);
648
651
  const v3Out = scrubbed3.value;
649
652
  const audit3 = verifyBundleV3(v3Out, { hostPublicKeyPem: publicKeyFromPrivate(signingKeyPem) });
650
653
  const emitterDefects = audit3.rejections.filter((r) => !['blocked-certified', 'relaxed-profile-certified', 'independent-unverifiable'].includes(r.kind));
651
654
  if (emitterDefects.length > 0) {
652
- process.stderr.write('openwop-conformance --certify: assembled v3 bundle FAILED self-verification (emitter defect):\n' + emitterDefects.map((r) => ` - [${r.kind}] ${r.detail}`).join('\n') + '\n');
655
+ // Keep the rejected bundle: a self-verification failure with nothing to
656
+ // inspect sent a host patching a scratch copy of this CLI to see it.
657
+ const rejectedPath = `${outPath}.rejected.json`;
658
+ writeFileSync(rejectedPath, `${JSON.stringify(v3Out, null, 2)}\n`);
659
+ process.stderr.write('openwop-conformance --certify: assembled v3 bundle FAILED self-verification (emitter defect):\n' + emitterDefects.map((r) => ` - [${r.kind}] ${r.detail}`).join('\n') + `\n the rejected bundle is at ${rejectedPath} (NOT a certification artifact — do not check it in)\n`);
653
660
  process.exit(2);
654
661
  }
655
662
  const v3Schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'certification-bundle.schema.json'), 'utf8'));
@@ -255,19 +255,33 @@ export function scrubEvidence(value, secrets) {
255
255
  * The secrets a `--certify` run must keep out of its own evidence: the
256
256
  * credential it was handed, plus every `OPENWOP_*` environment value that
257
257
  * names a key/token/secret/password, plus the conformance canary.
258
+ *
259
+ * An *identifier* of a key is not the key. `OPENWOP_BUNDLE_SIGNING_KEY_ID`
260
+ * (and `_VERIFIER_KEY_ID`) name the public `keyId` RFC 0168 §E.2 requires the
261
+ * host to publish in `discovery.document.signingKeys[]`; matching them on
262
+ * `KEY` scrubbed that id out of the embedded document AFTER `discovery.sha256`
263
+ * was taken, so every host that passed the id by env failed its own bundle's
264
+ * self-verification (`discovery.document` ≠ `discovery.sha256`) — suites 2.0.8
265
+ * and 2.0.9, found by MyndHyve 2026-09-10 across three cuts; the same host had
266
+ * passed the day before by passing the id as a flag. Names ending in `_ID`
267
+ * are excluded, and a caller may name values that MUST stay visible (`except`)
268
+ * regardless of which variable carried them.
258
269
  */
259
- export function evidenceSecretsFromEnv(env, extra = []) {
270
+ export function evidenceSecretsFromEnv(env, extra = [], except = []) {
271
+ const keep = new Set(except.filter((s) => s !== undefined && s.trim() !== ''));
260
272
  const out = new Set();
261
273
  for (const [k, v] of Object.entries(env)) {
262
274
  if (!k.startsWith('OPENWOP_'))
263
275
  continue;
264
276
  if (!/(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/.test(k))
265
277
  continue;
266
- if (v !== undefined && v.trim() !== '')
278
+ if (/_ID$/.test(k))
279
+ continue; // a key's identifier is published, not secret
280
+ if (v !== undefined && v.trim() !== '' && !keep.has(v))
267
281
  out.add(v);
268
282
  }
269
283
  for (const s of extra)
270
- if (s !== undefined && s.trim() !== '')
284
+ if (s !== undefined && s.trim() !== '' && !keep.has(s))
271
285
  out.add(s);
272
286
  out.add(CONFORMANCE_SECRET_CANARY);
273
287
  return [...out];
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "package": "@openwop/spec-artifacts",
3
- "version": "2.0.10",
4
- "stampSha256": "6958747f86a6caa1edac56ec46696255efeff5f5c4e725d275995203276a7058"
3
+ "version": "2.0.11",
4
+ "stampSha256": "73ec5b7c6f2dceeefad0afb5d47446c3566a33ed6123db4bafcb26af38767fa6"
5
5
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "2.0.10",
3
+ "version": "2.0.11",
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.0.10"
59
+ "@openwop/spec-artifacts": "2.0.11"
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": 565,
6
- "tests": 2152,
7
- "withStableId": 2152,
6
+ "tests": 2153,
7
+ "withStableId": 2153,
8
8
  "interpolatedTitles": 0,
9
- "explicitIds": 2107
9
+ "explicitIds": 2108
10
10
  },
11
11
  "records": [
12
12
  {
@@ -7082,9 +7082,26 @@
7082
7082
  ]
7083
7083
  },
7084
7084
  {
7085
- "id": "openwop.it.certification-bundle-redaction.verifybundlev2-rejects-a-bundle-carrying-the-conformance-canary-anywhere-and-acc",
7085
+ "id": "openwop.it.certification-bundle-redaction.evidencesecretsfromenv-never-scrubs-a-key-identifier-id-variables-are-excluded-a",
7086
7086
  "file": "certification-bundle-redaction.test.ts",
7087
7087
  "line": 157,
7088
+ "title": "evidenceSecretsFromEnv never scrubs a key IDENTIFIER: *_ID variables are excluded and `except` values stay visible whichever variable carried them",
7089
+ "explicitId": "openwop.it.certification-bundle-redaction.key-identifier-never-scrubbed",
7090
+ "citations": [
7091
+ {
7092
+ "section": "RFC 0168 §E.2",
7093
+ "requirement": "the keyId a host publishes in discovery.document.signingKeys[] MUST survive evidence scrubbing — a *_ID variable is an identifier, not a secret"
7094
+ },
7095
+ {
7096
+ "section": "RFC 0168 §E.2",
7097
+ "requirement": "a value the emitter names as `except` (the keyId it is about to publish) is never scrubbed, whichever variable carried it"
7098
+ }
7099
+ ]
7100
+ },
7101
+ {
7102
+ "id": "openwop.it.certification-bundle-redaction.verifybundlev2-rejects-a-bundle-carrying-the-conformance-canary-anywhere-and-acc",
7103
+ "file": "certification-bundle-redaction.test.ts",
7104
+ "line": 177,
7088
7105
  "title": "verifyBundleV2 REJECTS a bundle carrying the conformance canary anywhere, and accepts the scrubbed twin",
7089
7106
  "explicitId": "openwop.it.certification-bundle-redaction.verifybundlev2-rejects-a-bundle-carrying-the-conformance-canary-anywhere-and-acc",
7090
7107
  "citations": [
@@ -7097,7 +7114,7 @@
7097
7114
  {
7098
7115
  "id": "openwop.it.certification-bundle-redaction.the-canary-in-a-discovery-document-key-is-still-a-rejection-keys-are-evidence",
7099
7116
  "file": "certification-bundle-redaction.test.ts",
7100
- "line": 172,
7117
+ "line": 192,
7101
7118
  "title": "the canary in a discovery-document KEY is still a rejection (keys are evidence)",
7102
7119
  "explicitId": "openwop.it.certification-bundle-redaction.the-canary-in-a-discovery-document-key-is-still-a-rejection-keys-are-evidence",
7103
7120
  "citations": [
@@ -7110,7 +7127,7 @@
7110
7127
  {
7111
7128
  "id": "openwop.it.certification-bundle-redaction.the-target-configuration-digest-is-a-digest-the-api-key-is-not-a-substring-of-an",
7112
7129
  "file": "certification-bundle-redaction.test.ts",
7113
- "line": 178,
7130
+ "line": 198,
7114
7131
  "title": "the target-configuration digest is a digest: the API key is not a substring of any string in the document",
7115
7132
  "explicitId": "openwop.it.certification-bundle-redaction.the-target-configuration-digest-is-a-digest-the-api-key-is-not-a-substring-of-an",
7116
7133
  "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.0.10",
5
- "corpusTag": "v2.0.10",
4
+ "version": "2.0.11",
5
+ "corpusTag": "v2.0.11",
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": "39081c59fb696159806b0f2f9a42e7e9ff830d622fcf2ad4159b21357580a955",
11
11
  "api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
12
- "api/seams-v2.yaml": "3417edc9197081b8854d60afd4758f84c86c53e2b9686012ed0f764b0c3150a7",
13
- "api/v2/asyncapi.yaml": "bc2c53438cb143d2b98ba96f5470ca182017711715e66b2d00d1b9e797df3c41",
14
- "api/v2/openapi.yaml": "a500508bf79ab2fe651ebc74e5fc5f30aaee2c1a6fc29fb7ae698e761e4c0696",
12
+ "api/seams-v2.yaml": "94727fc973ebe2ed2fbe54dfec1300bc56d23b27f6d5f248672b68926921d64e",
13
+ "api/v2/asyncapi.yaml": "8d8728a51cd6113cabb5c0ee700ddcae4b8232633f86024fcbbba5818d47ccc8",
14
+ "api/v2/openapi.yaml": "aa98fa0c2efc8dfede85f94113e900b4bec5432d3b679b8e574a8d01a061a22e",
15
15
  "api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
16
16
  "schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
17
17
  "schemas/a2a-task-state.schema.json": "c9365918f993f943b4b619d42551eb066a1ed33a08d895d51b395432a5b1f1bc",
@@ -199,7 +199,7 @@
199
199
  "schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
200
200
  "spec/v1/alias-detectors.json": "fee4594ef49953953ffcd0b3813300067d16b3e65ebff2aac722034ac9b3f545",
201
201
  "spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
202
- "spec/v1/core-standard-manifest.json": "c19060310025c9e5622cf5218f54da8e681c1e7dd84a89f7177aaea295d42a6a",
202
+ "spec/v1/core-standard-manifest.json": "ef8d7fe2710c243c6d2451720167d8da0bbbebb6fb0ee64a420a3efdc296d8fe",
203
203
  "spec/v1/deprecations.json": "1d5acb69a9b8ccb57275a95605f74aef1d920685f8407c9d382a46b59dc803bb",
204
204
  "spec/v1/deprecations.schema.json": "18c87e78bedc210431f795ae44c5b5d202f2f3317850d5cf86867d4f1fa1cdfb",
205
205
  "spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
@@ -211,27 +211,27 @@
211
211
  "spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
212
212
  "spec/v1/operation-path-manifest.json": "5f5f4e3842669371730ebbd1aace3fb794192615f0018dc144f484ba5db82ac3",
213
213
  "spec/v1/spec-gaps.json": "6cc9962c6b969f632e07a78b52a4f61447ff579e2990cbae989866f584a86042",
214
- "spec/v2/README.md": "d7af039a68c81af57ac0a91521308f038c71608988edb196a34d9da9d9474f1e",
215
- "spec/v2/core/capabilities.md": "95d1eee8d21e66c0c84591f2cf8bc81438bd39d7419f0ca67a589ec6e6cda4d1",
216
- "spec/v2/core/conformance.md": "c235144f9a83bd5fcc9d15998e480970f5ac05ea9e57fc0036abe3a21f3d2016",
217
- "spec/v2/core/connection-packs.md": "4b3fb729d94842000f0af91e2dcc6f163a910a37de41f3ac07dfcfb03179c3f1",
218
- "spec/v2/core/errors.md": "f2ca092dce5d599111c28fb58638a82cc46807a1cefc38e65df454d3f133334e",
219
- "spec/v2/core/events.md": "6f76e236b345e0c2a20e5b33e8230ebb73b8d20559fcc67a1c47041b6c633f87",
220
- "spec/v2/core/form-content-packs.md": "eb84cda14d567c3d6a81d1806566b7fb20b70cf85bdc534183bf66529e213179",
221
- "spec/v2/core/headers.md": "7e522187bac1de477ede8071d2241f8baca6df0909b20b09b01748e618a33685",
222
- "spec/v2/core/idempotency.md": "ed6981d0021b101c244d2d4d2222d72bf01ceb6e12622c9b720d4e120c29bd47",
223
- "spec/v2/core/identity.md": "ea5a5626aa6189f7cb8949fe5404d213df19f9452b872fa1ed7f1e0449e889d4",
224
- "spec/v2/core/interop.md": "4d3aa4aff637edd4f8ae064ac9fcbabb0d650d55b2a95a3a0b021a2e737df616",
225
- "spec/v2/core/interrupt.md": "e456eb307d43121baf0f1b6c7d514f11f71c1f3fbd0f36ded99d8d804cf8ba49",
226
- "spec/v2/core/overview.md": "7e9ec5f693f59f36139671138050c731abeb1b33eb262abe51554371e0c86991",
227
- "spec/v2/core/packs.md": "c0a98373f5d10a1fbc6d6865d9068125eca8983b9bc5d45780776e010c4cf1b4",
228
- "spec/v2/core/persistence.md": "fdfc2269ebf63972626654a6907c648aa9b231be65a54679930b6279dd12afeb",
229
- "spec/v2/core/replay.md": "793e0256c3d7551e20ccaf9ff51990f1ac9db096508e80ff57dd1bbf9f89f7fb",
230
- "spec/v2/core/runs.md": "7fe674de4c54a9f86f00c4fc0f3ff18ea191a2bdd9b048beafc2b93f97857fcc",
231
- "spec/v2/core/security-defaults.md": "8bd0a3be854310bb7ec49c757e75914a0c9713e94db25193d5c158e1e3e04ac9",
232
- "spec/v2/core/versioning.md": "9da951272cb11a7f66c4f294c8d6dca97408535ea16445cb63f1d9330ae2cdae",
233
- "spec/v2/core/webhooks.md": "7a8a5c5eebe3226b5eacecfb222abd91d2e44a20eb201a15b36a7ffced2e1cde",
234
- "spec/v2/core/workflow-chain-packs.md": "79f5ea4e52020531fd360868c12ebf0640c2e833f6b6fb207091a063130a68e5",
214
+ "spec/v2/README.md": "202d95478ebbc7eb5efd2c3016168054bc16b263085eadb42d132e50d80c2e40",
215
+ "spec/v2/core/capabilities.md": "fee5eb083ec5f6173c57e2673f4d5a264807f71ad00ef8847e51cefc552eea84",
216
+ "spec/v2/core/conformance.md": "dc42444698fdac31bdb8cb9de0c4d36b25ee770158b6e4bab59c67f19560fa35",
217
+ "spec/v2/core/connection-packs.md": "7f767bb12a39e737503625b163e088c432d2095c98a1040ae78044008101234f",
218
+ "spec/v2/core/errors.md": "b46467fe39cf87fb3514ae8d498939a61cd7df306e73eaf5112833a8b6afb740",
219
+ "spec/v2/core/events.md": "2f3aedf07e9267ff559cf48b106d433ce76d7ff1bb6fd5f4ddcaaa9edc3c0cb0",
220
+ "spec/v2/core/form-content-packs.md": "405486e105b6d3b0bf727317af3dfc91af06f727d5bdf8991c71ddeef61d9bae",
221
+ "spec/v2/core/headers.md": "882dfb34febcc669812c7cd1b0c640b6f40fc7a8e806cf49eef8d3ad1b8ec1c3",
222
+ "spec/v2/core/idempotency.md": "220faad1e01c49cd2f5ab4849c5cd276850a11213906de38caba96a72c132320",
223
+ "spec/v2/core/identity.md": "d2c87c8c59ac92cb1df1550afe910d069a21b6967a966c2d54b9c247a15646e9",
224
+ "spec/v2/core/interop.md": "54a0e2c8b4ada51d5fa37f3828777a1f0ccf67120af79fcfbfad4fab9e2fa9ba",
225
+ "spec/v2/core/interrupt.md": "c44bd714563224aa2c3806376a07b265017140be2643d7a3ac340198159ab36d",
226
+ "spec/v2/core/overview.md": "3fef48a6e5b2a6ba01b881d48a3b671d5345c33475d15555dae6946599c25eb9",
227
+ "spec/v2/core/packs.md": "00ea589b627f8027d39e4c99463530129815c973b437c8fbef1ad05d952051bf",
228
+ "spec/v2/core/persistence.md": "682f2bad8c87df8af021e276c02f502e3d1131465d332e960f46e5caa66dedf0",
229
+ "spec/v2/core/replay.md": "ebc4a530e583c31447e95457ca9a3f64e8a51d71ce838a66705b7ae88ee55ab7",
230
+ "spec/v2/core/runs.md": "de5ed935f86edb7f5f3aeb7ae1f714ec1b586786c0096773ef0f58514cbc7edb",
231
+ "spec/v2/core/security-defaults.md": "cf51b04fab31ef82376d08f33b5a62af9014be1e6db7240bf2493c37cf62d360",
232
+ "spec/v2/core/versioning.md": "e98bb5de4261f6fac71e22be9f3a5b5e88060ea2b6d1d88870e4af88b097801b",
233
+ "spec/v2/core/webhooks.md": "a1a32fb8f283f0bbc57cb35cdd5272b79657f1bb97df13a8f8daa232d06e8fc5",
234
+ "spec/v2/core/workflow-chain-packs.md": "004935d6776658d1c33d06fa4fe9aba40efae4a9ca1e5031d759316d3144adaf",
235
235
  "spec/v2/declaration.json": "bd8a1dcee899a4478ac96b52da6ee07e1cb5fce4465939ac32e19e52f4d5c6b2",
236
236
  "spec/v2/declaration.schema.json": "eac5f8080bd572f147bd57d0c4ac8ad73b14536d9236222490ae29a33b21e184",
237
237
  "spec/v2/errors.json": "f179414a92f30b5dadf26e9e57699649a67324bd7186d5287647d17605d137d3",
@@ -268,8 +268,8 @@
268
268
  "spec/v2/path-manifest.json": "034152e09b1458c66810d4050e20a273b2b9b8fe2d92b66e8b819477de58a1be",
269
269
  "spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
270
270
  "spec/v2/profiles.json": "0636f19fceae625390003a347e70ef4797d84766b5c24ce8a02cea52aadebca4",
271
- "spec/v2/release.json": "c866befad1b142734532a7bf987ead39497369ee50678b107c1a0c3f5fea6c6d",
271
+ "spec/v2/release.json": "5e98d2087cd69d3d2a7a6ab011c2f8ee6c86a7e3c98630267b6175d067f23649",
272
272
  "spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
273
273
  },
274
- "corpusCommit": "203d6e8c285fc45875a3f1beca4f4812bb6bbb7e"
274
+ "corpusCommit": "2487cf4f9855684a4e874ec7cd8ad0a80b7b3ca6"
275
275
  }
package/src/cli.ts CHANGED
@@ -663,13 +663,20 @@ async function runCertify(args: ParsedArgs, baseUrl: string, apiKey: string): Pr
663
663
  if (!vk || !vkId) { process.stderr.write('openwop-conformance --certify: --evidence-tier independent needs --verifier-key and --verifier-key-id (RFC 0168 §E.2)\n'); process.exit(2); }
664
664
  v3.verifierSignature = verifierSign(unsigned, vk, vkId);
665
665
  }
666
- const secrets3 = evidenceSecretsFromEnv(process.env, [apiKey, signingKeyPem]);
666
+ // The published keyId is never a secret, whichever variable carried it
667
+ // (evidenceSecretsFromEnv: the `_ID` exclusion is the rule, `except` is
668
+ // the belt for a host that names its env var differently).
669
+ const secrets3 = evidenceSecretsFromEnv(process.env, [apiKey, signingKeyPem], [keyId]);
667
670
  const scrubbed3 = scrubEvidence(v3, secrets3);
668
671
  const v3Out = scrubbed3.value as BundleV3;
669
672
  const audit3 = verifyBundleV3(v3Out, { hostPublicKeyPem: publicKeyFromPrivate(signingKeyPem) });
670
673
  const emitterDefects = audit3.rejections.filter((r) => !['blocked-certified', 'relaxed-profile-certified', 'independent-unverifiable'].includes(r.kind));
671
674
  if (emitterDefects.length > 0) {
672
- process.stderr.write('openwop-conformance --certify: assembled v3 bundle FAILED self-verification (emitter defect):\n' + emitterDefects.map((r) => ` - [${r.kind}] ${r.detail}`).join('\n') + '\n');
675
+ // Keep the rejected bundle: a self-verification failure with nothing to
676
+ // inspect sent a host patching a scratch copy of this CLI to see it.
677
+ const rejectedPath = `${outPath}.rejected.json`;
678
+ writeFileSync(rejectedPath, `${JSON.stringify(v3Out, null, 2)}\n`);
679
+ process.stderr.write('openwop-conformance --certify: assembled v3 bundle FAILED self-verification (emitter defect):\n' + emitterDefects.map((r) => ` - [${r.kind}] ${r.detail}`).join('\n') + `\n the rejected bundle is at ${rejectedPath} (NOT a certification artifact — do not check it in)\n`);
673
680
  process.exit(2);
674
681
  }
675
682
  const v3Schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'certification-bundle.schema.json'), 'utf8')) as Record<string, unknown>;
@@ -342,15 +342,32 @@ export function scrubEvidence<T>(value: T, secrets: readonly string[]): ScrubRes
342
342
  * The secrets a `--certify` run must keep out of its own evidence: the
343
343
  * credential it was handed, plus every `OPENWOP_*` environment value that
344
344
  * names a key/token/secret/password, plus the conformance canary.
345
+ *
346
+ * An *identifier* of a key is not the key. `OPENWOP_BUNDLE_SIGNING_KEY_ID`
347
+ * (and `_VERIFIER_KEY_ID`) name the public `keyId` RFC 0168 §E.2 requires the
348
+ * host to publish in `discovery.document.signingKeys[]`; matching them on
349
+ * `KEY` scrubbed that id out of the embedded document AFTER `discovery.sha256`
350
+ * was taken, so every host that passed the id by env failed its own bundle's
351
+ * self-verification (`discovery.document` ≠ `discovery.sha256`) — suites 2.0.8
352
+ * and 2.0.9, found by MyndHyve 2026-09-10 across three cuts; the same host had
353
+ * passed the day before by passing the id as a flag. Names ending in `_ID`
354
+ * are excluded, and a caller may name values that MUST stay visible (`except`)
355
+ * regardless of which variable carried them.
345
356
  */
346
- export function evidenceSecretsFromEnv(env: NodeJS.ProcessEnv, extra: readonly (string | undefined)[] = []): string[] {
357
+ export function evidenceSecretsFromEnv(
358
+ env: NodeJS.ProcessEnv,
359
+ extra: readonly (string | undefined)[] = [],
360
+ except: readonly (string | undefined)[] = [],
361
+ ): string[] {
362
+ const keep = new Set(except.filter((s): s is string => s !== undefined && s.trim() !== ''));
347
363
  const out = new Set<string>();
348
364
  for (const [k, v] of Object.entries(env)) {
349
365
  if (!k.startsWith('OPENWOP_')) continue;
350
366
  if (!/(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/.test(k)) continue;
351
- if (v !== undefined && v.trim() !== '') out.add(v);
367
+ if (/_ID$/.test(k)) continue; // a key's identifier is published, not secret
368
+ if (v !== undefined && v.trim() !== '' && !keep.has(v)) out.add(v);
352
369
  }
353
- for (const s of extra) if (s !== undefined && s.trim() !== '') out.add(s);
370
+ for (const s of extra) if (s !== undefined && s.trim() !== '' && !keep.has(s)) out.add(s);
354
371
  out.add(CONFORMANCE_SECRET_CANARY);
355
372
  return [...out];
356
373
  }
@@ -154,6 +154,26 @@ describe('RFC 0148 §C — certification-bundle-redaction: secret canaries never
154
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());
155
155
  });
156
156
 
157
+ it('evidenceSecretsFromEnv never scrubs a key IDENTIFIER: *_ID variables are excluded and `except` values stay visible whichever variable carried them', () => {
158
+ // Suites 2.0.8–2.0.10: OPENWOP_BUNDLE_SIGNING_KEY_ID matched on KEY and the
159
+ // published keyId was redacted out of discovery.document after its digest —
160
+ // three MyndHyve cuts failed self-verification on 2026-09-10 for this alone.
161
+ const env = {
162
+ OPENWOP_BUNDLE_SIGNING_KEY: '-----BEGIN PRIVATE KEY-----\nsecret\n-----END PRIVATE KEY-----',
163
+ OPENWOP_BUNDLE_SIGNING_KEY_ID: 'myndhyve-bundle-2026-09',
164
+ OPENWOP_BUNDLE_VERIFIER_KEY_ID: 'steward-verifier-1',
165
+ OPENWOP_API_KEY: 'k1',
166
+ OPENWOP_ODDLY_NAMED_KEY: 'published-key-id-in-a-secret-shaped-variable',
167
+ } as NodeJS.ProcessEnv;
168
+ const secrets = evidenceSecretsFromEnv(env, [], ['published-key-id-in-a-secret-shaped-variable']);
169
+ 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
+ expect(secrets).not.toContain('steward-verifier-1');
171
+ 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');
173
+ expect(secrets.some((s) => s.includes('BEGIN PRIVATE KEY'))).toBe(true);
174
+ expect(secrets).toContain(CONFORMANCE_SECRET_CANARY);
175
+ });
176
+
157
177
  it('verifyBundleV2 REJECTS a bundle carrying the conformance canary anywhere, and accepts the scrubbed twin', () => {
158
178
  const rs = rows();
159
179
  rs[1] = { ...(rs[1] as BundleV2Requirement), detail: `resolved ${CONFORMANCE_SECRET_CANARY} in plain text` };