@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.
@@ -8,6 +8,10 @@ const addFormats = require('ajv-formats');
8
8
 
9
9
  const { createKdnaAssetReader } = require('./asset-reader');
10
10
  const container = require('./container');
11
+ const {
12
+ KDSIG_PROFILE,
13
+ KDSIG_PROFILE_VERSION,
14
+ } = require('./signature');
11
15
  const digestEvidenceSchema = require('../schema/digest-evidence.schema.json');
12
16
  const runtimeCapsuleSchema = require('../schema/runtime-capsule.schema.json');
13
17
 
@@ -433,6 +437,82 @@ function assertSuccessfulDigestEvidence(digests) {
433
437
  }
434
438
  }
435
439
 
440
+ const SIGNATURE_EVIDENCE_FIELDS = Object.freeze({
441
+ absent: ['state'],
442
+ verified: ['state', 'profile', 'profile_version', 'key_fingerprint', 'content_digest'],
443
+ });
444
+
445
+ /**
446
+ * Normalize Runtime Capsule signature evidence (RFC-0021 M1). Unsigned assets
447
+ * report `{ state: 'absent' }`; signed assets report the verified kdsig.ed25519
448
+ * evidence. Any other shape fails closed — signatures never downgrade to
449
+ * "absent" and unknown profiles are never collapsed into known ones.
450
+ */
451
+ function normalizeSignatureEvidence(signature) {
452
+ if (signature === undefined) return { state: 'absent' };
453
+ if (!signature || typeof signature !== 'object' || Array.isArray(signature)) {
454
+ fail(
455
+ 'KDNA_RUNTIME_CAPSULE_BUILD_INVALID',
456
+ 'Runtime Capsule signature evidence must be an object.',
457
+ );
458
+ }
459
+ if (signature.state === 'absent') {
460
+ const extra = Object.keys(signature).filter((key) => key !== 'state');
461
+ if (extra.length) {
462
+ fail(
463
+ 'KDNA_RUNTIME_CAPSULE_BUILD_INVALID',
464
+ 'Absent signature evidence must not carry extra fields.',
465
+ );
466
+ }
467
+ return { state: 'absent' };
468
+ }
469
+ if (signature.state === 'verified') {
470
+ const keys = Object.keys(signature).sort();
471
+ const expected = [...SIGNATURE_EVIDENCE_FIELDS.verified].sort();
472
+ if (keys.join(',') !== expected.join(',')) {
473
+ fail(
474
+ 'KDNA_RUNTIME_CAPSULE_BUILD_INVALID',
475
+ `Verified signature evidence must carry exactly: ${SIGNATURE_EVIDENCE_FIELDS.verified.join(', ')}.`,
476
+ );
477
+ }
478
+ if (signature.profile !== KDSIG_PROFILE) {
479
+ fail(
480
+ 'KDNA_ASSET_SIGNATURE_UNSUPPORTED',
481
+ `Signature profile ${JSON.stringify(signature.profile)} is not supported (supported: ${KDSIG_PROFILE}).`,
482
+ );
483
+ }
484
+ if (signature.profile_version !== KDSIG_PROFILE_VERSION) {
485
+ fail(
486
+ 'KDNA_ASSET_SIGNATURE_UNSUPPORTED',
487
+ `Signature profile_version ${JSON.stringify(signature.profile_version)} is not supported (supported: ${KDSIG_PROFILE_VERSION}).`,
488
+ );
489
+ }
490
+ if (!SHA256_RE.test(signature.key_fingerprint || '')) {
491
+ fail(
492
+ 'KDNA_RUNTIME_CAPSULE_BUILD_INVALID',
493
+ 'Verified signature evidence requires a sha256 key_fingerprint.',
494
+ );
495
+ }
496
+ if (!SHA256_RE.test(signature.content_digest || '')) {
497
+ fail(
498
+ 'KDNA_RUNTIME_CAPSULE_BUILD_INVALID',
499
+ 'Verified signature evidence requires a sha256 content_digest.',
500
+ );
501
+ }
502
+ return {
503
+ state: 'verified',
504
+ profile: signature.profile,
505
+ profile_version: signature.profile_version,
506
+ key_fingerprint: signature.key_fingerprint,
507
+ content_digest: signature.content_digest,
508
+ };
509
+ }
510
+ fail(
511
+ 'KDNA_ASSET_SIGNATURE_UNSUPPORTED',
512
+ `Signature state ${JSON.stringify(signature.state)} is not supported (supported: absent, verified).`,
513
+ );
514
+ }
515
+
436
516
  function normalizeRuntimeAccess(access) {
437
517
  return access === undefined ? 'public' : access;
438
518
  }
