@aikdna/kdna-core 0.20.0 → 0.22.0

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.
@@ -7,6 +7,7 @@ const crypto = require('crypto');
7
7
  const zlib = require('zlib');
8
8
  const cbor = require('cbor-x');
9
9
  const { loaderVersionUnsupportedMessage } = require('./loader-compatibility');
10
+ const { SIGNATURE_ENTRY_NAME } = require('./signature');
10
11
 
11
12
  const KDNA_MEDIA_TYPE = 'application/vnd.kdna.asset';
12
13
  const ASSET_SOURCE_BYTES = Symbol.for('@aikdna/kdna-core.asset-source-bytes');
@@ -46,10 +47,6 @@ function compareEntryNamesByUtf8(left, right) {
46
47
  return Buffer.compare(Buffer.from(left, 'utf8'), Buffer.from(right, 'utf8'));
47
48
  }
48
49
 
49
- function sortedEntryNames(entries) {
50
- return [...entries.keys()].sort(compareEntryNamesByUtf8);
51
- }
52
-
53
50
  function parseJson(buf, entryName) {
54
51
  try {
55
52
  return JSON.parse(Buffer.isBuffer(buf) ? buf.toString('utf8') : String(buf));
@@ -190,7 +187,6 @@ function normalizeInput(input) {
190
187
 
191
188
  function manifestForDigest(manifest) {
192
189
  const copy = { ...(manifest || {}) };
193
- delete copy.signature;
194
190
  delete copy.asset_digest;
195
191
  delete copy.container_sha256;
196
192
  delete copy.content_digest;
@@ -203,13 +199,23 @@ function manifestForDigest(manifest) {
203
199
  return copy;
204
200
  }
205
201
 
206
- function buildContentDigest(asset) {
202
+ // Entries excluded from the canonical content tree (docs/CANONICALIZATION.md).
203
+ // signature.kdsig can never be part of the content it signs.
204
+ const CONTENT_DIGEST_EXCLUDED_ENTRIES = new Set(['.DS_Store', 'build-receipt.json', 'signature.kdsig']);
205
+
206
+ /**
207
+ * Compute the canonical content digest over a map of entry name → Buffer.
208
+ * Shared by the asset reader and the container signature gate so both always
209
+ * hash the same canonical bytes.
210
+ */
211
+ function contentDigestFromEntryBuffers(buffers) {
212
+ const names = buffers instanceof Map ? [...buffers.keys()] : Object.keys(buffers);
213
+ const readBuffer = (name) => (buffers instanceof Map ? buffers.get(name) : buffers[name]);
207
214
  const parts = [];
208
- const excluded = new Set(['.DS_Store', 'signature.json', 'build-receipt.json']);
209
- for (const entryName of sortedEntryNames(asset.entries)) {
210
- if (excluded.has(entryName)) continue;
215
+ for (const entryName of names.sort(compareEntryNamesByUtf8)) {
216
+ if (CONTENT_DIGEST_EXCLUDED_ENTRIES.has(entryName)) continue;
211
217
  if (entryName.startsWith('reports/')) continue;
212
- const entryBuf = asset.readEntry(entryName);
218
+ const entryBuf = readBuffer(entryName);
213
219
  let digestBuf = entryBuf;
214
220
  if (JSON_ENTRY_RE.test(entryName)) {
215
221
  const parsed = parseJson(entryBuf, entryName);
@@ -221,90 +227,12 @@ function buildContentDigest(asset) {
221
227
  return `sha256:${sha256Hex(Buffer.from(parts.join('\n')))}`;
222
228
  }
223
229
 
224
- function manifestForSignature(manifest, { stripDigestFields = true } = {}) {
225
- // Mirrors manifestForDigest above. The two must agree on every field
226
- // they strip, or the signing and digest paths will hash different
227
- // representations of the same manifest and verifiers will report a
228
- // mismatch on otherwise-valid assets.
229
- //
230
- // Bug: prior version omitted the `authoring.content_digest` strip
231
- // that manifestForDigest performs, so the signing payload included
232
- // a digest field that the digest payload did not.
233
- const copy = { ...(manifest || {}) };
234
- delete copy.signature;
235
- delete copy._source;
236
- if (stripDigestFields) {
237
- delete copy.asset_digest;
238
- delete copy.container_sha256;
239
- delete copy.content_digest;
240
- if (copy.authoring && typeof copy.authoring === 'object') {
241
- const auth = { ...copy.authoring };
242
- delete auth.content_digest;
243
- copy.authoring = auth;
244
- }
245
- }
246
- return copy;
247
- }
248
-
249
- function canonicalJsonEntry(entryName, entryBuf, options = {}) {
250
- const parsed = parseJson(entryBuf, entryName);
251
- const value = entryName === 'kdna.json' ? manifestForSignature(parsed, options) : parsed;
252
- return Buffer.from(stableStringify(value));
253
- }
254
-
255
- function buildSigningPayload(asset, options = {}) {
256
- // Bug (#147): the prior version only excluded `.DS_Store` and
257
- // `signature.json`, while `buildContentDigest` above also excludes
258
- // `build-receipt.json` and any entry under `reports/`. A producer
259
- // that signed with this code and a verifier that digested the
260
- // asset with `buildContentDigest` would compute two different
261
- // byte strings and the signature would fail to verify. The fix
262
- // mirrors `buildContentDigest`'s exclusion set exactly.
263
- const parts = [];
264
- for (const entryName of sortedEntryNames(asset.entries)) {
265
- if (entryName === '.DS_Store' || entryName === 'signature.json') continue;
266
- if (entryName === 'build-receipt.json') continue;
267
- if (entryName.startsWith('reports/')) continue;
268
- const entryBuf = asset.readEntry(entryName);
269
- let payloadBuf = entryBuf;
270
- if (JSON_ENTRY_RE.test(entryName)) {
271
- payloadBuf = canonicalJsonEntry(entryName, entryBuf, options);
272
- }
273
- parts.push(`${entryName}:${sha256Hex(payloadBuf)}`);
274
- }
275
- return parts.join('\n');
276
- }
277
-
278
- function verifySignature(asset, manifest, errors, warnings) {
279
- if (!manifest.signature) {
280
- warnings.push('kdna.json.signature missing');
281
- return null;
282
- }
283
- if (!manifest.author?.public_key_pem) {
284
- errors.push('kdna.json.author.public_key_pem missing');
285
- return false;
286
- }
287
- if (!manifest.author?.pubkey) {
288
- errors.push('kdna.json.author.pubkey missing');
289
- return false;
290
- }
291
-
292
- const fingerprint = `ed25519:${sha256Hex(Buffer.from(manifest.author.public_key_pem))}`;
293
- if (fingerprint !== manifest.author.pubkey) {
294
- errors.push('author.public_key_pem fingerprint does not match author.pubkey');
295
- return false;
296
- }
297
-
298
- try {
299
- const signature = Buffer.from(String(manifest.signature).replace(/^ed25519:/, ''), 'hex');
300
- const publicKey = crypto.createPublicKey(manifest.author.public_key_pem);
301
- const ok = crypto.verify(null, Buffer.from(buildSigningPayload(asset)), publicKey, signature);
302
- if (!ok) errors.push('Ed25519 signature invalid');
303
- return ok;
304
- } catch (e) {
305
- errors.push(`signature verification failed: ${e.message}`);
306
- return false;
230
+ function buildContentDigest(asset) {
231
+ const buffers = new Map();
232
+ for (const entryName of asset.entries.keys()) {
233
+ buffers.set(entryName, asset.readEntry(entryName));
307
234
  }
235
+ return contentDigestFromEntryBuffers(buffers);
308
236
  }
309
237
 
310
238
  function verifyMediaType(asset, errors) {
@@ -464,9 +392,12 @@ function verifySync(asset, options = {}) {
464
392
 
465
393
  // The current implementation is the sole authority for container,
466
394
  // manifest, CBOR payload, checksum, and load-contract validity. The reader
467
- // adds transport identity and legacy signature hooks; it must not maintain a
468
- // second manifest or payload dialect.
395
+ // adds transport identity; it must not maintain a second manifest or payload
396
+ // dialect.
469
397
  appendCurrentValidationErrors(asset, errors);
398
+ if (options.requireSignature === true && !asset.entries.has(SIGNATURE_ENTRY_NAME)) {
399
+ errors.push(`required asset signature is absent: missing entry ${SIGNATURE_ENTRY_NAME}`);
400
+ }
470
401
 
471
402
  if (!asset.entries.has('kdna.json')) errors.push('required entry missing: kdna.json');
472
403
  verifyMediaType(asset, errors);
@@ -484,7 +415,6 @@ function verifySync(asset, options = {}) {
484
415
  }
485
416
 
486
417
  let manifest = null;
487
- let signature_valid = null;
488
418
  if (asset.entries.has('kdna.json')) {
489
419
  try {
490
420
  manifest = readManifest(asset);
@@ -515,9 +445,6 @@ function verifySync(asset, options = {}) {
515
445
  }
516
446
  }
517
447
  }
518
- if (options.requireSignature || manifest.signature) {
519
- signature_valid = verifySignature(asset, manifest, errors, warnings);
520
- }
521
448
  // ── Human Lock signature verification ──────────────────
522
449
  if (asset.entries.has('KDNA_Core.json')) {
523
450
  try {
@@ -540,7 +467,6 @@ function verifySync(asset, options = {}) {
540
467
  manifest,
541
468
  asset_digest,
542
469
  content_digest,
543
- signature_valid,
544
470
  };
545
471
  }
546
472
 
@@ -630,6 +556,9 @@ function createKdnaAssetReader() {
630
556
  const entries = [...asset.entries.keys()].sort();
631
557
 
632
558
  appendCurrentValidationErrors(asset, errors);
559
+ if (options.requireSignature === true && !asset.entries.has(SIGNATURE_ENTRY_NAME)) {
560
+ errors.push(`required asset signature is absent: missing entry ${SIGNATURE_ENTRY_NAME}`);
561
+ }
633
562
 
634
563
  if (!asset.entries.has('kdna.json')) errors.push('required entry missing: kdna.json');
635
564
  verifyMediaType(asset, errors);
@@ -647,7 +576,6 @@ function createKdnaAssetReader() {
647
576
  }
648
577
 
649
578
  let manifest = null;
650
- let signature_valid = null;
651
579
  if (asset.entries.has('kdna.json')) {
652
580
  try {
653
581
  manifest = parseJson(asset.readEntry('kdna.json'), 'kdna.json');
@@ -678,9 +606,6 @@ function createKdnaAssetReader() {
678
606
  }
679
607
  }
680
608
  }
681
- if (options.requireSignature || manifest.signature) {
682
- signature_valid = verifySignature(asset, manifest, errors, warnings);
683
- }
684
609
  } catch (e) {
685
610
  errors.push(e.message);
686
611
  }
@@ -694,7 +619,6 @@ function createKdnaAssetReader() {
694
619
  manifest,
695
620
  asset_digest,
696
621
  content_digest,
697
- signature_valid,
698
622
  };
699
623
  },
700
624
 
@@ -708,5 +632,6 @@ function createKdnaAssetReader() {
708
632
 
709
633
  module.exports = {
710
634
  STANDARD_ENTRIES,
635
+ contentDigestFromEntryBuffers,
711
636
  createKdnaAssetReader,
712
637
  };