@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 +24 -0
- package/dist/cli.js +9 -2
- package/dist/lib/certification-bundle-verify.js +17 -3
- package/dist/spec-artifacts.lock.json +2 -2
- package/package.json +2 -2
- package/requirements.json +23 -6
- package/schemas/CORPUS-STAMP.json +29 -29
- package/src/cli.ts +9 -2
- package/src/lib/certification-bundle-verify.ts +20 -3
- package/src/scenarios/certification-bundle-redaction.test.ts +20 -0
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.0.
|
|
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.
|
|
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":
|
|
7
|
-
"withStableId":
|
|
6
|
+
"tests": 2153,
|
|
7
|
+
"withStableId": 2153,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
|
-
"explicitIds":
|
|
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.
|
|
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":
|
|
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":
|
|
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.
|
|
5
|
-
"corpusTag": "v2.0.
|
|
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": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
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": "
|
|
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": "
|
|
215
|
-
"spec/v2/core/capabilities.md": "
|
|
216
|
-
"spec/v2/core/conformance.md": "
|
|
217
|
-
"spec/v2/core/connection-packs.md": "
|
|
218
|
-
"spec/v2/core/errors.md": "
|
|
219
|
-
"spec/v2/core/events.md": "
|
|
220
|
-
"spec/v2/core/form-content-packs.md": "
|
|
221
|
-
"spec/v2/core/headers.md": "
|
|
222
|
-
"spec/v2/core/idempotency.md": "
|
|
223
|
-
"spec/v2/core/identity.md": "
|
|
224
|
-
"spec/v2/core/interop.md": "
|
|
225
|
-
"spec/v2/core/interrupt.md": "
|
|
226
|
-
"spec/v2/core/overview.md": "
|
|
227
|
-
"spec/v2/core/packs.md": "
|
|
228
|
-
"spec/v2/core/persistence.md": "
|
|
229
|
-
"spec/v2/core/replay.md": "
|
|
230
|
-
"spec/v2/core/runs.md": "
|
|
231
|
-
"spec/v2/core/security-defaults.md": "
|
|
232
|
-
"spec/v2/core/versioning.md": "
|
|
233
|
-
"spec/v2/core/webhooks.md": "
|
|
234
|
-
"spec/v2/core/workflow-chain-packs.md": "
|
|
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": "
|
|
271
|
+
"spec/v2/release.json": "5e98d2087cd69d3d2a7a6ab011c2f8ee6c86a7e3c98630267b6175d067f23649",
|
|
272
272
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624"
|
|
273
273
|
},
|
|
274
|
-
"corpusCommit": "
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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` };
|