@@ -475,22 +555,7 @@ function buildRuntimeCapsule({
475
555
  if (!['public', 'licensed', 'remote'].includes(runtimeAccess)) {
476
556
  fail('KDNA_RUNTIME_CAPSULE_BUILD_INVALID', `Unsupported Runtime access: ${runtimeAccess}.`);
477
557
  }
478
- if (
479
- signature !== undefined &&
480
- (
481
- !signature ||
482
- typeof signature !== 'object' ||
483
- Array.isArray(signature) ||
484
- signature.state !== 'absent' ||
485
- Object.keys(signature).some((key) => key !== 'state')
486
- )
487
- ) {
488
- fail(
489
- 'KDNA_ASSET_SIGNATURE_UNSUPPORTED',
490
- 'Asset signatures are outside the current Preview contract.',
491
- );
492
- }
493
- const signatureEvidence = { state: 'absent' };
558
+ const signatureEvidence = normalizeSignatureEvidence(signature);
494
559
 
495
560
  const capsule = {
496
561
  type: 'kdna.runtime-capsule',
@@ -0,0 +1,320 @@
1
+ /**
2
+ * KDNA asset signatures — RFC-0021 M1 (`kdsig.ed25519`).
3
+ *
4
+ * A `.kdna` container carries an optional top-level `signature.kdsig` entry:
5
+ * a JSON signature bundle whose Ed25519 signature covers a deterministic
6
+ * signing payload derived from the asset's canonical content digest
7
+ * (docs/CANONICALIZATION.md). Verification is offline and fail-closed: any
8
+ * malformed, unsupported, or unverifiable bundle rejects the asset.
9
+ *
10
+ * A signature proves integrity and provenance only. It never proves
11
+ * expertise, truthfulness, safety, or fitness for purpose.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const crypto = require('crypto');
17
+
18
+ const KDSIG_PROFILE = 'kdsig.ed25519';
19
+ const KDSIG_PROFILE_VERSION = '0.1.0';
20
+ const KDSIG_ALGORITHM = 'ed25519';
21
+ const SIGNATURE_ENTRY_NAME = 'signature.kdsig';
22
+
23
+ const RAW_PUBLIC_KEY_HEX_LENGTH = 64;
24
+ const RAW_SIGNATURE_HEX_LENGTH = 128;
25
+ const SHA256_HEX_LENGTH = 64;
26
+
27
+ // RFC 8410 DER prefixes for raw Ed25519 key material.
28
+ const PKCS8_ED25519_PREFIX = Buffer.from('302e020100300506032b657004220420', 'hex');
29
+ const SPKI_ED25519_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
30
+
31
+ const BUNDLE_FIELDS = Object.freeze([
32
+ 'algorithm',
33
+ 'content_digest',
34
+ 'profile',
35
+ 'profile_version',
36
+ 'public_key',
37
+ 'signature',
38
+ ]);
39
+
40
+ function sha256Hex(buf) {
41
+ return crypto.createHash('sha256').update(buf).digest('hex');
42
+ }
43
+
44
+ function signatureError(code, message) {
45
+ const error = new Error(message);
46
+ error.code = code;
47
+ return error;
48
+ }
49
+
50
+ function isLowercaseHex(value, length) {
51
+ return typeof value === 'string' && value.length === length && /^[0-9a-f]+$/.test(value);
52
+ }
53
+
54
+ function assertContentDigestShape(value) {
55
+ if (
56
+ typeof value !== 'string'
57
+ || !value.startsWith('sha256:')
58
+ || !isLowercaseHex(value.slice('sha256:'.length), SHA256_HEX_LENGTH)
59
+ ) {
60
+ throw signatureError(
61
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
62
+ `signature bundle content_digest must be "sha256:<64 lowercase hex>", got ${JSON.stringify(value)}`,
63
+ );
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Build the exact bytes that Ed25519 signs. The payload is domain-separated by
69
+ * the profile coordinate and binds the asset's canonical content digest, so a
70
+ * signature can never be replayed across profiles, versions, or assets.
71
+ */
72
+ function buildSigningPayload(contentDigest) {
73
+ assertContentDigestShape(contentDigest);
74
+ return Buffer.from(`${KDSIG_PROFILE}:${KDSIG_PROFILE_VERSION}:${contentDigest}`, 'utf8');
75
+ }
76
+
77
+ function privateKeyObjectFromSeed(seedHex) {
78
+ if (!isLowercaseHex(seedHex, RAW_PUBLIC_KEY_HEX_LENGTH)) {
79
+ throw signatureError(
80
+ 'KDNA_SIGNATURE_KEY_INVALID',
81
+ 'Ed25519 private key must be a 32-byte seed encoded as 64 lowercase hex characters',
82
+ );
83
+ }
84
+ const der = Buffer.concat([PKCS8_ED25519_PREFIX, Buffer.from(seedHex, 'hex')]);
85
+ try {
86
+ return crypto.createPrivateKey({ key: der, format: 'der', type: 'pkcs8' });
87
+ } catch (e) {
88
+ throw signatureError('KDNA_SIGNATURE_KEY_INVALID', `invalid Ed25519 private key: ${e.message}`);
89
+ }
90
+ }
91
+
92
+ function publicKeyObjectFromRaw(publicKeyHex) {
93
+ if (!isLowercaseHex(publicKeyHex, RAW_PUBLIC_KEY_HEX_LENGTH)) {
94
+ throw signatureError(
95
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
96
+ 'signature bundle public_key must be a 32-byte Ed25519 key encoded as 64 lowercase hex characters',
97
+ );
98
+ }
99
+ const der = Buffer.concat([SPKI_ED25519_PREFIX, Buffer.from(publicKeyHex, 'hex')]);
100
+ try {
101
+ return crypto.createPublicKey({ key: der, format: 'der', type: 'spki' });
102
+ } catch (e) {
103
+ throw signatureError(
104
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
105
+ `signature bundle public_key is not a valid Ed25519 key: ${e.message}`,
106
+ );
107
+ }
108
+ }
109
+
110
+ function rawPublicKeyHexFromObject(keyObject) {
111
+ const spki = keyObject.export({ format: 'der', type: 'spki' });
112
+ return spki.subarray(SPKI_ED25519_PREFIX.length).toString('hex');
113
+ }
114
+
115
+ /**
116
+ * Generate a new Ed25519 signing key pair. The private key is returned as the
117
+ * raw 32-byte seed in lowercase hex; custody is the signer's responsibility.
118
+ */
119
+ function generateSigningKeyPair() {
120
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ed25519');
121
+ const pkcs8 = privateKey.export({ format: 'der', type: 'pkcs8' });
122
+ const seedHex = pkcs8.subarray(PKCS8_ED25519_PREFIX.length).toString('hex');
123
+ return {
124
+ algorithm: KDSIG_ALGORITHM,
125
+ private_key: seedHex,
126
+ public_key: rawPublicKeyHexFromObject(publicKey),
127
+ };
128
+ }
129
+
130
+ /**
131
+ * Fingerprint of a raw Ed25519 public key: sha256 over the 32 key bytes.
132
+ */
133
+ function keyFingerprint(publicKeyHex) {
134
+ if (!isLowercaseHex(publicKeyHex, RAW_PUBLIC_KEY_HEX_LENGTH)) {
135
+ throw signatureError(
136
+ 'KDNA_SIGNATURE_KEY_INVALID',
137
+ 'keyFingerprint expects a 32-byte Ed25519 public key encoded as 64 lowercase hex characters',
138
+ );
139
+ }
140
+ return `sha256:${sha256Hex(Buffer.from(publicKeyHex, 'hex'))}`;
141
+ }
142
+
143
+ function stableStringify(value) {
144
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`;
145
+ if (value && typeof value === 'object') {
146
+ return `{${Object.keys(value)
147
+ .sort()
148
+ .map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
149
+ .join(',')}}`;
150
+ }
151
+ return JSON.stringify(value);
152
+ }
153
+
154
+ /**
155
+ * Serialize a signature bundle to its canonical wire bytes (sorted keys, no
156
+ * insignificant whitespace). Verification accepts any JSON encoding of the
157
+ * same fields; canonical bytes are what signers and vectors pin.
158
+ */
159
+ function serializeSignatureBundle(bundle) {
160
+ return Buffer.from(`${stableStringify(bundle)}\n`, 'utf8');
161
+ }
162
+
163
+ function parseSignatureBundle(bytes) {
164
+ let text;
165
+ try {
166
+ text = Buffer.isBuffer(bytes) ? bytes.toString('utf8') : String(bytes);
167
+ } catch (e) {
168
+ throw signatureError('KDNA_INTEGRITY_SIGNATURE_FAILED', `signature bundle is not UTF-8: ${e.message}`);
169
+ }
170
+ let bundle;
171
+ try {
172
+ bundle = JSON.parse(text);
173
+ } catch (e) {
174
+ throw signatureError(
175
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
176
+ `signature.kdsig is not valid JSON: ${e.message}`,
177
+ );
178
+ }
179
+ if (!bundle || typeof bundle !== 'object' || Array.isArray(bundle)) {
180
+ throw signatureError('KDNA_INTEGRITY_SIGNATURE_FAILED', 'signature bundle must be a JSON object');
181
+ }
182
+ for (const field of BUNDLE_FIELDS) {
183
+ if (typeof bundle[field] !== 'string') {
184
+ throw signatureError(
185
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
186
+ `signature bundle is missing required string field: ${field}`,
187
+ );
188
+ }
189
+ }
190
+ const extra = Object.keys(bundle).filter((key) => !BUNDLE_FIELDS.includes(key));
191
+ if (extra.length) {
192
+ throw signatureError(
193
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
194
+ `signature bundle carries unsupported fields: ${extra.sort().join(', ')}`,
195
+ );
196
+ }
197
+ if (bundle.profile !== KDSIG_PROFILE) {
198
+ throw signatureError(
199
+ 'KDNA_SIGNATURE_PROFILE_UNSUPPORTED',
200
+ `signature bundle profile ${JSON.stringify(bundle.profile)} is not supported (supported: ${KDSIG_PROFILE})`,
201
+ );
202
+ }
203
+ if (bundle.profile_version !== KDSIG_PROFILE_VERSION) {
204
+ throw signatureError(
205
+ 'KDNA_SIGNATURE_VERSION_UNSUPPORTED',
206
+ `signature bundle profile_version ${JSON.stringify(bundle.profile_version)} is not supported (supported: ${KDSIG_PROFILE_VERSION})`,
207
+ );
208
+ }
209
+ if (bundle.algorithm !== KDSIG_ALGORITHM) {
210
+ throw signatureError(
211
+ 'KDNA_SIGNATURE_PROFILE_UNSUPPORTED',
212
+ `signature bundle algorithm ${JSON.stringify(bundle.algorithm)} is not supported (supported: ${KDSIG_ALGORITHM})`,
213
+ );
214
+ }
215
+ assertContentDigestShape(bundle.content_digest);
216
+ if (!isLowercaseHex(bundle.public_key, RAW_PUBLIC_KEY_HEX_LENGTH)) {
217
+ throw signatureError(
218
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
219
+ 'signature bundle public_key must be 64 lowercase hex characters',
220
+ );
221
+ }
222
+ if (!isLowercaseHex(bundle.signature, RAW_SIGNATURE_HEX_LENGTH)) {
223
+ throw signatureError(
224
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
225
+ 'signature bundle signature must be 128 lowercase hex characters',
226
+ );
227
+ }
228
+ return {
229
+ profile: bundle.profile,
230
+ profile_version: bundle.profile_version,
231
+ algorithm: bundle.algorithm,
232
+ content_digest: bundle.content_digest,
233
+ public_key: bundle.public_key,
234
+ signature: bundle.signature,
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Sign a canonical content digest with an Ed25519 seed (64 lowercase hex).
240
+ * Returns the bundle object; serialize with serializeSignatureBundle().
241
+ */
242
+ function signContentDigest(contentDigest, privateKeySeedHex) {
243
+ assertContentDigestShape(contentDigest);
244
+ const privateKey = privateKeyObjectFromSeed(privateKeySeedHex);
245
+ const publicKeyHex = rawPublicKeyHexFromObject(crypto.createPublicKey(privateKey));
246
+ const signatureBytes = crypto.sign(null, buildSigningPayload(contentDigest), privateKey);
247
+ return {
248
+ algorithm: KDSIG_ALGORITHM,
249
+ content_digest: contentDigest,
250
+ profile: KDSIG_PROFILE,
251
+ profile_version: KDSIG_PROFILE_VERSION,
252
+ public_key: publicKeyHex,
253
+ signature: signatureBytes.toString('hex'),
254
+ };
255
+ }
256
+
257
+ /**
258
+ * Verify a parsed bundle against a canonical content digest. Fail-closed:
259
+ * every mismatch throws with a stable error code. Returns verification
260
+ * evidence on success.
261
+ */
262
+ function verifySignatureBundle(bundle, contentDigest, options = {}) {
263
+ const parsed = parseSignatureBundle(bundle);
264
+ assertContentDigestShape(contentDigest);
265
+ if (parsed.content_digest !== contentDigest) {
266
+ throw signatureError(
267
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
268
+ `signature bundle content_digest ${parsed.content_digest} does not match the asset content_digest ${contentDigest}`,
269
+ );
270
+ }
271
+ if (
272
+ typeof options.expectedPublicKey === 'string'
273
+ && options.expectedPublicKey.toLowerCase() !== parsed.public_key
274
+ ) {
275
+ throw signatureError(
276
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
277
+ 'signature bundle public_key does not match the expected pinned key',
278
+ );
279
+ }
280
+ const publicKey = publicKeyObjectFromRaw(parsed.public_key);
281
+ const signatureBytes = Buffer.from(parsed.signature, 'hex');
282
+ let valid;
283
+ try {
284
+ valid = crypto.verify(null, buildSigningPayload(contentDigest), publicKey, signatureBytes);
285
+ } catch (e) {
286
+ throw signatureError(
287
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
288
+ `Ed25519 verification error: ${e.message}`,
289
+ );
290
+ }
291
+ if (valid !== true) {
292
+ throw signatureError(
293
+ 'KDNA_INTEGRITY_SIGNATURE_FAILED',
294
+ 'Ed25519 signature verification failed: signature does not match the canonical signing payload',
295
+ );
296
+ }
297
+ return {
298
+ state: 'verified',
299
+ profile: parsed.profile,
300
+ profile_version: parsed.profile_version,
301
+ algorithm: parsed.algorithm,
302
+ content_digest: parsed.content_digest,
303
+ public_key: parsed.public_key,
304
+ key_fingerprint: keyFingerprint(parsed.public_key),
305
+ };
306
+ }
307
+
308
+ module.exports = {
309
+ KDSIG_PROFILE,
310
+ KDSIG_PROFILE_VERSION,
311
+ KDSIG_ALGORITHM,
312
+ SIGNATURE_ENTRY_NAME,
313
+ buildSigningPayload,
314
+ generateSigningKeyPair,
315
+ keyFingerprint,
316
+ parseSignatureBundle,
317
+ serializeSignatureBundle,
318
+ signContentDigest,
319
+ verifySignatureBundle,
320
+ };
package/src/types.d.ts CHANGED
@@ -743,6 +743,12 @@ export interface KDNAValidationReport {
743
743
  schema_valid: boolean;
744
744
  payload_valid: boolean;
745
745
  checksums_valid: boolean;
746
+ signature_valid: boolean;
747
+ signature_state: 'absent' | 'verified' | 'invalid';
748
+ signature_evidence: (KDNASignatureEvidenceVerified & {
749
+ algorithm: 'ed25519';
750
+ public_key: string;
751
+ }) | null;
746
752
  load_contract_valid: boolean;
747
753
  loader_version: string;
748
754
  min_loader_version: string | null;
@@ -783,6 +789,22 @@ export interface KDNADigestEvidence {
783
789
  runtime_entry_set: KDNADigestValue;
784
790
  }
785
791
 
792
+ export interface KDNASignatureEvidenceAbsent {
793
+ state: 'absent';
794
+ }
795
+
796
+ export interface KDNASignatureEvidenceVerified {
797
+ state: 'verified';
798
+ profile: 'kdsig.ed25519';
799
+ profile_version: '0.1.0';
800
+ key_fingerprint: string;
801
+ content_digest: string;
802
+ }
803
+
804
+ export type KDNASignatureEvidence =
805
+ | KDNASignatureEvidenceAbsent
806
+ | KDNASignatureEvidenceVerified;
807
+
786
808
  export interface KDNARuntimeCapsule {
787
809
  type: 'kdna.runtime-capsule';
788
810
  contract_version: '0.1.0';
@@ -793,7 +815,7 @@ export interface KDNARuntimeCapsule {
793
815
  judgment_version: string;
794
816
  };
795
817
  digests: KDNADigestEvidence;
796
- signature: { state: 'absent' };
818
+ signature: KDNASignatureEvidence;
797
819
  access: 'public' | 'licensed' | 'remote';
798
820
  profile: 'index' | 'compact' | 'scenario' | 'full';
799
821
  context: Record<string, any>;
@@ -804,7 +826,7 @@ export interface KDNARuntimeCapsule {
804
826
  input_kind: 'packaged_file' | 'packaged_bytes';
805
827
  runtime_eligible: true;
806
828
  schema_valid: true;
807
- signature_state: 'absent';
829
+ signature_state: 'absent' | 'verified';
808
830
  profile: 'index' | 'compact' | 'scenario' | 'full';
809
831
  projection_report?: {
810
832
  status: 'complete' | 'partial';
@@ -852,11 +874,72 @@ export function buildRuntimeCapsule(input: {
852
874
  projection: Record<string, any>;
853
875
  manifest: KDNAManifest;
854
876
  digests: KDNADigestEvidence;
855
- signature?: { state: 'absent' };
877
+ signature?: KDNASignatureEvidence;
856
878
  inputKind: 'packaged_file' | 'packaged_bytes';
857
879
  loadedAt?: string;
858
880
  schemaValid?: boolean;
859
881
  }): KDNARuntimeCapsule;
882
+
883
+ export const KDSIG_PROFILE: 'kdsig.ed25519';
884
+ export const KDSIG_PROFILE_VERSION: '0.1.0';
885
+ export const KDSIG_ALGORITHM: 'ed25519';
886
+ export const SIGNATURE_ENTRY_NAME: 'signature.kdsig';
887
+
888
+ export interface KDNASignatureBundle {
889
+ algorithm: 'ed25519';
890
+ content_digest: string;
891
+ profile: 'kdsig.ed25519';
892
+ profile_version: '0.1.0';
893
+ public_key: string;
894
+ signature: string;
895
+ }
896
+
897
+ export function buildSigningPayload(contentDigest: string): Uint8Array;
898
+ export function generateSigningKeyPair(): {
899
+ algorithm: 'ed25519';
900
+ private_key: string;
901
+ public_key: string;
902
+ };
903
+ export function keyFingerprint(publicKeyHex: string): string;
904
+ export function parseSignatureBundle(bytes: Uint8Array | string): KDNASignatureBundle;
905
+ export function serializeSignatureBundle(bundle: KDNASignatureBundle): Uint8Array;
906
+ export function signContentDigest(
907
+ contentDigest: string,
908
+ privateKeySeedHex: string,
909
+ ): KDNASignatureBundle;
910
+ export function verifySignatureBundle(
911
+ bundle: Uint8Array | string,
912
+ contentDigest: string,
913
+ options?: { expectedPublicKey?: string },
914
+ ): KDNASignatureEvidenceVerified & { algorithm: 'ed25519'; public_key: string };
915
+ export function signKDNA(
916
+ input: string | Uint8Array,
917
+ privateKey: string,
918
+ options?: { outputPath?: string },
919
+ ): Promise<{
920
+ containerBytes: Uint8Array;
921
+ bundle: KDNASignatureBundle;
922
+ bundleBytes: Uint8Array;
923
+ content_digest: string;
924
+ }>;
925
+ export function signKDNASync(
926
+ input: string | Uint8Array,
927
+ privateKey: string,
928
+ options?: { outputPath?: string },
929
+ ): {
930
+ containerBytes: Uint8Array;
931
+ bundle: KDNASignatureBundle;
932
+ bundleBytes: Uint8Array;
933
+ content_digest: string;
934
+ };
935
+ export function verifyKDNASignature(
936
+ input: string | Uint8Array,
937
+ options?: { required?: boolean; expectedPublicKey?: string },
938
+ ): Promise<KDNASignatureEvidence>;
939
+ export function verifyKDNASignatureSync(
940
+ input: string | Uint8Array,
941
+ options?: { required?: boolean; expectedPublicKey?: string },
942
+ ): KDNASignatureEvidence;
860
943
  export function loadRuntimeCapsule(
861
944
  input: string | Uint8Array,
862
945
  options?: {
@@ -1360,6 +1443,11 @@ export interface KDNALayout {
1360
1443
  }
1361
1444
 
1362
1445
  export function readLayout(inputPath: string): KDNALayout;
1446
+ export function readLayoutBytes(input: Uint8Array): KDNALayout;
1447
+ export function fullEntryBufferMap(layout: KDNALayout): Record<string, Uint8Array>;
1448
+ export function contentDigestFromEntryBuffers(
1449
+ buffers: Map<string, Uint8Array> | Record<string, Uint8Array>,
1450
+ ): string;
1363
1451
 
1364
1452
  export interface KDNAChecksumEntry {
1365
1453
  algorithm: 'sha256';
@@ -1383,7 +1471,7 @@ export function inspect(
1383
1471
  input: string | Uint8Array,
1384
1472
  options?: Record<string, any>,
1385
1473
  ): Record<string, any>;
1386
- export const KDNA_LOADER_VERSION: '0.21.0';
1474
+ export const KDNA_LOADER_VERSION: '0.22.0';
1387
1475
  export const STRICT_LOADER_VERSION: RegExp;
1388
1476
  export function parseLoaderVersion(version: unknown): readonly [string, string, string] | null;
1389
1477
  export function compareLoaderVersions(left: string, right: string): -1 | 0 | 1;
@@ -1444,6 +1532,7 @@ export interface KDNALoadPlan {
1444
1532
  source_fingerprint: string;
1445
1533
  } | null;
1446
1534
  checks: Record<string, boolean>;
1535
+ signature_state: 'absent' | 'verified' | 'invalid';
1447
1536
  issues: KDNALoadPlanIssue[];
1448
1537
  source: {
1449
1538
  kind: 'dir' | 'file' | 'memory' | string | null;
@@ -1460,6 +1549,17 @@ export function planLoad(
1460
1549
  ): KDNALoadPlan;
1461
1550
  export function buildChecksums(sourceDir: string): KDNAChecksums;
1462
1551
  export function pack(sourceDir: string, outputPath: string): void;
1552
+ export function packEntryMap(map: Record<string, Uint8Array>): Uint8Array;
1553
+ export function signContainerBytes(
1554
+ input: Uint8Array,
1555
+ privateKey: string,
1556
+ options?: Record<string, any>,
1557
+ ): {
1558
+ containerBytes: Uint8Array;
1559
+ bundle: KDNASignatureBundle;
1560
+ bundleBytes: Uint8Array;
1561
+ content_digest: string;
1562
+ };
1463
1563
  export function unpack(inputPath: string, outputDir: string): void;
1464
1564
  export function loadAuthorized(
1465
1565
  input: string | Uint8Array,