From 563c22f9bbc9e151f7b718098ad79c51180b62c0 Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 18 Oct 2026 16:47:13 -0500 Subject: [PATCH 1/6] test(closure): UTF16 canonical tuple digest and duplicate/root rejects codegen/closure-semantic.mjs --- codegen/closure-semantic.mjs | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 codegen/closure-semantic.mjs diff --git a/codegen/closure-semantic.mjs b/codegen/closure-semantic.mjs new file mode 100644 index 00000000..d20bb753 --- /dev/null +++ b/codegen/closure-semantic.mjs @@ -0,0 +1,37 @@ +import {createHash} from 'node:crypto'; +const tupleKeys=['ecosystem','registry_uri','package_name','package_version','resolved_revision','artifact_digest','source_digest','outer_checksum']; +const scalar=(v)=>v===undefined||v===null?'':v; +export const utf16Compare=(a,b)=>ab?1:0; +function canonical(value) { + if(Array.isArray(value))return '['+value.map(canonical).join(',')+']'; + if(value && typeof value==='object')return '{'+Object.keys(value).sort(utf16Compare).map(k=>JSON.stringify(k)+':'+canonical(value[k])).join(',')+'}'; + if(typeof value==='number' && !Number.isFinite(value))throw Error('nonfinite'); + return JSON.stringify(value); +} +const key=(d)=>tupleKeys.map(k=>scalar(d[k])); +export function normalizeClosure(closure) { + if(!closure||!Array.isArray(closure.dependencies)||!closure.root)throw Error('invalid_closure'); + const dependencies=closure.dependencies.map(d=>structuredClone(d)); + for(const o of [closure.root,...dependencies]) { + for(const k of Object.keys(o))if(k.endsWith('_digest')||k==='outer_checksum') { + if(typeof o[k]!=='string')throw Error('invalid_digest'); + o[k]=o[k].toLowerCase(); + } + } + const root=structuredClone(closure.root); + for(const k of Object.keys(root))if(k.endsWith('_digest')||k==='outer_checksum')root[k]=root[k].toLowerCase(); + const compare=(a,b)=>{ + const ka=key(a),kb=key(b); + for(let i=0;icompare(d,root)===0))throw Error('root_repeated'); + const out={...closure,root,dependencies}; + delete out.closure_digest; + if(typeof out.dependency_lock_digest==='string')out.dependency_lock_digest=out.dependency_lock_digest.toLowerCase(); + const preimage=canonical(out); + const digest=createHash('sha256').update(Buffer.from(preimage,'utf8')).digest('hex'); + return {preimage,digest,normalized:{...out,closure_digest:digest}}; +} From 1b4a767b17ba67be3cf75cdb14c8bbdb5c53fcaa Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 18 Oct 2026 16:47:15 -0500 Subject: [PATCH 2/6] test(closure): UTF16 canonical tuple digest and duplicate/root rejects codegen/closure-semantic.test.mjs --- codegen/closure-semantic.test.mjs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 codegen/closure-semantic.test.mjs diff --git a/codegen/closure-semantic.test.mjs b/codegen/closure-semantic.test.mjs new file mode 100644 index 00000000..2c26bd6c --- /dev/null +++ b/codegen/closure-semantic.test.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import {test} from 'node:test'; +import {readFileSync} from 'node:fs'; +import {normalizeClosure,utf16Compare} from './closure-semantic.mjs'; +const fixture=JSON.parse(readFileSync(new URL('../validation/package-security/instances/PackageDependencyClosure/valid/hex-closure.json',import.meta.url),'utf8')); +test('UTF-16 collation differs from UTF-8 for astral versus BMP private-use',()=>{ + assert.equal(utf16Compare('\u{10000}','\uE000'),-1); + const a=structuredClone(fixture); + a.dependencies[0].package_name='\uE000'; + a.dependencies[1].package_name='\u{10000}'; + const one=normalizeClosure(a); + const reverse=normalizeClosure({...a,dependencies:[...a.dependencies].reverse()}); + assert.equal(one.digest,reverse.digest); + assert.equal(one.preimage,reverse.preimage); + assert.equal(one.normalized.dependencies[0].package_name,'\u{10000}'); + assert.match(one.digest,/^[0-9a-f]{64}$/); +}); +test('duplicate and root identities fail instead of disappearing',()=>{ + const duplicate={...fixture,dependencies:[fixture.dependencies[0],structuredClone(fixture.dependencies[0])]}; + assert.throws(()=>normalizeClosure(duplicate),/duplicate_dependency/); + const root={...fixture,dependencies:[fixture.root]}; + assert.throws(()=>normalizeClosure(root),/root_repeated/); +}); +test('lock and resolver metadata are in the semantic digest',()=>{ + const a=normalizeClosure(fixture); + assert.notEqual(a.digest,normalizeClosure({...fixture,resolver_version:'9.9.9'}).digest); + assert.notEqual(a.digest,normalizeClosure({...fixture,dependency_lock_digest:'f'.repeat(64)}).digest); +}); From ec8ccc87493b933c18cc2fad4bb188f13ac8fa0e Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 18 Oct 2026 16:57:07 -0500 Subject: [PATCH 3/6] test(closure): replace placeholder checksum with immutable semantic SHA-256 golden --- .../instances/PackageDependencyClosure/valid/hex-closure.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/validation/package-security/instances/PackageDependencyClosure/valid/hex-closure.json b/validation/package-security/instances/PackageDependencyClosure/valid/hex-closure.json index 3502c9d4..4159ea6e 100644 --- a/validation/package-security/instances/PackageDependencyClosure/valid/hex-closure.json +++ b/validation/package-security/instances/PackageDependencyClosure/valid/hex-closure.json @@ -36,7 +36,7 @@ } ], "dependency_lock_digest": "4444444444444444444444444444444444444444444444444444444444444444", - "closure_digest": "5555555555555555555555555555555555555555555555555555555555555555", + "closure_digest": "2f704cfa7c9f59894d97d4b2552e7dec0e6bdee6852a70b1dca8cd50b9e78684", "resolver_id": "zed-hex-resolver", "resolver_version": "1.0.0" } From 17893583bd36df91b892481fb2119b6493b9f430 Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 18 Oct 2026 16:57:20 -0500 Subject: [PATCH 4/6] test(closure): publish exact normalized UTF8 JCS preimage for golden Hex closure --- .../package-security/golden/hex-closure.canonical-preimage.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 validation/package-security/golden/hex-closure.canonical-preimage.json diff --git a/validation/package-security/golden/hex-closure.canonical-preimage.json b/validation/package-security/golden/hex-closure.canonical-preimage.json new file mode 100644 index 00000000..63bf01cd --- /dev/null +++ b/validation/package-security/golden/hex-closure.canonical-preimage.json @@ -0,0 +1 @@ +{"dependencies":[{"artifact_digest":"2222222222222222222222222222222222222222222222222222222222222222","ecosystem":"hex","outer_checksum":"3333333333333333333333333333333333333333333333333333333333333333","package_name":"gleam_json","package_version":"3.0.2","registry_uri":"https://repo.hex.pm","resolved_revision":"3.0.2","source_digest":"1111111111111111111111111111111111111111111111111111111111111111","source_uri":"https://github.com/gleam-lang/json"},{"artifact_digest":"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee","ecosystem":"hex","outer_checksum":"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff","package_name":"gleam_stdlib","package_version":"0.62.1","registry_uri":"https://repo.hex.pm","resolved_revision":"0.62.1","source_digest":"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd","source_uri":"https://github.com/gleam-lang/stdlib"}],"dependency_lock_digest":"4444444444444444444444444444444444444444444444444444444444444444","format":"zed-package-dependency-closure-v1","resolver_id":"zed-hex-resolver","resolver_version":"1.0.0","root":{"artifact_digest":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","ecosystem":"hex","outer_checksum":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","package_name":"beam_app","package_version":"1.0.0","registry_uri":"https://repo.hex.pm","resolved_revision":"1.0.0","source_digest":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","source_uri":"https://github.com/example/beam_app"}} From 9e7b1a9ddcd85148267a4ee37eb78d6970b3e178 Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 18 Oct 2026 16:57:53 -0500 Subject: [PATCH 5/6] test(closure): pin exact normalized preimage and Unicode golden digests --- codegen/closure-semantic.test.mjs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/codegen/closure-semantic.test.mjs b/codegen/closure-semantic.test.mjs index 2c26bd6c..1695a3b2 100644 --- a/codegen/closure-semantic.test.mjs +++ b/codegen/closure-semantic.test.mjs @@ -13,7 +13,7 @@ test('UTF-16 collation differs from UTF-8 for astral versus BMP private-use',()= assert.equal(one.digest,reverse.digest); assert.equal(one.preimage,reverse.preimage); assert.equal(one.normalized.dependencies[0].package_name,'\u{10000}'); - assert.match(one.digest,/^[0-9a-f]{64}$/); + assert.equal(one.digest,'84efe3277da20763fbc5134d9568390493d6b4f7faffd02e4f47e66302af68a4'); }); test('duplicate and root identities fail instead of disappearing',()=>{ const duplicate={...fixture,dependencies:[fixture.dependencies[0],structuredClone(fixture.dependencies[0])]}; @@ -26,3 +26,12 @@ test('lock and resolver metadata are in the semantic digest',()=>{ assert.notEqual(a.digest,normalizeClosure({...fixture,resolver_version:'9.9.9'}).digest); assert.notEqual(a.digest,normalizeClosure({...fixture,dependency_lock_digest:'f'.repeat(64)}).digest); }); + +test('published Hex golden preimage is byte-for-byte JCS and SHA-256 matched',()=>{ + const expected=readFileSync(new URL('../validation/package-security/golden/hex-closure.canonical-preimage.json',import.meta.url),'utf8').trimEnd(); + const receipt=normalizeClosure(fixture); + assert.equal(receipt.digest,'2f704cfa7c9f59894d97d4b2552e7dec0e6bdee6852a70b1dca8cd50b9e78684'); + assert.equal(receipt.digest,fixture.closure_digest); + assert.equal(receipt.preimage,expected); + assert.equal(Buffer.byteLength(receipt.preimage,'utf8'),1550); +}); From 74eb23d26476068b593b2cc25c52525c983e06ff Mon Sep 17 00:00:00 2001 From: "Mr. Denman Mills" Date: Sun, 18 Oct 2026 16:58:17 -0500 Subject: [PATCH 6/6] docs(closure): specify UTF16 collation and immutable golden JCS vector --- validation/package-security/README.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/validation/package-security/README.md b/validation/package-security/README.md index ed00be26..40c30fa5 100644 --- a/validation/package-security/README.md +++ b/validation/package-security/README.md @@ -30,7 +30,7 @@ This lane models evidence and admission receipts; it does not make a scanner or Producers and consumers must recompute `closure_digest`; it is not trusted merely because it is present. The v1 digest is SHA-256 over RFC 8785 JCS serialization of the closure object **without** the `closure_digest` member. Before serialization: 1. normalize hexadecimal digests to lowercase; -2. sort `dependencies` by the tuple `(ecosystem, registry_uri-or-empty, package_name, package_version, resolved_revision, artifact_digest, source_digest, outer_checksum-or-empty)`; +2. sort `dependencies` by the tuple `(ecosystem, registry_uri-or-empty, package_name, package_version, resolved_revision, artifact_digest, source_digest, outer_checksum-or-empty)` using **lexicographic UTF-16 code-unit order** for each string, ascending, with no locale collation or Unicode normalization; for example U+10000 sorts before U+E000 (unlike UTF-8 byte order); 3. reject duplicate tuples rather than silently deduplicating them; 4. require the root artifact to be separate from, and not repeated in, `dependencies`; 5. retain the exact `dependency_lock_digest`, `resolver_id`, and `resolver_version` in the signed/hashed payload. @@ -39,6 +39,19 @@ A consumer must treat a closure as changed if the lock digest, resolver identity For Hex closures, each member carries the registry `outer_checksum` plus independent Zed artifact/source digests. A closure made only from package names and versions is insufficient for safety reuse. + +The authoritative semantic golden vector is +`instances/PackageDependencyClosure/valid/hex-closure.json`: its +`closure_digest` equals SHA-256 +`2f704cfa7c9f59894d97d4b2552e7dec0e6bdee6852a70b1dca8cd50b9e78684`. +The **exact UTF-8 canonical preimage**, without `closure_digest`, is +`golden/hex-closure.canonical-preimage.json` (1550 bytes excluding the +file's one trailing newline). Tests exercise reversed input, duplicate/root +repetition, resolver and lock drift, and an astral-versus-BMP Unicode vector. +These are behavior/conformance fixtures and **not** an additional JSON Schema +or TypeSpec authority. Downstream Rust, Dart and TypeScript implementations +must verify the same bytes and digests independently before promotion. + ## Repository boundary These contracts are portable public validation shapes. They do not contain registry credentials, network policy, deployment topology, database connection details, or executable ORM models.