@aikdna/kdna-core 0.21.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 +19 -0
- package/README.md +7 -5
- package/package.json +2 -2
- package/schema/load-contract.schema.json +3 -2
- package/schema/load-plan.schema.json +7 -0
- package/schema/runtime-capsule.schema.json +25 -9
- package/src/asset-reader.js +29 -13
- package/src/container/index.js +279 -12
- package/src/index.js +2 -0
- package/src/index.mjs +22 -0
- package/src/public-api.js +55 -1
- package/src/runtime-capsule.js +81 -16
- package/src/signature.js +320 -0
- package/src/types.d.ts +104 -4
package/src/container/index.js
CHANGED
|
@@ -18,8 +18,9 @@
|
|
|
18
18
|
* - mimetype must be the first entry in a .kdna container
|
|
19
19
|
* - mimetype must be STORED (compression method 0) in a .kdna container
|
|
20
20
|
* - the source directory must contain mimetype, kdna.json, payload.kdnab
|
|
21
|
-
* - checksums.json and
|
|
22
|
-
* -
|
|
21
|
+
* - checksums.json, attachments/, and signature.kdsig are optional
|
|
22
|
+
* - signature.kdsig is the RFC-0021 M1 `kdsig.ed25519` bundle; when present
|
|
23
|
+
* it must verify fail-closed against the canonical content digest
|
|
23
24
|
* - lineage must be a single object (not an array)
|
|
24
25
|
* - pack output is reproducible within one pinned toolchain/compressor;
|
|
25
26
|
* DEFLATE bytes may differ across compressors, zlib versions, or systems
|
|
@@ -91,7 +92,7 @@ const { readAsset } = (() => {
|
|
|
91
92
|
|
|
92
93
|
const MIMETYPE = 'application/vnd.kdna.asset';
|
|
93
94
|
const REQUIRED_DIR_ENTRIES = ['mimetype', 'kdna.json', 'payload.kdnab'];
|
|
94
|
-
const OPTIONAL_DIR_ENTRIES = ['checksums.json', 'attachments'];
|
|
95
|
+
const OPTIONAL_DIR_ENTRIES = ['checksums.json', 'attachments', 'signature.kdsig'];
|
|
95
96
|
const ALLOWED_TOP_LEVEL_ENTRIES = new Set([
|
|
96
97
|
...REQUIRED_DIR_ENTRIES,
|
|
97
98
|
...OPTIONAL_DIR_ENTRIES,
|
|
@@ -587,6 +588,13 @@ function readLayout(absPath) {
|
|
|
587
588
|
if (fs.existsSync(full)) {
|
|
588
589
|
if (fs.statSync(full).isFile()) {
|
|
589
590
|
map[f] = fs.readFileSync(full);
|
|
591
|
+
} else if (f === 'attachments') {
|
|
592
|
+
// Attachment files join the entry map under their relative names so
|
|
593
|
+
// digest and signature operations see the same entry set as the
|
|
594
|
+
// packaged container.
|
|
595
|
+
for (const entry of listSourceDir(full)) {
|
|
596
|
+
map[`attachments/${entry.rel}`] = fs.readFileSync(entry.full);
|
|
597
|
+
}
|
|
590
598
|
} else {
|
|
591
599
|
// Optional subdirectories are recorded without becoming payloads.
|
|
592
600
|
map[f] = null;
|
|
@@ -606,7 +614,8 @@ function readLayout(absPath) {
|
|
|
606
614
|
e.name === 'mimetype' ||
|
|
607
615
|
e.name === 'kdna.json' ||
|
|
608
616
|
e.name === 'payload.kdnab' ||
|
|
609
|
-
e.name === 'checksums.json'
|
|
617
|
+
e.name === 'checksums.json' ||
|
|
618
|
+
e.name === 'signature.kdsig'
|
|
610
619
|
) {
|
|
611
620
|
map[e.name] = e.data;
|
|
612
621
|
}
|
|
@@ -663,7 +672,8 @@ function readLayoutBytes(input) {
|
|
|
663
672
|
entry.name === 'mimetype' ||
|
|
664
673
|
entry.name === 'kdna.json' ||
|
|
665
674
|
entry.name === 'payload.kdnab' ||
|
|
666
|
-
entry.name === 'checksums.json'
|
|
675
|
+
entry.name === 'checksums.json' ||
|
|
676
|
+
entry.name === 'signature.kdsig'
|
|
667
677
|
) {
|
|
668
678
|
map[entry.name] = entry.data;
|
|
669
679
|
}
|
|
@@ -811,6 +821,7 @@ function buildInspectOutput(container) {
|
|
|
811
821
|
load_contract_default_profile: m.load_contract ? m.load_contract.default_profile : null,
|
|
812
822
|
};
|
|
813
823
|
if (container.map['checksums.json']) out.checksums_present = true;
|
|
824
|
+
if (container.map['signature.kdsig']) out.signature_present = true;
|
|
814
825
|
return out;
|
|
815
826
|
}
|
|
816
827
|
|
|
@@ -827,6 +838,9 @@ function runValidate(layout) {
|
|
|
827
838
|
schema_valid: true,
|
|
828
839
|
payload_valid: true,
|
|
829
840
|
checksums_valid: true,
|
|
841
|
+
signature_valid: true,
|
|
842
|
+
signature_state: 'absent',
|
|
843
|
+
signature_evidence: null,
|
|
830
844
|
load_contract_valid: true,
|
|
831
845
|
...assessLoaderCompatibility(layout.manifest),
|
|
832
846
|
};
|
|
@@ -981,6 +995,26 @@ function runValidate(layout) {
|
|
|
981
995
|
}
|
|
982
996
|
}
|
|
983
997
|
|
|
998
|
+
// signature gate — RFC-0021 M1 (`kdsig.ed25519`). An absent signature is
|
|
999
|
+
// not an error; a present signature must verify fail-closed.
|
|
1000
|
+
if (layout.map['signature.kdsig'] !== undefined) {
|
|
1001
|
+
try {
|
|
1002
|
+
const signature = require('../signature');
|
|
1003
|
+
const { contentDigestFromEntryBuffers } = require('../asset-reader');
|
|
1004
|
+
const contentDigest = contentDigestFromEntryBuffers(fullEntryBufferMap(layout));
|
|
1005
|
+
const evidence = signature.verifySignatureBundle(
|
|
1006
|
+
layout.map['signature.kdsig'],
|
|
1007
|
+
contentDigest,
|
|
1008
|
+
);
|
|
1009
|
+
result.signature_state = 'verified';
|
|
1010
|
+
result.signature_evidence = evidence;
|
|
1011
|
+
} catch (e) {
|
|
1012
|
+
result.signature_valid = false;
|
|
1013
|
+
result.signature_state = 'invalid';
|
|
1014
|
+
problems.push(`signature: ${e.message}`);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
|
|
984
1018
|
// load_contract gate — only if manifest references a load_contract block
|
|
985
1019
|
if (layout.manifest.load_contract) {
|
|
986
1020
|
const lc = layout.manifest.load_contract;
|
|
@@ -1098,12 +1132,39 @@ function verifyDigests(checksums, map, problems, result) {
|
|
|
1098
1132
|
return stillValid;
|
|
1099
1133
|
}
|
|
1100
1134
|
|
|
1135
|
+
/**
|
|
1136
|
+
* Collect every file entry of a layout as name → Buffer, so digest and
|
|
1137
|
+
* signature operations always cover the full entry set (including
|
|
1138
|
+
* attachments), regardless of which entries layout.map parses.
|
|
1139
|
+
*/
|
|
1140
|
+
function fullEntryBufferMap(layout) {
|
|
1141
|
+
const buffers = {};
|
|
1142
|
+
if (Array.isArray(layout.entries)) {
|
|
1143
|
+
for (const entry of layout.entries) {
|
|
1144
|
+
// Entries are either parsed ZIP entry objects ({ name, data }) or plain
|
|
1145
|
+
// entry names from the unified container model; both resolve through
|
|
1146
|
+
// layout.map.
|
|
1147
|
+
const name = typeof entry === 'string' ? entry : entry.name;
|
|
1148
|
+
const data = typeof entry === 'string' ? layout.map[entry] : entry.data;
|
|
1149
|
+
if (data === undefined || data === null) continue;
|
|
1150
|
+
buffers[name] = data;
|
|
1151
|
+
}
|
|
1152
|
+
return buffers;
|
|
1153
|
+
}
|
|
1154
|
+
for (const [name, data] of Object.entries(layout.map)) {
|
|
1155
|
+
if (data === null || data === undefined) continue;
|
|
1156
|
+
buffers[name] = data;
|
|
1157
|
+
}
|
|
1158
|
+
return buffers;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1101
1161
|
function finalizeValidate(result, problems) {
|
|
1102
1162
|
result.overall_valid =
|
|
1103
1163
|
result.format_valid &&
|
|
1104
1164
|
result.schema_valid &&
|
|
1105
1165
|
result.payload_valid &&
|
|
1106
1166
|
result.checksums_valid &&
|
|
1167
|
+
result.signature_valid &&
|
|
1107
1168
|
result.load_contract_valid;
|
|
1108
1169
|
result.problems = problems;
|
|
1109
1170
|
return result;
|
|
@@ -1260,6 +1321,108 @@ function pack(sourceDir, outputPath) {
|
|
|
1260
1321
|
return { outputPath, entries: order };
|
|
1261
1322
|
}
|
|
1262
1323
|
|
|
1324
|
+
/**
|
|
1325
|
+
* Deterministically pack an in-memory entry map into .kdna container bytes.
|
|
1326
|
+
* mimetype is forced first and STORED; remaining entries are ordered by UTF-8
|
|
1327
|
+
* path bytes and DEFLATE-compressed; timestamps are pinned to the DOS epoch.
|
|
1328
|
+
*/
|
|
1329
|
+
function packEntryMap(map) {
|
|
1330
|
+
for (const f of REQUIRED_DIR_ENTRIES) {
|
|
1331
|
+
if (!map[f]) throw new Error(`cannot pack: missing required entry ${f}`);
|
|
1332
|
+
}
|
|
1333
|
+
const mime = Buffer.isBuffer(map.mimetype) ? map.mimetype.toString('utf8') : String(map.mimetype);
|
|
1334
|
+
if (mime !== MIMETYPE) {
|
|
1335
|
+
throw new Error(`cannot pack: mimetype is "${mime}", expected "${MIMETYPE}"`);
|
|
1336
|
+
}
|
|
1337
|
+
const names = Object.keys(map).filter((name) => name !== 'mimetype');
|
|
1338
|
+
names.sort((a, b) => Buffer.compare(Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8')));
|
|
1339
|
+
const order = ['mimetype', ...names];
|
|
1340
|
+
|
|
1341
|
+
const localChunks = [];
|
|
1342
|
+
const centralChunks = [];
|
|
1343
|
+
let offset = 0;
|
|
1344
|
+
for (const rel of order) {
|
|
1345
|
+
const data = rel === 'mimetype' ? Buffer.from(MIMETYPE, 'utf8') : Buffer.from(map[rel]);
|
|
1346
|
+
const nameBytes = Buffer.from(rel, 'utf8');
|
|
1347
|
+
const method = rel === 'mimetype' ? 0 : 8;
|
|
1348
|
+
const built = buildLocalHeader(nameBytes, data, method);
|
|
1349
|
+
localChunks.push(built.local, built.compressed);
|
|
1350
|
+
centralChunks.push(
|
|
1351
|
+
buildCentral(
|
|
1352
|
+
{
|
|
1353
|
+
method,
|
|
1354
|
+
crc: built.crc,
|
|
1355
|
+
time: built.time,
|
|
1356
|
+
date: built.date,
|
|
1357
|
+
compressed: built.compressed,
|
|
1358
|
+
dataLength: built.dataLength,
|
|
1359
|
+
offset,
|
|
1360
|
+
},
|
|
1361
|
+
nameBytes,
|
|
1362
|
+
),
|
|
1363
|
+
);
|
|
1364
|
+
offset += built.local.length + built.compressed.length;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
const centralOffset = offset;
|
|
1368
|
+
let centralSize = 0;
|
|
1369
|
+
for (const c of centralChunks) centralSize += c.length;
|
|
1370
|
+
const eocd = Buffer.alloc(22);
|
|
1371
|
+
eocd.writeUInt32LE(0x06054b50, 0);
|
|
1372
|
+
eocd.writeUInt16LE(0, 4);
|
|
1373
|
+
eocd.writeUInt16LE(0, 6);
|
|
1374
|
+
eocd.writeUInt16LE(order.length, 8);
|
|
1375
|
+
eocd.writeUInt16LE(order.length, 10);
|
|
1376
|
+
eocd.writeUInt32LE(centralSize, 12);
|
|
1377
|
+
eocd.writeUInt32LE(centralOffset, 16);
|
|
1378
|
+
eocd.writeUInt16LE(0, 20);
|
|
1379
|
+
|
|
1380
|
+
return Buffer.concat([...localChunks, ...centralChunks, eocd]);
|
|
1381
|
+
}
|
|
1382
|
+
|
|
1383
|
+
/**
|
|
1384
|
+
* Sign a packaged .kdna container per RFC-0021 M1 (`kdsig.ed25519`).
|
|
1385
|
+
*
|
|
1386
|
+
* The container must pass every validation gate after any pre-existing
|
|
1387
|
+
* signature entry is removed; signing never certifies a broken asset. The
|
|
1388
|
+
* returned bytes carry a fresh `signature.kdsig` bundle whose Ed25519
|
|
1389
|
+
* signature covers the canonical content digest. Fail-closed throughout.
|
|
1390
|
+
*/
|
|
1391
|
+
function signContainerBytes(input, privateKeySeedHex, _opts = {}) {
|
|
1392
|
+
const signature = require('../signature');
|
|
1393
|
+
const { contentDigestFromEntryBuffers } = require('../asset-reader');
|
|
1394
|
+
|
|
1395
|
+
const bytes = Buffer.isBuffer(input) ? input : Buffer.from(input);
|
|
1396
|
+
const layout = readLayoutBytes(bytes);
|
|
1397
|
+
|
|
1398
|
+
// Rebuild the full entry map (including attachments) from the parsed
|
|
1399
|
+
// ZIP entries; layout.map only carries the well-known protocol entries.
|
|
1400
|
+
const unsignedMap = {};
|
|
1401
|
+
for (const entry of layout.entries) {
|
|
1402
|
+
if (entry.name === 'signature.kdsig') continue;
|
|
1403
|
+
unsignedMap[entry.name] = entry.data;
|
|
1404
|
+
}
|
|
1405
|
+
const validation = runValidate({ ...layout, map: unsignedMap });
|
|
1406
|
+
if (!validation.overall_valid) {
|
|
1407
|
+
const error = new Error(
|
|
1408
|
+
`cannot sign: container does not pass validation (${validation.problems.join('; ')})`,
|
|
1409
|
+
);
|
|
1410
|
+
error.code = 'KDNA_SIGNATURE_INPUT_INVALID';
|
|
1411
|
+
throw error;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
const contentDigest = contentDigestFromEntryBuffers(unsignedMap);
|
|
1415
|
+
const bundle = signature.signContentDigest(contentDigest, privateKeySeedHex);
|
|
1416
|
+
const bundleBytes = signature.serializeSignatureBundle(bundle);
|
|
1417
|
+
const signedMap = { ...unsignedMap, [signature.SIGNATURE_ENTRY_NAME]: bundleBytes };
|
|
1418
|
+
return {
|
|
1419
|
+
containerBytes: packEntryMap(signedMap),
|
|
1420
|
+
bundle,
|
|
1421
|
+
bundleBytes,
|
|
1422
|
+
content_digest: contentDigest,
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1263
1426
|
// ─── unpack ────────────────────────────────────────────────────────────
|
|
1264
1427
|
|
|
1265
1428
|
/**
|
|
@@ -1340,6 +1503,12 @@ function buildLoadPlanIssue(code, severity, message) {
|
|
|
1340
1503
|
|
|
1341
1504
|
function validationProblemCode(problem) {
|
|
1342
1505
|
if (/checksums?:/i.test(problem)) return 'KDNA_INTEGRITY_DIGEST_FAILED';
|
|
1506
|
+
if (/signature bundle profile_version .* is not supported/i.test(problem)) {
|
|
1507
|
+
return 'KDNA_SIGNATURE_VERSION_UNSUPPORTED';
|
|
1508
|
+
}
|
|
1509
|
+
if (/signature bundle (profile|algorithm) .* is not supported/i.test(problem)) {
|
|
1510
|
+
return 'KDNA_SIGNATURE_PROFILE_UNSUPPORTED';
|
|
1511
|
+
}
|
|
1343
1512
|
if (/signature/i.test(problem)) return 'KDNA_INTEGRITY_SIGNATURE_FAILED';
|
|
1344
1513
|
if (/payload:/i.test(problem)) return 'KDNA_FORMAT_INVALID';
|
|
1345
1514
|
if (/manifest:/i.test(problem)) return 'KDNA_FORMAT_INVALID';
|
|
@@ -1413,9 +1582,11 @@ function baseLoadPlan(inputPath, layout, validation, opts = {}) {
|
|
|
1413
1582
|
schema_valid: validation.schema_valid,
|
|
1414
1583
|
payload_valid: validation.payload_valid,
|
|
1415
1584
|
checksums_valid: validation.checksums_valid,
|
|
1585
|
+
signature_valid: validation.signature_valid,
|
|
1416
1586
|
load_contract_valid: validation.load_contract_valid,
|
|
1417
1587
|
overall_valid: validation.overall_valid,
|
|
1418
1588
|
},
|
|
1589
|
+
signature_state: validation.signature_state,
|
|
1419
1590
|
issues: [],
|
|
1420
1591
|
source: {
|
|
1421
1592
|
kind: layout.kind,
|
|
@@ -1481,7 +1652,8 @@ function planLoad(inputPath, opts = {}) {
|
|
|
1481
1652
|
access: null, access_alias: null, entitlement_profile: null,
|
|
1482
1653
|
state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
|
|
1483
1654
|
input_fingerprint: null,
|
|
1484
|
-
checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, load_contract_valid: false, overall_valid: false },
|
|
1655
|
+
checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, signature_valid: false, load_contract_valid: false, overall_valid: false },
|
|
1656
|
+
signature_state: 'absent',
|
|
1485
1657
|
issues: [buildLoadPlanIssue('KDNA_FORMAT_INVALID', 'blocking', e.message)],
|
|
1486
1658
|
source: inputSource(inputPath, 'memory'),
|
|
1487
1659
|
});
|
|
@@ -1505,7 +1677,8 @@ function planLoad(inputPath, opts = {}) {
|
|
|
1505
1677
|
access: null, access_alias: null, entitlement_profile: null,
|
|
1506
1678
|
state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
|
|
1507
1679
|
input_fingerprint: null,
|
|
1508
|
-
checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, load_contract_valid: false, overall_valid: false },
|
|
1680
|
+
checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, signature_valid: false, load_contract_valid: false, overall_valid: false },
|
|
1681
|
+
signature_state: 'absent',
|
|
1509
1682
|
issues: [buildLoadPlanIssue(
|
|
1510
1683
|
e.code === 'KDNA_FORMAT_UNKNOWN' ? 'KDNA_FORMAT_UNKNOWN' : 'KDNA_FORMAT_INVALID',
|
|
1511
1684
|
'blocking', e.message,
|
|
@@ -1526,7 +1699,8 @@ function planLoad(inputPath, opts = {}) {
|
|
|
1526
1699
|
access: null, access_alias: null, entitlement_profile: null,
|
|
1527
1700
|
state: 'invalid', required_action: 'block', can_load_now: false, projection_policy: 'none',
|
|
1528
1701
|
input_fingerprint: null,
|
|
1529
|
-
checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, load_contract_valid: false, overall_valid: false },
|
|
1702
|
+
checks: { format_valid: false, schema_valid: false, payload_valid: false, checksums_valid: false, signature_valid: false, load_contract_valid: false, overall_valid: false },
|
|
1703
|
+
signature_state: 'absent',
|
|
1530
1704
|
issues: [buildLoadPlanIssue('KDNA_FORMAT_INVALID', 'blocking', e.message)],
|
|
1531
1705
|
source: inputSource(inputPath, null),
|
|
1532
1706
|
});
|
|
@@ -1979,6 +2153,8 @@ module.exports = {
|
|
|
1979
2153
|
isKdnaSourceDir,
|
|
1980
2154
|
detectContainerFormat,
|
|
1981
2155
|
readLayout: readLayout,
|
|
2156
|
+
readLayoutBytes,
|
|
2157
|
+
fullEntryBufferMap,
|
|
1982
2158
|
inspect,
|
|
1983
2159
|
validate,
|
|
1984
2160
|
planLoad,
|
|
@@ -1986,6 +2162,8 @@ module.exports = {
|
|
|
1986
2162
|
buildChecksums: buildChecksums,
|
|
1987
2163
|
computeRuntimeEntrySetDigest,
|
|
1988
2164
|
pack,
|
|
2165
|
+
packEntryMap,
|
|
2166
|
+
signContainerBytes,
|
|
1989
2167
|
unpack,
|
|
1990
2168
|
load: loadAsset,
|
|
1991
2169
|
loadAsset: loadAsset,
|
|
@@ -2116,7 +2294,8 @@ function loadAuthorized(inputPath, opts = {}) {
|
|
|
2116
2294
|
extendsChain: plan.extends_chain || [],
|
|
2117
2295
|
_validation: {
|
|
2118
2296
|
schema_valid: plan.checks && plan.checks.overall_valid === true,
|
|
2119
|
-
signature_valid: plan.signature_valid === true,
|
|
2297
|
+
signature_valid: plan.checks && plan.checks.signature_valid === true,
|
|
2298
|
+
signature_state: plan.signature_state,
|
|
2120
2299
|
},
|
|
2121
2300
|
_runtimeAssetBytes: inputSnapshot,
|
|
2122
2301
|
_runtimeInputKind: inputKind,
|
|
@@ -2158,9 +2337,11 @@ function packagedAssetRequiredPlan(inputPath) {
|
|
|
2158
2337
|
schema_valid: false,
|
|
2159
2338
|
payload_valid: false,
|
|
2160
2339
|
checksums_valid: false,
|
|
2340
|
+
signature_valid: false,
|
|
2161
2341
|
load_contract_valid: false,
|
|
2162
2342
|
overall_valid: false,
|
|
2163
2343
|
},
|
|
2344
|
+
signature_state: 'absent',
|
|
2164
2345
|
issues: [buildLoadPlanIssue(
|
|
2165
2346
|
'KDNA_ASSET_FILE_REQUIRED',
|
|
2166
2347
|
'blocking',
|
|
@@ -2259,7 +2440,8 @@ function loadRemoteRuntimeAssetForServer(inputPath) {
|
|
|
2259
2440
|
extendsChain: [],
|
|
2260
2441
|
_validation: {
|
|
2261
2442
|
schema_valid: plan.checks.overall_valid === true,
|
|
2262
|
-
signature_valid: plan.signature_valid === true,
|
|
2443
|
+
signature_valid: plan.checks.signature_valid === true,
|
|
2444
|
+
signature_state: plan.signature_state,
|
|
2263
2445
|
},
|
|
2264
2446
|
_runtimeAssetBytes: inputSnapshot,
|
|
2265
2447
|
_runtimeInputKind: inputKind,
|
|
@@ -2319,6 +2501,27 @@ function hasStandardJudgmentRoleContent(role) {
|
|
|
2319
2501
|
);
|
|
2320
2502
|
}
|
|
2321
2503
|
|
|
2504
|
+
function projectMinimalAxiom(axiom) {
|
|
2505
|
+
const normalized = normalizeCompactAxiom(axiom);
|
|
2506
|
+
if (!normalized) return null;
|
|
2507
|
+
return {
|
|
2508
|
+
type: 'axiom_applicability',
|
|
2509
|
+
id: normalized.id || null,
|
|
2510
|
+
one_sentence: normalized.one_sentence || null,
|
|
2511
|
+
does_not_apply_when: normalized.does_not_apply_when || [],
|
|
2512
|
+
failure_risk: normalized.failure_risk || null,
|
|
2513
|
+
};
|
|
2514
|
+
}
|
|
2515
|
+
|
|
2516
|
+
function hasMinimalPromptContent(content) {
|
|
2517
|
+
if (!content) return false;
|
|
2518
|
+
return Boolean(
|
|
2519
|
+
content.highest_question ||
|
|
2520
|
+
(Array.isArray(content.axioms) && content.axioms.length > 0) ||
|
|
2521
|
+
(Array.isArray(content.boundaries) && content.boundaries.length > 0)
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2322
2525
|
function hasCompactPromptContent(content) {
|
|
2323
2526
|
if (!content) return false;
|
|
2324
2527
|
return Boolean(
|
|
@@ -2566,6 +2769,17 @@ function loadAssetUnsafe(inputPath, opts = {}) {
|
|
|
2566
2769
|
|| (payload.reasoning && ((payload.reasoning.self_check && payload.reasoning.self_check.length > 0) || (payload.reasoning.failure_modes && payload.reasoning.failure_modes.length > 0)));
|
|
2567
2770
|
profiles.push('index');
|
|
2568
2771
|
if (hasJudgment) profiles.push('compact');
|
|
2772
|
+
// RFC-0020: minimal availability follows the manifest declaration, not
|
|
2773
|
+
// payload inference. An asset that declares minimal in load_contract
|
|
2774
|
+
// (and carries core judgment) may be projected minimally.
|
|
2775
|
+
if (
|
|
2776
|
+
hasJudgment &&
|
|
2777
|
+
m.load_contract &&
|
|
2778
|
+
m.load_contract.profiles &&
|
|
2779
|
+
m.load_contract.profiles.minimal
|
|
2780
|
+
) {
|
|
2781
|
+
profiles.push('minimal');
|
|
2782
|
+
}
|
|
2569
2783
|
if (payload.scenarios && payload.scenarios.length > 0) profiles.push('scenario');
|
|
2570
2784
|
profiles.push('full');
|
|
2571
2785
|
return profiles;
|
|
@@ -2657,6 +2871,34 @@ function loadAssetUnsafe(inputPath, opts = {}) {
|
|
|
2657
2871
|
if (m.load_contract && m.load_contract.profiles && m.load_contract.profiles.compact && m.load_contract.profiles.compact.max_tokens_hint) {
|
|
2658
2872
|
result.max_tokens_hint = m.load_contract.profiles.compact.max_tokens_hint;
|
|
2659
2873
|
}
|
|
2874
|
+
} else if (profile === 'minimal') {
|
|
2875
|
+
const core = payload.core || {};
|
|
2876
|
+
// RFC-0020: minimal availability follows the manifest declaration. If the
|
|
2877
|
+
// asset does not declare minimal in load_contract, fail closed with an
|
|
2878
|
+
// empty projection rather than silently serving a projection the asset
|
|
2879
|
+
// did not opt into.
|
|
2880
|
+
if (!result.profile_available) {
|
|
2881
|
+
result.content = {};
|
|
2882
|
+
result.projection_report = buildCompactProjectionReport(payload);
|
|
2883
|
+
} else {
|
|
2884
|
+
const normalizeList = (items) => (items || []).map((item) => {
|
|
2885
|
+
if (typeof item === 'string') return { type: 'text', text: item };
|
|
2886
|
+
if (item && typeof item === 'object') return item;
|
|
2887
|
+
return null;
|
|
2888
|
+
}).filter(Boolean);
|
|
2889
|
+
// RFC-0020: minimal projects the core judgment surface a small model needs:
|
|
2890
|
+
// highest_question + boundary-friendly axioms + full boundaries. It is a
|
|
2891
|
+
// strict subset of compact (deterministic reduction, no inference).
|
|
2892
|
+
result.content = {
|
|
2893
|
+
highest_question: core.highest_question || null,
|
|
2894
|
+
axioms: (core.axioms || []).map(projectMinimalAxiom).filter(Boolean),
|
|
2895
|
+
boundaries: normalizeList(core.boundaries),
|
|
2896
|
+
};
|
|
2897
|
+
result.projection_report = buildCompactProjectionReport(payload);
|
|
2898
|
+
if (m.load_contract && m.load_contract.profiles && m.load_contract.profiles.minimal && m.load_contract.profiles.minimal.max_tokens_hint) {
|
|
2899
|
+
result.max_tokens_hint = m.load_contract.profiles.minimal.max_tokens_hint;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2660
2902
|
} else if (profile === 'scenario') {
|
|
2661
2903
|
result.content = { scenarios: payload.scenarios || [] };
|
|
2662
2904
|
} else if (profile === 'full') {
|
|
@@ -2750,15 +2992,23 @@ function loadAssetUnsafe(inputPath, opts = {}) {
|
|
|
2750
2992
|
const c = result.content;
|
|
2751
2993
|
const compactHasNoPromptContent =
|
|
2752
2994
|
result.profile === 'compact' && !hasCompactPromptContent(c);
|
|
2995
|
+
const minimalHasNoPromptContent =
|
|
2996
|
+
result.profile === 'minimal' && !hasMinimalPromptContent(c);
|
|
2753
2997
|
const legacyProfileHasNoPromptContent =
|
|
2754
2998
|
result.profile !== 'compact' &&
|
|
2999
|
+
result.profile !== 'minimal' &&
|
|
2755
3000
|
c &&
|
|
2756
3001
|
c.scenarios &&
|
|
2757
3002
|
c.scenarios.length === 0 &&
|
|
2758
3003
|
!c.highest_question &&
|
|
2759
3004
|
!(c.axioms && c.axioms.length) &&
|
|
2760
3005
|
!(c.boundaries && c.boundaries.length);
|
|
2761
|
-
if (
|
|
3006
|
+
if (
|
|
3007
|
+
!c ||
|
|
3008
|
+
compactHasNoPromptContent ||
|
|
3009
|
+
minimalHasNoPromptContent ||
|
|
3010
|
+
legacyProfileHasNoPromptContent
|
|
3011
|
+
) {
|
|
2762
3012
|
return {
|
|
2763
3013
|
status: result.status,
|
|
2764
3014
|
profile: result.profile,
|
|
@@ -2850,6 +3100,23 @@ function wrapAsCapsule(result, layout, profile, opts) {
|
|
|
2850
3100
|
return result;
|
|
2851
3101
|
}
|
|
2852
3102
|
|
|
3103
|
+
function buildSignatureEvidence(layout) {
|
|
3104
|
+
if (!layout || !layout.map || layout.map['signature.kdsig'] === undefined) {
|
|
3105
|
+
return { state: 'absent' };
|
|
3106
|
+
}
|
|
3107
|
+
const signature = require('../signature');
|
|
3108
|
+
const { contentDigestFromEntryBuffers } = require('../asset-reader');
|
|
3109
|
+
const contentDigest = contentDigestFromEntryBuffers(fullEntryBufferMap(layout));
|
|
3110
|
+
const evidence = signature.verifySignatureBundle(layout.map['signature.kdsig'], contentDigest);
|
|
3111
|
+
return {
|
|
3112
|
+
state: 'verified',
|
|
3113
|
+
profile: evidence.profile,
|
|
3114
|
+
profile_version: evidence.profile_version,
|
|
3115
|
+
key_fingerprint: evidence.key_fingerprint,
|
|
3116
|
+
content_digest: evidence.content_digest,
|
|
3117
|
+
};
|
|
3118
|
+
}
|
|
3119
|
+
|
|
2853
3120
|
function buildRuntimeCapsuleProjection(loadResult, layout, profile, opts = {}) {
|
|
2854
3121
|
const m = layout.manifest;
|
|
2855
3122
|
const val = opts._validation || {};
|
|
@@ -2861,7 +3128,7 @@ function buildRuntimeCapsuleProjection(loadResult, layout, profile, opts = {}) {
|
|
|
2861
3128
|
projection: loadResult,
|
|
2862
3129
|
manifest: m,
|
|
2863
3130
|
digests,
|
|
2864
|
-
signature:
|
|
3131
|
+
signature: buildSignatureEvidence(layout),
|
|
2865
3132
|
inputKind: opts._runtimeInputKind,
|
|
2866
3133
|
loadedAt: opts.loadedAt,
|
|
2867
3134
|
schemaValid: val.schema_valid === true,
|
package/src/index.js
CHANGED
|
@@ -15,9 +15,11 @@ const runtimeApi = require('./runtime-api');
|
|
|
15
15
|
const runtimeCapsule = require('./runtime-capsule');
|
|
16
16
|
const runtimeContract = require('./runtime-contract');
|
|
17
17
|
const loaderCompatibility = require('./loader-compatibility');
|
|
18
|
+
const assetSignature = require('./signature');
|
|
18
19
|
|
|
19
20
|
module.exports = {
|
|
20
21
|
...publicApi,
|
|
22
|
+
...assetSignature,
|
|
21
23
|
...loader,
|
|
22
24
|
...validate,
|
|
23
25
|
...render,
|
package/src/index.mjs
CHANGED
|
@@ -17,10 +17,14 @@ export {
|
|
|
17
17
|
isKdnaSourceDir,
|
|
18
18
|
detectContainerFormat,
|
|
19
19
|
readLayout,
|
|
20
|
+
readLayoutBytes,
|
|
21
|
+
fullEntryBufferMap,
|
|
20
22
|
inspect,
|
|
21
23
|
validate,
|
|
22
24
|
buildChecksums,
|
|
23
25
|
pack,
|
|
26
|
+
packEntryMap,
|
|
27
|
+
signContainerBytes,
|
|
24
28
|
unpack,
|
|
25
29
|
FORBIDDEN_OUTPUT_TERMS,
|
|
26
30
|
parseSemver,
|
|
@@ -80,8 +84,25 @@ export const verifyDigest = publicApi.verifyDigest;
|
|
|
80
84
|
export const verifyDigestSync = publicApi.verifyDigestSync;
|
|
81
85
|
export const matchDomain = publicApi.matchDomain;
|
|
82
86
|
export const matchDomainSync = publicApi.matchDomainSync;
|
|
87
|
+
export const signKDNA = publicApi.signKDNA;
|
|
88
|
+
export const signKDNASync = publicApi.signKDNASync;
|
|
89
|
+
export const verifyKDNASignature = publicApi.verifyKDNASignature;
|
|
90
|
+
export const verifyKDNASignatureSync = publicApi.verifyKDNASignatureSync;
|
|
83
91
|
export const composeKDNA = publicApi.composeKDNA;
|
|
84
92
|
|
|
93
|
+
import assetSignature from './signature.js';
|
|
94
|
+
export const KDSIG_PROFILE = assetSignature.KDSIG_PROFILE;
|
|
95
|
+
export const KDSIG_PROFILE_VERSION = assetSignature.KDSIG_PROFILE_VERSION;
|
|
96
|
+
export const KDSIG_ALGORITHM = assetSignature.KDSIG_ALGORITHM;
|
|
97
|
+
export const SIGNATURE_ENTRY_NAME = assetSignature.SIGNATURE_ENTRY_NAME;
|
|
98
|
+
export const buildSigningPayload = assetSignature.buildSigningPayload;
|
|
99
|
+
export const generateSigningKeyPair = assetSignature.generateSigningKeyPair;
|
|
100
|
+
export const keyFingerprint = assetSignature.keyFingerprint;
|
|
101
|
+
export const parseSignatureBundle = assetSignature.parseSignatureBundle;
|
|
102
|
+
export const serializeSignatureBundle = assetSignature.serializeSignatureBundle;
|
|
103
|
+
export const signContentDigest = assetSignature.signContentDigest;
|
|
104
|
+
export const verifySignatureBundle = assetSignature.verifySignatureBundle;
|
|
105
|
+
|
|
85
106
|
export const DIGEST_PROFILE = runtimeCapsule.DIGEST_PROFILE;
|
|
86
107
|
export const DIGEST_PROFILE_VERSION = runtimeCapsule.DIGEST_PROFILE_VERSION;
|
|
87
108
|
export const CAPSULE_DIGEST_PROFILE = runtimeCapsule.CAPSULE_DIGEST_PROFILE;
|
|
@@ -121,6 +142,7 @@ export const validateJudgmentTrace = runtimeContract.validateJudgmentTrace;
|
|
|
121
142
|
|
|
122
143
|
export const STANDARD_ENTRIES = assetReader.STANDARD_ENTRIES;
|
|
123
144
|
export const createKdnaAssetReader = assetReader.createKdnaAssetReader;
|
|
145
|
+
export const contentDigestFromEntryBuffers = assetReader.contentDigestFromEntryBuffers;
|
|
124
146
|
|
|
125
147
|
export const {
|
|
126
148
|
LICENSED_ENTRY_PROFILE,
|
package/src/public-api.js
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const fs = require('fs');
|
|
11
|
-
const { createKdnaAssetReader } = require('./asset-reader');
|
|
11
|
+
const { createKdnaAssetReader, contentDigestFromEntryBuffers } = require('./asset-reader');
|
|
12
12
|
const container = require('./container');
|
|
13
13
|
const runtimeApi = require('./runtime-api');
|
|
14
|
+
const signature = require('./signature');
|
|
14
15
|
|
|
15
16
|
function readerFrom(options = {}) {
|
|
16
17
|
return options.reader || createKdnaAssetReader();
|
|
@@ -176,6 +177,55 @@ function matchDomainSync(input, candidates, options = {}) {
|
|
|
176
177
|
return results.sort((a, b) => b.score - a.score || String(a.asset_id).localeCompare(String(b.asset_id)));
|
|
177
178
|
}
|
|
178
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Sign a packaged .kdna asset per RFC-0021 M1 (`kdsig.ed25519`). Returns the
|
|
182
|
+
* signed container bytes, the signature bundle, and the signed content
|
|
183
|
+
* digest. `privateKey` is the raw 32-byte Ed25519 seed as 64 lowercase hex
|
|
184
|
+
* characters. When `options.outputPath` is set the signed container is also
|
|
185
|
+
* written to that path.
|
|
186
|
+
*/
|
|
187
|
+
function signKDNASync(input, privateKey, options = {}) {
|
|
188
|
+
const result = container.signContainerBytes(canonicalBytes(input), privateKey, options);
|
|
189
|
+
if (typeof options.outputPath === 'string' && options.outputPath.length > 0) {
|
|
190
|
+
fs.writeFileSync(options.outputPath, result.containerBytes);
|
|
191
|
+
}
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function signKDNA(input, privateKey, options = {}) {
|
|
196
|
+
return signKDNASync(input, privateKey, options);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Verify the RFC-0021 M1 (`kdsig.ed25519`) signature of a packaged .kdna
|
|
201
|
+
* asset offline. Fail-closed: any malformed, unsupported, or unverifiable
|
|
202
|
+
* bundle throws. Returns `{ state: 'absent' }` for unsigned assets unless
|
|
203
|
+
* `options.required` is true (then absence also throws), and the verified
|
|
204
|
+
* evidence otherwise. `options.expectedPublicKey` pins the signer key.
|
|
205
|
+
*/
|
|
206
|
+
function verifyKDNASignatureSync(input, options = {}) {
|
|
207
|
+
const layout = container.readLayoutBytes(canonicalBytes(input));
|
|
208
|
+
const bundleBytes = layout.map[signature.SIGNATURE_ENTRY_NAME];
|
|
209
|
+
if (bundleBytes === undefined) {
|
|
210
|
+
if (options.required === true) {
|
|
211
|
+
const error = new Error(
|
|
212
|
+
`required asset signature is absent: missing entry ${signature.SIGNATURE_ENTRY_NAME}`,
|
|
213
|
+
);
|
|
214
|
+
error.code = 'KDNA_SIGNATURE_ABSENT';
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
return { state: 'absent' };
|
|
218
|
+
}
|
|
219
|
+
const contentDigest = contentDigestFromEntryBuffers(container.fullEntryBufferMap(layout));
|
|
220
|
+
return signature.verifySignatureBundle(bundleBytes, contentDigest, {
|
|
221
|
+
expectedPublicKey: options.expectedPublicKey,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
async function verifyKDNASignature(input, options = {}) {
|
|
226
|
+
return verifyKDNASignatureSync(input, options);
|
|
227
|
+
}
|
|
228
|
+
|
|
179
229
|
async function composeKDNA(inputs, options = {}) {
|
|
180
230
|
void inputs;
|
|
181
231
|
void options;
|
|
@@ -203,5 +253,9 @@ module.exports = {
|
|
|
203
253
|
verifyDigestSync,
|
|
204
254
|
matchDomain,
|
|
205
255
|
matchDomainSync,
|
|
256
|
+
signKDNA,
|
|
257
|
+
signKDNASync,
|
|
258
|
+
verifyKDNASignature,
|
|
259
|
+
verifyKDNASignatureSync,
|
|
206
260
|
composeKDNA,
|
|
207
261
|
};
|