@blamejs/blamejs-shop 0.3.40 → 0.3.42
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 +4 -0
- package/lib/asset-manifest.json +1 -1
- package/lib/storefront.js +14 -3
- package/lib/vendor/MANIFEST.json +2 -2
- package/lib/vendor/blamejs/CHANGELOG.md +8 -0
- package/lib/vendor/blamejs/README.md +8 -5
- package/lib/vendor/blamejs/SECURITY.md +7 -0
- package/lib/vendor/blamejs/api-snapshot.json +266 -2
- package/lib/vendor/blamejs/examples/wiki/lib/source-comment-block-validator.js +1 -0
- package/lib/vendor/blamejs/index.js +7 -1
- package/lib/vendor/blamejs/lib/agent-idempotency.js +113 -0
- package/lib/vendor/blamejs/lib/agent-orchestrator.js +108 -0
- package/lib/vendor/blamejs/lib/agent-snapshot.js +137 -0
- package/lib/vendor/blamejs/lib/agent-tenant.js +193 -17
- package/lib/vendor/blamejs/lib/ai-input.js +167 -3
- package/lib/vendor/blamejs/lib/ai-output.js +463 -0
- package/lib/vendor/blamejs/lib/ai-prompt.js +304 -0
- package/lib/vendor/blamejs/lib/archive-wrap.js +234 -1
- package/lib/vendor/blamejs/lib/archive.js +1 -0
- package/lib/vendor/blamejs/lib/audit.js +3 -0
- package/lib/vendor/blamejs/lib/auth/oid4vp.js +47 -28
- package/lib/vendor/blamejs/lib/cluster.js +186 -14
- package/lib/vendor/blamejs/lib/codepoint-class.js +18 -0
- package/lib/vendor/blamejs/lib/compliance-ai-act.js +446 -0
- package/lib/vendor/blamejs/lib/consent.js +104 -8
- package/lib/vendor/blamejs/lib/content-credentials.js +851 -41
- package/lib/vendor/blamejs/lib/crypto-field.js +5 -0
- package/lib/vendor/blamejs/lib/db.js +15 -0
- package/lib/vendor/blamejs/lib/framework-error.js +21 -0
- package/lib/vendor/blamejs/lib/mail-srs.js +122 -19
- package/lib/vendor/blamejs/lib/privacy.js +168 -0
- package/lib/vendor/blamejs/lib/safe-archive.js +196 -136
- package/lib/vendor/blamejs/lib/validate-opts.js +24 -0
- package/lib/vendor/blamejs/lib/vault/rotate.js +175 -15
- package/lib/vendor/blamejs/lib/vault-aad.js +84 -33
- package/lib/vendor/blamejs/package.json +1 -1
- package/lib/vendor/blamejs/release-notes/v0.14.11.json +72 -0
- package/lib/vendor/blamejs/release-notes/v0.14.12.json +95 -0
- package/lib/vendor/blamejs/release-notes/v0.14.13.json +52 -0
- package/lib/vendor/blamejs/release-notes/v0.14.14.json +31 -0
- package/lib/vendor/blamejs/test/00-primitives.js +9 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-idempotency.test.js +103 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-orchestrator.test.js +91 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-snapshot.test.js +186 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/agent-tenant.test.js +140 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-input.test.js +59 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-output.test.js +125 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/ai-prompt.test.js +133 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/app-shutdown.test.js +6 -1
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-read.test.js +94 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/archive-wrap.test.js +176 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/cluster-vault-rotation.test.js +243 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/codebase-patterns.test.js +250 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/compliance-ai-act.test.js +130 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/consent-purposes.test.js +70 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/content-credentials.test.js +289 -2
- package/lib/vendor/blamejs/test/layer-0-primitives/federation-vc-suite.test.js +22 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/mail-srs.test.js +61 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/privacy-vendor-review.test.js +69 -0
- package/lib/vendor/blamejs/test/layer-0-primitives/vault-rotate-aad.test.js +158 -0
- package/package.json +1 -1
|
@@ -56,6 +56,7 @@ var safeSql = require("../safe-sql");
|
|
|
56
56
|
var C = require("../constants");
|
|
57
57
|
var cryptoField = require("../crypto-field");
|
|
58
58
|
var bCrypto = require("../crypto");
|
|
59
|
+
var vaultAad = require("../vault-aad");
|
|
59
60
|
var dbSchema = require("../db-schema");
|
|
60
61
|
var lazyRequire = require("../lazy-require");
|
|
61
62
|
var { boot } = require("../log");
|
|
@@ -63,6 +64,23 @@ var numericBounds = require("../numeric-bounds");
|
|
|
63
64
|
var safeJson = require("../safe-json");
|
|
64
65
|
var validateOpts = require("../validate-opts");
|
|
65
66
|
var vaultWrap = lazyRequire(function () { return require("./wrap"); });
|
|
67
|
+
// lazyRequire (named dbModuleLazy to match the canonical binding in
|
|
68
|
+
// lib/backup/index.js and to avoid shadowing the local SQLite handle `db`
|
|
69
|
+
// inside rotate()): the db at-rest AAD constructors live in lib/db.js.
|
|
70
|
+
var dbModuleLazy = lazyRequire(function () { return require("../db"); });
|
|
71
|
+
// Framework AAD modules whose stores live outside db.enc — lazyRequire'd
|
|
72
|
+
// at top-of-file (deferred, never inline in a function body) so rotate's
|
|
73
|
+
// detect-and-refuse can read each module's AAD_ROTATION descriptor without
|
|
74
|
+
// eagerly loading them at require time.
|
|
75
|
+
var agentIdempotencyLazy = lazyRequire(function () { return require("../agent-idempotency"); });
|
|
76
|
+
var agentOrchestratorLazy = lazyRequire(function () { return require("../agent-orchestrator"); });
|
|
77
|
+
var agentTenantLazy = lazyRequire(function () { return require("../agent-tenant"); });
|
|
78
|
+
var agentSnapshotLazy = lazyRequire(function () { return require("../agent-snapshot"); });
|
|
79
|
+
// Tenant archive blobs (recipient: "tenant") are keyed off the vault root but
|
|
80
|
+
// live in operator-placed storage (files / object stores / backups) the
|
|
81
|
+
// rotation pipeline never walks, so archive-wrap exports the same external
|
|
82
|
+
// AAD_ROTATION descriptor and must be gated here too.
|
|
83
|
+
var archiveWrapLazy = lazyRequire(function () { return require("../archive-wrap"); });
|
|
66
84
|
var { defineClass } = require("../framework-error");
|
|
67
85
|
|
|
68
86
|
var rotateLog = boot("vault-rotate");
|
|
@@ -254,6 +272,10 @@ function verify(opts) {
|
|
|
254
272
|
var keys = opts.keys;
|
|
255
273
|
var db = opts.db;
|
|
256
274
|
var oldKeys = opts.oldKeys || null;
|
|
275
|
+
// Serialized roots for AAD-cell verification — match getKeysJson() so an
|
|
276
|
+
// AAD cell sealed under the new root opens here.
|
|
277
|
+
var keysJson = JSON.stringify(keys, null, 2);
|
|
278
|
+
var oldKeysJson = oldKeys ? JSON.stringify(oldKeys, null, 2) : null;
|
|
257
279
|
numericBounds.requirePositiveFiniteIntIfPresent(opts.sampleMin,
|
|
258
280
|
"verify: sampleMin", VaultRotateError, "vault-rotate/bad-opt");
|
|
259
281
|
var sampleMin = opts.sampleMin !== undefined
|
|
@@ -308,7 +330,29 @@ function verify(opts) {
|
|
|
308
330
|
for (var sf = 0; sf < schema.sealedFields.length; sf++) {
|
|
309
331
|
var col = schema.sealedFields[sf];
|
|
310
332
|
var v = row[col];
|
|
311
|
-
if (typeof v !== "string"
|
|
333
|
+
if (typeof v !== "string") continue;
|
|
334
|
+
|
|
335
|
+
if (vaultAad.isAadSealed(v)) {
|
|
336
|
+
// AAD cell: reconstruct the seal-side AAD (cryptoField._aadParts)
|
|
337
|
+
// and verify under the new root; flag a regression if it still
|
|
338
|
+
// opens under the old root (rotation didn't take effect).
|
|
339
|
+
var aad = cryptoField._aadParts(schema, table, col, row);
|
|
340
|
+
try { vaultAad.unsealRoot(v, aad, keysJson); }
|
|
341
|
+
catch (e) {
|
|
342
|
+
rowFailed = true;
|
|
343
|
+
failures.push({ table: table, column: col, _id: row._id, error: (e && e.message) || String(e) });
|
|
344
|
+
}
|
|
345
|
+
if (oldKeysJson && !foundOldFail) {
|
|
346
|
+
try {
|
|
347
|
+
vaultAad.unsealRoot(v, aad, oldKeysJson);
|
|
348
|
+
regressions.push({ table: table, column: col, _id: row._id,
|
|
349
|
+
error: "old keys still decrypt this AAD value — rotation did not take effect" });
|
|
350
|
+
} catch (_e) { foundOldFail = true; }
|
|
351
|
+
}
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (v.indexOf(VAULT_PREFIX) !== 0) continue;
|
|
312
356
|
var payload = v.substring(VAULT_PREFIX.length);
|
|
313
357
|
|
|
314
358
|
try { bCrypto.decrypt(payload, keys); }
|
|
@@ -379,6 +423,41 @@ function verify(opts) {
|
|
|
379
423
|
var ROW_BATCH_SIZE_DEFAULT = 0x3E8;
|
|
380
424
|
var VAULT_PREFIX_LEN = C.VAULT_PREFIX.length;
|
|
381
425
|
|
|
426
|
+
// db.enc / db.key.enc AAD constructors come from the module that OWNS the
|
|
427
|
+
// db at-rest format (lib/db.js _dbEncAad / _dbKeyAad), not re-declared
|
|
428
|
+
// here — one source of truth for the wire-format literals so a rotation
|
|
429
|
+
// re-seal binds the SAME deployment AAD db.init expects on next open.
|
|
430
|
+
|
|
431
|
+
// Framework modules that seal AAD cells on operator-supplied (external)
|
|
432
|
+
// stores this pipeline never reaches (it only walks db.enc). Each exports
|
|
433
|
+
// an AAD_ROTATION descriptor + a reseal hook to rotate its store
|
|
434
|
+
// out-of-band. rotate() REFUSES a keypair rotation unless the operator
|
|
435
|
+
// acknowledges (opts.externalAadResealed) each has been re-sealed —
|
|
436
|
+
// otherwise those cells silently orphan under the retired root (CWE-320).
|
|
437
|
+
// Only the module PATHS live here; the table / backend metadata lives in
|
|
438
|
+
// each module's AAD_ROTATION export (single source of truth). lazyRequire
|
|
439
|
+
// so loading rotate.js doesn't eagerly pull the agent modules.
|
|
440
|
+
var EXTERNAL_AAD_MODULE_LOADERS = [
|
|
441
|
+
agentIdempotencyLazy, agentOrchestratorLazy, agentTenantLazy, agentSnapshotLazy,
|
|
442
|
+
archiveWrapLazy,
|
|
443
|
+
];
|
|
444
|
+
|
|
445
|
+
function _externalAadTables() {
|
|
446
|
+
var tables = [];
|
|
447
|
+
for (var i = 0; i < EXTERNAL_AAD_MODULE_LOADERS.length; i += 1) {
|
|
448
|
+
var mod;
|
|
449
|
+
try { mod = EXTERNAL_AAD_MODULE_LOADERS[i](); }
|
|
450
|
+
catch (_e) { continue; } // module unavailable in this process — skip
|
|
451
|
+
var desc = mod && mod.AAD_ROTATION;
|
|
452
|
+
if (!desc) continue;
|
|
453
|
+
var list = Array.isArray(desc) ? desc : [desc];
|
|
454
|
+
for (var j = 0; j < list.length; j += 1) {
|
|
455
|
+
if (list[j] && list[j].backend === "external" && list[j].table) tables.push(list[j].table);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return tables;
|
|
459
|
+
}
|
|
460
|
+
|
|
382
461
|
function _emit(cb, ev) {
|
|
383
462
|
if (typeof cb === "function") {
|
|
384
463
|
try { cb(ev); } catch (_e) { /* progress-callback errors are non-fatal */ }
|
|
@@ -444,7 +523,7 @@ function _walkAndReSeal(node, oldKeys, newKeys) {
|
|
|
444
523
|
|
|
445
524
|
function _runStmt(db, sql) { db.prepare(sql).run(); }
|
|
446
525
|
|
|
447
|
-
function _rotateColumn(db, table, column,
|
|
526
|
+
function _rotateColumn(db, table, column, schema, roots, batchSize, progress) {
|
|
448
527
|
// Identifiers reach SQL through safeSql.quoteIdentifier — runs
|
|
449
528
|
// validateIdentifier (rejects bad shape / reserved words /
|
|
450
529
|
// sqlite_-prefix) + emits the dialect-correct quoted form.
|
|
@@ -453,8 +532,18 @@ function _rotateColumn(db, table, column, oldKeys, newKeys, batchSize, progress)
|
|
|
453
532
|
var total = db.prepare("SELECT COUNT(*) AS n FROM " + qt + " WHERE " + qc + " IS NOT NULL").get().n;
|
|
454
533
|
if (total === 0) return 0;
|
|
455
534
|
|
|
535
|
+
// AAD-bound tables (registerTable({aad:true})) seal each cell under a
|
|
536
|
+
// (table, rowId, column, schemaVersion) tuple. Rotation reads the
|
|
537
|
+
// rowIdField value and reconstructs the IDENTICAL AAD via the seal-side
|
|
538
|
+
// builder cryptoField._aadParts (one source of truth), then re-seals
|
|
539
|
+
// old-root -> new-root. Plain (non-AAD) cells use the plain-vault reseal.
|
|
540
|
+
var aadMode = !!(schema && schema.aad);
|
|
541
|
+
var rowIdField = aadMode ? schema.rowIdField : null;
|
|
542
|
+
var needRid = aadMode && rowIdField && rowIdField !== "_id";
|
|
543
|
+
var qrid = needRid ? safeSql.quoteIdentifier(rowIdField, "sqlite") : null;
|
|
544
|
+
|
|
456
545
|
var sel = db.prepare(
|
|
457
|
-
"SELECT _id, " + qc + " AS v FROM " + qt +
|
|
546
|
+
"SELECT _id, " + qc + " AS v" + (qrid ? ", " + qrid + " AS rid" : "") + " FROM " + qt +
|
|
458
547
|
" WHERE " + qc + " IS NOT NULL AND _id > ? ORDER BY _id LIMIT ?"
|
|
459
548
|
);
|
|
460
549
|
var upd = db.prepare("UPDATE " + qt + " SET " + qc + " = ? WHERE _id = ?");
|
|
@@ -468,8 +557,19 @@ function _rotateColumn(db, table, column, oldKeys, newKeys, batchSize, progress)
|
|
|
468
557
|
dbSchema.runInTransaction(db, function () {
|
|
469
558
|
for (var i = 0; i < rows.length; i++) {
|
|
470
559
|
var row = rows[i];
|
|
471
|
-
if (typeof row.v
|
|
472
|
-
|
|
560
|
+
if (typeof row.v !== "string") continue;
|
|
561
|
+
if (aadMode && vaultAad.isAadSealed(row.v)) {
|
|
562
|
+
// Rebuild the exact AAD the seal side used. cryptoField._aadParts
|
|
563
|
+
// reads row[schema.rowIdField]; feed it the rowIdField value we
|
|
564
|
+
// selected (rid, or _id when rowIdField IS _id).
|
|
565
|
+
var rowForAad = {};
|
|
566
|
+
rowForAad[rowIdField] = needRid ? row.rid : row._id;
|
|
567
|
+
var aad = cryptoField._aadParts(schema, table, column, rowForAad);
|
|
568
|
+
upd.run(vaultAad.resealRoot(row.v, aad, roots.oldRootJson, roots.newRootJson), row._id);
|
|
569
|
+
} else if (row.v.indexOf(C.VAULT_PREFIX) === 0) {
|
|
570
|
+
// Plain vault: cell (non-AAD table, or a legacy pre-AAD cell in
|
|
571
|
+
// an AAD table that the next sealRow upgrades).
|
|
572
|
+
upd.run(_reSealValue(row.v, roots.oldKeys, roots.newKeys), row._id);
|
|
473
573
|
}
|
|
474
574
|
}
|
|
475
575
|
});
|
|
@@ -555,6 +655,27 @@ async function rotate(opts) {
|
|
|
555
655
|
throw new VaultRotateError("vault-rotate/no-passphrase",
|
|
556
656
|
"rotate: wrapped mode requires opts.newPassphrase (Buffer)");
|
|
557
657
|
}
|
|
658
|
+
// Detect-and-refuse: AAD-bound state on operator-supplied stores is NOT
|
|
659
|
+
// reached by this pipeline (it walks only db.enc). Refuse unless the
|
|
660
|
+
// operator acknowledges each such store has been re-sealed via its
|
|
661
|
+
// module hook — otherwise a keypair rotation silently orphans them.
|
|
662
|
+
var externalAad = _externalAadTables();
|
|
663
|
+
if (externalAad.length > 0) {
|
|
664
|
+
var ack = opts.externalAadResealed;
|
|
665
|
+
var acknowledged = ack === true ||
|
|
666
|
+
(Array.isArray(ack) && externalAad.every(function (t) { return ack.indexOf(t) !== -1; }));
|
|
667
|
+
if (!acknowledged) {
|
|
668
|
+
throw new VaultRotateError("vault-rotate/external-aad-unresealed",
|
|
669
|
+
"rotate: AAD-bound state on operator-supplied stores is not reached by this " +
|
|
670
|
+
"pipeline and would be orphaned under the retired keypair: " + externalAad.join(", ") +
|
|
671
|
+
". Re-seal each via its module hook (b.agent.idempotency.reseal / " +
|
|
672
|
+
"b.agent.orchestrator.reseal / b.agent.tenant AAD_ROTATION reseal / " +
|
|
673
|
+
"b.agent.snapshot.reseal / b.archive.rewrapTenant for archive-wrap:tenant-blobs) " +
|
|
674
|
+
"BEFORE retiring the old keypair, then pass " +
|
|
675
|
+
"opts.externalAadResealed: [" + externalAad.map(function (t) { return JSON.stringify(t); }).join(", ") +
|
|
676
|
+
"] to acknowledge. If you do not use these features, pass opts.externalAadResealed: true.");
|
|
677
|
+
}
|
|
678
|
+
}
|
|
558
679
|
var rowBatchSize = opts.rowBatchSize || ROW_BATCH_SIZE_DEFAULT;
|
|
559
680
|
var progress = opts.progressCallback;
|
|
560
681
|
var warnings = [];
|
|
@@ -608,6 +729,12 @@ async function rotate(opts) {
|
|
|
608
729
|
// 2. write new vault key
|
|
609
730
|
_emit(progress, { phase: "write_vault_key" });
|
|
610
731
|
var keysJson = JSON.stringify(newKeys, null, 2);
|
|
732
|
+
// Serialized roots for the explicit-root AAD reseal path. These match
|
|
733
|
+
// b.vault.getKeysJson() EXACTLY (JSON.stringify(keys, null, 2)) so an
|
|
734
|
+
// AAD cell re-sealed under newRootJson here unseals once the new keypair
|
|
735
|
+
// is live after the atomic swap.
|
|
736
|
+
var oldRootJson = JSON.stringify(oldKeys, null, 2);
|
|
737
|
+
var newRootJson = keysJson;
|
|
611
738
|
if (mode === "wrapped") {
|
|
612
739
|
var sealed = await vaultWrap().wrap(keysJson, opts.newPassphrase);
|
|
613
740
|
nodeFs.writeFileSync(nodePath.join(stagingDir, paths.vaultKeySealed), sealed, { mode: 0o600 });
|
|
@@ -621,14 +748,29 @@ async function rotate(opts) {
|
|
|
621
748
|
var dbKey = null;
|
|
622
749
|
if (nodeFs.existsSync(dbKeySealedPath)) {
|
|
623
750
|
var sealedKey = nodeFs.readFileSync(dbKeySealedPath, "utf8").trim();
|
|
624
|
-
if (
|
|
751
|
+
if (vaultAad.isAadSealed(sealedKey)) {
|
|
752
|
+
// AAD-bound db.key.enc (db.js since v0.14.7): unseal under the OLD
|
|
753
|
+
// root with the deployment-context AAD, then re-emit under the NEW
|
|
754
|
+
// root with the SAME context (an in-place swap keeps dataDir +
|
|
755
|
+
// keyPath, so source and target AAD match). The vault.aad: shape is
|
|
756
|
+
// preserved — a plain-vault re-emit would strip the deployment-
|
|
757
|
+
// substitution binding (CWE-345 / CWE-441).
|
|
758
|
+
var dbKeyAad = dbModuleLazy()._dbKeyAad(dataDir, dbKeySealedPath);
|
|
759
|
+
var dbKeyB64Aad = vaultAad.unsealRoot(sealedKey, dbKeyAad, oldRootJson);
|
|
760
|
+
dbKey = Buffer.from(dbKeyB64Aad, "base64");
|
|
761
|
+
var resealedAad = vaultAad.sealRoot(dbKeyB64Aad, dbKeyAad, newRootJson);
|
|
762
|
+
nodeFs.writeFileSync(nodePath.join(stagingDir, paths.dbKeySealed), resealedAad, { mode: 0o600 });
|
|
763
|
+
} else if (sealedKey.indexOf(C.VAULT_PREFIX) === 0) {
|
|
764
|
+
// Legacy plain-sealed db.key.enc (pre-AAD). Re-key in place; db.init
|
|
765
|
+
// read-migrates plain -> AAD on the next boot.
|
|
766
|
+
var dbKeyB64 = bCrypto.decrypt(sealedKey.substring(VAULT_PREFIX_LEN), oldKeys);
|
|
767
|
+
dbKey = Buffer.from(dbKeyB64, "base64");
|
|
768
|
+
var resealedKey = C.VAULT_PREFIX + bCrypto.encrypt(dbKeyB64, newKeys);
|
|
769
|
+
nodeFs.writeFileSync(nodePath.join(stagingDir, paths.dbKeySealed), resealedKey, { mode: 0o600 });
|
|
770
|
+
} else {
|
|
625
771
|
throw new VaultRotateError("vault-rotate/bad-dbkey",
|
|
626
|
-
"rotate: db.key.enc does not start with
|
|
772
|
+
"rotate: db.key.enc does not start with a vault prefix (vault: or vault.aad:)");
|
|
627
773
|
}
|
|
628
|
-
var dbKeyB64 = bCrypto.decrypt(sealedKey.substring(VAULT_PREFIX_LEN), oldKeys);
|
|
629
|
-
dbKey = Buffer.from(dbKeyB64, "base64");
|
|
630
|
-
var resealedKey = C.VAULT_PREFIX + bCrypto.encrypt(dbKeyB64, newKeys);
|
|
631
|
-
nodeFs.writeFileSync(nodePath.join(stagingDir, paths.dbKeySealed), resealedKey, { mode: 0o600 });
|
|
632
774
|
}
|
|
633
775
|
for (var as = 0; as < paths.additionalSealed.length; as++) {
|
|
634
776
|
var ase = paths.additionalSealed[as];
|
|
@@ -679,7 +821,14 @@ async function rotate(opts) {
|
|
|
679
821
|
|
|
680
822
|
if (nodeFs.existsSync(encDbPath) && dbKey) {
|
|
681
823
|
var packed = nodeFs.readFileSync(encDbPath);
|
|
682
|
-
|
|
824
|
+
// db.enc is XChaCha20-Poly1305-sealed AAD-bound to its dataDir
|
|
825
|
+
// (db.js _dbEncAad). Read with the dataDir AAD; retry without AAD for
|
|
826
|
+
// pre-AAD envelopes (mirrors db.js:765-768). The in-place swap keeps
|
|
827
|
+
// the same dataDir, so this AAD is reused on the re-encrypt below.
|
|
828
|
+
var dbEncAad = dbModuleLazy()._dbEncAad(dataDir);
|
|
829
|
+
var plainBytes;
|
|
830
|
+
try { plainBytes = bCrypto.decryptPacked(packed, dbKey, dbEncAad); }
|
|
831
|
+
catch (_eAad) { plainBytes = bCrypto.decryptPacked(packed, dbKey); }
|
|
683
832
|
var tmpDbPath = nodePath.join(stagingDir, "_blamejs_rotate.tmp.db");
|
|
684
833
|
nodeFs.writeFileSync(tmpDbPath, plainBytes, { mode: 0o600 });
|
|
685
834
|
|
|
@@ -698,6 +847,11 @@ async function rotate(opts) {
|
|
|
698
847
|
"WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
699
848
|
).all().map(function (r) { return r.name; });
|
|
700
849
|
|
|
850
|
+
// Serialized roots threaded to the AAD reseal path; oldRootJson /
|
|
851
|
+
// newRootJson match b.vault.getKeysJson() so rotated AAD cells unseal
|
|
852
|
+
// once the new keypair is live after the swap.
|
|
853
|
+
var roots = { oldKeys: oldKeys, newKeys: newKeys, oldRootJson: oldRootJson, newRootJson: newRootJson };
|
|
854
|
+
|
|
701
855
|
for (var ti = 0; ti < tablesToRotate.length; ti++) {
|
|
702
856
|
var table = tablesToRotate[ti];
|
|
703
857
|
var tableExists = db.prepare(
|
|
@@ -717,7 +871,7 @@ async function rotate(opts) {
|
|
|
717
871
|
for (var sc = 0; sc < schema.sealedFields.length; sc++) {
|
|
718
872
|
var col = schema.sealedFields[sc];
|
|
719
873
|
if (!liveColSet[col]) continue;
|
|
720
|
-
tableRows += _rotateColumn(db, table, col,
|
|
874
|
+
tableRows += _rotateColumn(db, table, col, schema, roots, rowBatchSize, progress);
|
|
721
875
|
}
|
|
722
876
|
}
|
|
723
877
|
tableRows += _rotateOverflow(db, table, oldKeys, newKeys, rowBatchSize, progress, warnings);
|
|
@@ -748,15 +902,17 @@ async function rotate(opts) {
|
|
|
748
902
|
// inside it. Files are written 0o600 implicitly via the dir's umask
|
|
749
903
|
// and removed before the rotation completes.
|
|
750
904
|
var rotatedBytes = nodeFs.readFileSync(tmpDbPath);
|
|
905
|
+
// Re-encrypt under the SAME dataDir AAD so db.init's AAD-first open
|
|
906
|
+
// succeeds after the staged dir is swapped over dataDir in place.
|
|
751
907
|
nodeFs.writeFileSync(nodePath.join(stagingDir, paths.encryptedDb),
|
|
752
|
-
bCrypto.encryptPacked(rotatedBytes, dbKey));
|
|
908
|
+
bCrypto.encryptPacked(rotatedBytes, dbKey, dbEncAad));
|
|
753
909
|
nodeFs.unlinkSync(tmpDbPath);
|
|
754
910
|
|
|
755
911
|
// Round-trip verify on the staged DB
|
|
756
912
|
_emit(progress, { phase: "verify" });
|
|
757
913
|
var verifyTmp = nodePath.join(stagingDir, "_blamejs_verify.tmp.db");
|
|
758
914
|
nodeFs.writeFileSync(verifyTmp,
|
|
759
|
-
bCrypto.decryptPacked(nodeFs.readFileSync(nodePath.join(stagingDir, paths.encryptedDb)), dbKey));
|
|
915
|
+
bCrypto.decryptPacked(nodeFs.readFileSync(nodePath.join(stagingDir, paths.encryptedDb)), dbKey, dbEncAad));
|
|
760
916
|
var vdb = new DatabaseSync(verifyTmp);
|
|
761
917
|
try {
|
|
762
918
|
verifyResult = verify({ keys: newKeys, db: vdb, oldKeys: oldKeys });
|
|
@@ -819,4 +975,8 @@ module.exports = {
|
|
|
819
975
|
DEFAULT_VERIFY_SAMPLE_MIN: DEFAULT_VERIFY_SAMPLE_MIN,
|
|
820
976
|
DEFAULT_VERIFY_SAMPLE_FRAC: DEFAULT_VERIFY_SAMPLE_FRAC,
|
|
821
977
|
ROW_BATCH_SIZE_DEFAULT: ROW_BATCH_SIZE_DEFAULT,
|
|
978
|
+
// Exposed for the rotation-gate coverage test: every lib module that exports
|
|
979
|
+
// an external AAD_ROTATION descriptor must be reachable here, or a keypair
|
|
980
|
+
// rotation silently orphans its store.
|
|
981
|
+
_externalAadTables: _externalAadTables,
|
|
822
982
|
};
|
|
@@ -147,13 +147,17 @@ function buildContextAad(parts) {
|
|
|
147
147
|
// 32 bytes). Constant-domain prefix prevents key collision with other
|
|
148
148
|
// uses of the vault root.
|
|
149
149
|
|
|
150
|
-
function _deriveKey(aadBytes) {
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
// a
|
|
156
|
-
//
|
|
150
|
+
function _deriveKey(aadBytes, rootKeysJson) {
|
|
151
|
+
// rootKeysJson lets the vault-key rotation pipeline derive the per-row
|
|
152
|
+
// key under a SPECIFIC vault root (old or new keypair) within one
|
|
153
|
+
// process; when omitted it uses the live singleton. The keys JSON
|
|
154
|
+
// includes the active keypair PEMs — hashing the whole serialized form
|
|
155
|
+
// gives a stable per-vault root secret. Rotating vault keys produces a
|
|
156
|
+
// different root, so prior AAD-sealed values must be re-sealed (the
|
|
157
|
+
// rotation pipeline walks them via sealRoot/unsealRoot/resealRoot).
|
|
158
|
+
var keysJson = (typeof rootKeysJson === "string" && rootKeysJson.length > 0)
|
|
159
|
+
? rootKeysJson
|
|
160
|
+
: vault().getKeysJson();
|
|
157
161
|
var rootHash = bCrypto().sha3Hash(keysJson);
|
|
158
162
|
var prefix = Buffer.from("vault.aad/v1/", "utf8");
|
|
159
163
|
var rootBuf = Buffer.from(rootHash, "hex");
|
|
@@ -161,7 +165,7 @@ function _deriveKey(aadBytes) {
|
|
|
161
165
|
return bCrypto().kdf(input, C.BYTES.bytes(32));
|
|
162
166
|
}
|
|
163
167
|
|
|
164
|
-
function
|
|
168
|
+
function _seal(plaintext, aadParts, rootKeysJson, suppressAudit) {
|
|
165
169
|
if (plaintext == null) {
|
|
166
170
|
throw new VaultAadError("vault-aad/bad-input",
|
|
167
171
|
"seal: plaintext is required (use null/undefined-stripping at the call site)");
|
|
@@ -176,26 +180,32 @@ function seal(plaintext, aadParts) {
|
|
|
176
180
|
"seal: value is already AAD-sealed (refuses to double-seal)");
|
|
177
181
|
}
|
|
178
182
|
var aadBytes = _canonicalize(aadParts);
|
|
179
|
-
var key = _deriveKey(aadBytes);
|
|
183
|
+
var key = _deriveKey(aadBytes, rootKeysJson);
|
|
180
184
|
var ptBuf = Buffer.from(plaintext, "utf8");
|
|
181
185
|
var packed = bCrypto().encryptPacked(ptBuf, key, aadBytes);
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
187
|
+
if (!suppressAudit) {
|
|
188
|
+
try {
|
|
189
|
+
audit().safeEmit({
|
|
190
|
+
action: "vault.aad.sealed",
|
|
191
|
+
outcome: "success",
|
|
192
|
+
actor: null,
|
|
193
|
+
metadata: {
|
|
194
|
+
aadKeys: Object.keys(aadParts).sort(), // allow:bare-canonicalize-walk — audit-emit metadata, not for signing
|
|
195
|
+
bytes: ptBuf.length,
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
} catch (_e) { /* drop-silent */ }
|
|
199
|
+
}
|
|
194
200
|
|
|
195
201
|
return AAD_PREFIX + packed.toString("base64");
|
|
196
202
|
}
|
|
197
203
|
|
|
198
|
-
function
|
|
204
|
+
function seal(plaintext, aadParts) {
|
|
205
|
+
return _seal(plaintext, aadParts, undefined, false);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _unseal(value, aadParts, rootKeysJson, suppressAudit) {
|
|
199
209
|
if (value == null || typeof value !== "string") {
|
|
200
210
|
throw new VaultAadError("vault-aad/bad-input",
|
|
201
211
|
"unseal: value must be a non-empty string");
|
|
@@ -205,7 +215,7 @@ function unseal(value, aadParts) {
|
|
|
205
215
|
"unseal: value is not AAD-sealed (missing " + JSON.stringify(AAD_PREFIX) + " prefix)");
|
|
206
216
|
}
|
|
207
217
|
var aadBytes = _canonicalize(aadParts);
|
|
208
|
-
var key = _deriveKey(aadBytes);
|
|
218
|
+
var key = _deriveKey(aadBytes, rootKeysJson);
|
|
209
219
|
var packed;
|
|
210
220
|
try { packed = Buffer.from(value.slice(AAD_PREFIX.length), "base64"); }
|
|
211
221
|
catch (e) {
|
|
@@ -215,17 +225,19 @@ function unseal(value, aadParts) {
|
|
|
215
225
|
var pt;
|
|
216
226
|
try { pt = bCrypto().decryptPacked(packed, key, aadBytes); }
|
|
217
227
|
catch (e) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
228
|
+
if (!suppressAudit) {
|
|
229
|
+
try {
|
|
230
|
+
audit().safeEmit({
|
|
231
|
+
action: "vault.aad.unseal_failed",
|
|
232
|
+
outcome: "denied",
|
|
233
|
+
actor: null,
|
|
234
|
+
metadata: {
|
|
235
|
+
aadKeys: Object.keys(aadParts).sort(), // allow:bare-canonicalize-walk — audit-emit metadata, not for signing
|
|
236
|
+
reason: e.message,
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
} catch (_e) { /* drop-silent */ }
|
|
240
|
+
}
|
|
229
241
|
throw new VaultAadError("vault-aad/aead-mismatch",
|
|
230
242
|
"unseal: AEAD authentication failed — value may have been tampered, " +
|
|
231
243
|
"copied from a different row, or sealed under different AAD");
|
|
@@ -233,6 +245,10 @@ function unseal(value, aadParts) {
|
|
|
233
245
|
return pt.toString("utf8");
|
|
234
246
|
}
|
|
235
247
|
|
|
248
|
+
function unseal(value, aadParts) {
|
|
249
|
+
return _unseal(value, aadParts, undefined, false);
|
|
250
|
+
}
|
|
251
|
+
|
|
236
252
|
function isAadSealed(value) {
|
|
237
253
|
return typeof value === "string" && value.indexOf(AAD_PREFIX) === 0;
|
|
238
254
|
}
|
|
@@ -246,10 +262,45 @@ function reseal(value, fromAad, toAad) {
|
|
|
246
262
|
return seal(plaintext, toAad);
|
|
247
263
|
}
|
|
248
264
|
|
|
265
|
+
// ---- explicit-root variants (vault-key rotation pipeline) ----
|
|
266
|
+
//
|
|
267
|
+
// The rotation pipeline must decrypt a cell under the OLD vault root and
|
|
268
|
+
// re-encrypt it under the NEW root within one process — the live-singleton
|
|
269
|
+
// _deriveKey cannot straddle two keypairs. These take the serialized vault
|
|
270
|
+
// keys JSON (b.vault.getKeysJson() output) for a specific root; the AAD
|
|
271
|
+
// tuple is unchanged, only the root differs. Per-cell audit is suppressed
|
|
272
|
+
// (the rotation pipeline has its own progress + verify reporting).
|
|
273
|
+
|
|
274
|
+
function sealRoot(plaintext, aadParts, rootKeysJson) {
|
|
275
|
+
if (typeof rootKeysJson !== "string" || rootKeysJson.length === 0) {
|
|
276
|
+
throw new VaultAadError("vault-aad/bad-root", "sealRoot: rootKeysJson (vault keys JSON) is required");
|
|
277
|
+
}
|
|
278
|
+
return _seal(plaintext, aadParts, rootKeysJson, true);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function unsealRoot(value, aadParts, rootKeysJson) {
|
|
282
|
+
if (typeof rootKeysJson !== "string" || rootKeysJson.length === 0) {
|
|
283
|
+
throw new VaultAadError("vault-aad/bad-root", "unsealRoot: rootKeysJson (vault keys JSON) is required");
|
|
284
|
+
}
|
|
285
|
+
return _unseal(value, aadParts, rootKeysJson, true);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Re-seal a value from the old root to the new root under the SAME AAD
|
|
289
|
+
// tuple: authenticate under the old root (throws aead-mismatch if the
|
|
290
|
+
// value was not sealed under oldRootJson + aadParts), then re-encrypt
|
|
291
|
+
// under the new root. The rotation pipeline composes this per cell.
|
|
292
|
+
function resealRoot(value, aadParts, oldRootJson, newRootJson) {
|
|
293
|
+
var plaintext = unsealRoot(value, aadParts, oldRootJson);
|
|
294
|
+
return sealRoot(plaintext, aadParts, newRootJson);
|
|
295
|
+
}
|
|
296
|
+
|
|
249
297
|
module.exports = {
|
|
250
298
|
seal: seal,
|
|
251
299
|
unseal: unseal,
|
|
252
300
|
reseal: reseal,
|
|
301
|
+
sealRoot: sealRoot,
|
|
302
|
+
unsealRoot: unsealRoot,
|
|
303
|
+
resealRoot: resealRoot,
|
|
253
304
|
isAadSealed: isAadSealed,
|
|
254
305
|
buildColumnAad: buildColumnAad,
|
|
255
306
|
buildContextAad: buildContextAad,
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../scripts/release-notes-schema.json",
|
|
3
|
+
"version": "0.14.11",
|
|
4
|
+
"date": "2026-05-31",
|
|
5
|
+
"headline": "Defensive LLM model-I/O primitives, C2PA timestamp countersignatures with CAWG identity assertions, and signed EU AI Act GPAI adherence declarations",
|
|
6
|
+
"summary": "Closes the output side of the LLM trust boundary and hardens content provenance and AI-Act attestation. b.ai.output.sanitize treats model output as untrusted and neutralizes XSS, gates every markdown-image / link and HTML src/href URL against SSRF (the EchoLeak zero-click exfiltration class, CVE-2025-32711), and flags SQL- and command-shaped fragments; b.ai.output.redact strips PII and secret disclosures. b.ai.input.classifyWithSources classifies a prompt together with its retrieval-augmented sources under a stricter, trust-tier-relative threshold, and the new b.ai.prompt namespace assembles prompts with escape-by-default boundaries — untrusted context / user segments are fenced in a per-render crypto-nonce delimiter the content cannot forge and stripped of bidi, control, zero-width, and Unicode-Tags smuggling characters. b.contentCredentials COSE signatures now carry an RFC 3161 timestamp countersignature (C2PA sigTst2, RFC 9921) verified entirely through b.tsa, so a signed manifest stays verifiable after its signing certificate expires, plus a CAWG identity assertion with trust-anchored verification. b.compliance.aiAct.gpai.declareAdherence emits a tamper-evident, ML-DSA-87-signed GPAI Code-of-Practice adherence declaration whose obligation set is derived from the regulation rather than operator-asserted.",
|
|
7
|
+
"sections": [
|
|
8
|
+
{
|
|
9
|
+
"heading": "Added",
|
|
10
|
+
"items": [
|
|
11
|
+
{
|
|
12
|
+
"title": "`b.ai.output.sanitize` and `b.ai.output.redact`",
|
|
13
|
+
"body": "A new `b.ai.output` namespace that treats LLM output as untrusted before it reaches a browser, a downstream fetcher, a SQL / command sink, or a log. `sanitize(text, opts)` neutralizes active markup via `b.guardHtml`, gates every markdown image / link and HTML `src` / `href` URL through `b.safeUrl.parse` (scheme + credential) and `b.ssrfGuard.classify` (internal / loopback / link-local / cloud-metadata IP-range) so auto-fetch URLs to attacker or internal hosts are neutralized, and flags SQL- and command-shaped fragments rather than silently repairing them. `redact(text, opts)` strips PII and secret disclosures via `b.redact` plus an entity-selectable pass (`pan` / `ssn` / `ein` / `iban` / `jwt` / `aws` / `phi` / `email` / `phone`). Defends OWASP LLM05:2025 Improper Output Handling and LLM02:2025 Sensitive Information Disclosure; the markdown-image URL gate closes the EchoLeak zero-click exfiltration class (CVE-2025-32711, CVSS 9.3)."
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"title": "`b.ai.input.classifyWithSources`",
|
|
17
|
+
"body": "Classifies a prompt together with its retrieval-augmented (RAG) sources, applying a stricter, trust-tier-relative threshold to retrieved data. Each source is `{ id, text, trust? }` with `trust` of `trusted` / `internal` / `untrusted` (unset defaults to `untrusted`, fail-closed); untrusted and internal sources escalate to `suspicious` on a single severity-2 signal and to `malicious` on any severity-3, where the direct prompt keeps the baseline threshold. The aggregate verdict is the worst across the prompt and all sources, and every malicious source is reported in `taintedSources`. Defends indirect prompt injection from poisoned context (OWASP LLM01:2025; NIST AI 600-1)."
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
"title": "`b.ai.prompt.template`",
|
|
21
|
+
"body": "A new `b.ai.prompt` namespace for assembling LLM prompts with escape-by-default boundaries. The `system` segment is operator-trusted; `context` and `user` segments are treated as untrusted (no global opt-out — mark a segment `{ text, trusted: true }` individually). Untrusted segments are wrapped in a per-render, high-entropy delimiter nonce the content cannot forge, with any forged boundary stripped before wrapping (spotlighting / datamarking, Microsoft 2024; NIST AI 100-2e2025), and stripped of bidi overrides (CVE-2021-42574 Trojan Source), C0 controls, zero-width characters, null bytes, and Unicode Tags (U+E0000..U+E007F ASCII-smuggling). Run `b.ai.input.refuseIfMalicious` on the untrusted content as defense in depth."
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"title": "C2PA RFC 3161 timestamp countersignature and CAWG identity assertion",
|
|
25
|
+
"body": "`b.contentCredentials.signCose` attaches an RFC 3161 timestamp countersignature (C2PA `sigTst2`, RFC 9921) and `b.contentCredentials.verifyCose` verifies it. Pass `timestamp:{ token }` to embed a TimeStampToken, or `timestamp:{}` to get back the DER `application/timestamp-query` to POST to a timestamp authority. `b.contentCredentials.attachIdentityAssertion` / `verifyIdentityAssertion` add the CAWG Identity Assertion v1.2: a signed creator / organization identity hash-bound to a manifest's referenced assertions, where the `x509` binding reports `verified:true` only when an identity trust anchor is supplied and the leaf chain verifies, and the `identity-claims-aggregator` and self-asserted paths stay `verified:false`."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"title": "`b.compliance.aiAct.gpai.declareAdherence` / `verifyAdherence`",
|
|
29
|
+
"body": "Signed, tamper-evident GPAI Code-of-Practice adherence declarations (Regulation (EU) 2024/1689 Art. 53(1)(a-d); Art. 55 for systemic-risk models under Art. 51(2)). The in-scope obligation set is derived from the classifier, never operator-asserted — a model at or above the 10^25-FLOP systemic-risk threshold that omits the Art. 55 chapter is refused. Each commitment's evidence reference must be a SHA3-512 digest; a malformed hash is rejected so a hollow attestation cannot bind. The declaration ships inside an ML-DSA-87-signed CycloneDX 1.6 ML-BOM via `b.ai.modelManifest`; verify re-canonicalizes before trusting any field and rejects a declaration past its validity window. Cites the GPAI Code of Practice (10 July 2025), Annex XI/XII, and Directive (EU) 2019/790 Art. 4(3)."
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"heading": "Security",
|
|
35
|
+
"items": [
|
|
36
|
+
{
|
|
37
|
+
"title": "Model output is now an untrusted channel by default",
|
|
38
|
+
"body": "When feeding retrieved documents into an LLM, classify them with `b.ai.input.classifyWithSources` (untrusted sources escalate on a single signal) rather than trusting model input; assemble prompts with `b.ai.prompt.template` so untrusted context / user text is fenced in a per-render crypto-nonce boundary it cannot forge; and pass model output through `b.ai.output.sanitize` / `b.ai.output.redact` before it is rendered, fetched, or logged. Each primitive is on by default and fail-closed — no opt-in flag enables the protection."
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"title": "Timestamp verification routes only through `b.tsa.verifyToken`",
|
|
42
|
+
"body": "C2PA `sigTst2` verification performs the full RFC 3161 check (CMS signature over the signed attributes, messageDigest recompute, critical sole `id-kp-timeStamping` EKU) — never a chain-only shortcut — closing the timestamp-validation-bypass class (CVE-2025-52556, CWE-347). Supply `timestampTrustAnchorsPem` to `verifyCose` to check the timestamp certificate chain; `verifyCose` returns `{ valid, reason, claims, alg, timestamp }` and never throws."
|
|
43
|
+
}
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"heading": "Detectors",
|
|
48
|
+
"items": [
|
|
49
|
+
{
|
|
50
|
+
"title": "LLM output URLs must keep the SSRF gate",
|
|
51
|
+
"body": "A new check requires the output sanitizer to gate every extracted URL through both `b.safeUrl.parse` and `b.ssrfGuard.classify`, so the markdown-image SSRF gate (the EchoLeak class) cannot be silently dropped."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"title": "RAG sources must compose `classifyWithSources`",
|
|
55
|
+
"body": "A new check flags any code that maps `b.ai.input.classify` over a sources array by hand, which would lose the trust-tier-relative threshold for retrieved data."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"title": "Prompt boundaries must use a per-render nonce",
|
|
59
|
+
"body": "A new check flags prompt-assembly that wraps untrusted content in a fixed, guessable literal fence (`<user_input>`, `[DATA]`) instead of a per-render high-entropy delimiter the content cannot forge."
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
"title": "C2PA timestamp verification must route through `b.tsa`",
|
|
63
|
+
"body": "A new check flags any bespoke certificate-chain-only walk on a timestamp token in place of `b.tsa.verifyToken`, preventing a re-introduction of the timestamp-validation-bypass class."
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
"title": "GPAI adherence declarations must be signed",
|
|
67
|
+
"body": "A new check flags any code that emits the GPAI Code-of-Practice adherence property without routing it through the `b.ai.modelManifest` signed envelope, keeping the declaration tamper-evident."
|
|
68
|
+
}
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|