@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.
- package/CHANGELOG.md +71 -0
- package/README.md +12 -2
- package/package.json +2 -2
- package/schema/load-contract.schema.json +3 -2
- package/schema/load-plan.schema.json +7 -0
- package/schema/manifest.schema.json +24 -14
- package/schema/payload-profile.schema.json +156 -6
- package/schema/runtime-capsule.schema.json +67 -10
- package/src/asset-reader.js +30 -105
- package/src/container/index.js +550 -44
- package/src/crypto-profile.js +18 -3
- package/src/external-key-grant.js +4 -8
- package/src/index.js +2 -0
- package/src/index.mjs +22 -2
- package/src/public-api.js +55 -13
- package/src/runtime-capsule.js +84 -1
- package/src/signature.js +320 -0
- package/src/types.d.ts +109 -19
package/src/asset-reader.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
209
|
-
|
|
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 =
|
|
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
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
468
|
-
//
|
|
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
|
};
|