@blamejs/core 0.14.10 → 0.14.11
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 +2 -0
- package/README.md +5 -2
- package/index.js +4 -0
- package/lib/ai-input.js +167 -3
- package/lib/ai-output.js +463 -0
- package/lib/ai-prompt.js +304 -0
- package/lib/audit.js +2 -0
- package/lib/codepoint-class.js +18 -0
- package/lib/compliance-ai-act.js +446 -0
- package/lib/content-credentials.js +851 -41
- package/lib/framework-error.js +16 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/compliance-ai-act.js
CHANGED
|
@@ -38,13 +38,21 @@
|
|
|
38
38
|
|
|
39
39
|
var validateOpts = require("./validate-opts");
|
|
40
40
|
var lazyRequire = require("./lazy-require");
|
|
41
|
+
var C = require("./constants");
|
|
41
42
|
var { ComplianceError } = require("./framework-error");
|
|
42
43
|
|
|
43
44
|
var prohibited = require("./compliance-ai-act-prohibited");
|
|
44
45
|
var risk = require("./compliance-ai-act-risk");
|
|
45
46
|
var transparency = require("./compliance-ai-act-transparency");
|
|
46
47
|
var logging = require("./compliance-ai-act-logging");
|
|
48
|
+
var safeJson = require("./safe-json");
|
|
47
49
|
var audit = lazyRequire(function () { return require("./audit"); });
|
|
50
|
+
// modelManifest carries the CycloneDX 1.6 ML-BOM build/sign/verify
|
|
51
|
+
// envelope. lazyRequire'd to dodge the framework's documented
|
|
52
|
+
// circular-load chain (index.js → compliance-* → audit → db →
|
|
53
|
+
// framework-error → constants → package.json), the same reason
|
|
54
|
+
// ai-model-manifest reads package.json behind a call.
|
|
55
|
+
var modelManifest = lazyRequire(function () { return require("./ai-model-manifest"); });
|
|
48
56
|
|
|
49
57
|
// ---- Article 113 deadline calendar ----
|
|
50
58
|
//
|
|
@@ -795,6 +803,441 @@ function crossWalkIso23894() {
|
|
|
795
803
|
});
|
|
796
804
|
}
|
|
797
805
|
|
|
806
|
+
// ---- GPAI Code-of-Practice adherence declaration (Art. 53/55) ----
|
|
807
|
+
//
|
|
808
|
+
// The General-Purpose AI Code of Practice (published 10 July 2025 by
|
|
809
|
+
// the AI Office) is the voluntary instrument by which a GPAI provider
|
|
810
|
+
// demonstrates compliance with Reg (EU) 2024/1689 Art. 53 (and Art. 55
|
|
811
|
+
// for systemic-risk models). Signing the Code is a public adherence
|
|
812
|
+
// declaration; this primitive emits a cryptographically-bound,
|
|
813
|
+
// tamper-evident version of that declaration so the obligation-set it
|
|
814
|
+
// covers cannot be silently downgraded and the per-commitment evidence
|
|
815
|
+
// cannot be replaced with a hollow claim.
|
|
816
|
+
//
|
|
817
|
+
// COP_VERSION_RE is shape-only — it pins the documented `YYYY-MM`
|
|
818
|
+
// release-label form of the Code (e.g. "2025-07") with a real month
|
|
819
|
+
// group; it is NOT a semantic validity check (the AI Office is the
|
|
820
|
+
// authority on which labels exist).
|
|
821
|
+
var COP_VERSION_RE = /^\d{4}-(0[1-9]|1[0-2])$/; // allow:regex-no-length-cap — fixed 7-char YYYY-MM Code-of-Practice release label, fully anchored
|
|
822
|
+
// SHA3-512 hex digest shape, matching b.crypto.sha3Hash output (64
|
|
823
|
+
// bytes → 128 lowercase-hex chars). An evidenceHash that does not match
|
|
824
|
+
// this shape is a hollow attestation — the compliance-theater shape
|
|
825
|
+
// this primitive exists to refuse.
|
|
826
|
+
var EVIDENCE_HASH_RE = /^[0-9a-f]{128}$/; // allow:regex-no-length-cap — fixed-length SHA3-512 hex (128 chars), fully anchored
|
|
827
|
+
|
|
828
|
+
// Default validity window for a phase-in adherence declaration. The
|
|
829
|
+
// Art. 53 obligations become applicable 2026-08-02 (DEADLINES
|
|
830
|
+
// .generalPurposeAI); a declaration that predates a material model
|
|
831
|
+
// change should not be relied on indefinitely. 90 days is the auditor-
|
|
832
|
+
// review default; operators override via opts.validityMs.
|
|
833
|
+
var DEFAULT_VALIDITY_MS = C.TIME.days(90);
|
|
834
|
+
|
|
835
|
+
var DECLARE_ALLOWED_KEYS = [
|
|
836
|
+
"modelId", "modelVersion", "provider", "isSystemicRisk", "trainingFlops",
|
|
837
|
+
"designatedSystemicRisk", "modalities", "copVersion", "commitments",
|
|
838
|
+
"trainingDataSummary", "generatedAt", "validityMs", "privateKeyPem",
|
|
839
|
+
"serialNumber", "audit",
|
|
840
|
+
];
|
|
841
|
+
|
|
842
|
+
// Derive the in-scope obligation set from the regulation, never from an
|
|
843
|
+
// operator-asserted list. declareAdherence is GPAI-specific, so kind is
|
|
844
|
+
// injected as "gpai" before gpaiClassify runs — otherwise a caller that
|
|
845
|
+
// omits kind/generalPurpose would classify as non-GPAI (isGpai:false →
|
|
846
|
+
// obligations:[]) and the scope-downgrade refusal below could never
|
|
847
|
+
// fire.
|
|
848
|
+
function _deriveGpaiObligations(opts) {
|
|
849
|
+
var probe = {
|
|
850
|
+
kind: "gpai",
|
|
851
|
+
trainingFlops: opts.trainingFlops,
|
|
852
|
+
designatedSystemicRisk: opts.designatedSystemicRisk === true ||
|
|
853
|
+
opts.isSystemicRisk === true ? true : undefined,
|
|
854
|
+
};
|
|
855
|
+
var verdict = gpaiClassify(probe);
|
|
856
|
+
return {
|
|
857
|
+
isSystemicRisk: verdict.isSystemicRisk,
|
|
858
|
+
obligations: verdict.obligations, // [{ article, title, description }, ...]
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
/**
|
|
863
|
+
* @primitive b.compliance.aiAct.gpai.adherenceForm
|
|
864
|
+
* @signature b.compliance.aiAct.gpai.adherenceForm(opts)
|
|
865
|
+
* @since 0.14.11
|
|
866
|
+
* @status stable
|
|
867
|
+
* @compliance eu-ai-act-art-11
|
|
868
|
+
* @related b.compliance.aiAct.gpai.declareAdherence, b.ai.modelManifest.build
|
|
869
|
+
*
|
|
870
|
+
* Build the unsigned GPAI Code-of-Practice adherence document for a
|
|
871
|
+
* general-purpose AI model. The obligation set is DERIVED from the
|
|
872
|
+
* regulation via the GPAI classifier (Reg (EU) 2024/1689 Art. 53(1)(a-d)
|
|
873
|
+
* always; Art. 55 when the model is a systemic-risk model under
|
|
874
|
+
* Art. 51(2)), never taken from an operator-supplied list. Each
|
|
875
|
+
* obligation is paired with the operator's commitment + evidence hash;
|
|
876
|
+
* the evidence hash is validated against the SHA3-512 hex shape so a
|
|
877
|
+
* hollow attestation (junk "hash") is refused at build time (CWE-345
|
|
878
|
+
* insufficient verification of data authenticity).
|
|
879
|
+
*
|
|
880
|
+
* This is the document `declareAdherence` signs; most operators call
|
|
881
|
+
* `declareAdherence` directly. The form is exposed for operators who
|
|
882
|
+
* want to inspect or persist the derived obligation set before signing.
|
|
883
|
+
*
|
|
884
|
+
* @opts
|
|
885
|
+
* modelId: string, // required — provider's model identifier
|
|
886
|
+
* modelVersion: string, // required — model version
|
|
887
|
+
* provider: object, // { name, address, contact }
|
|
888
|
+
* trainingFlops: number, // cumulative training compute (Art. 51(2) presumption at >= 1e25)
|
|
889
|
+
* isSystemicRisk: boolean, // operator-asserted systemic-risk designation (Art. 51(1)(b))
|
|
890
|
+
* designatedSystemicRisk: boolean, // AI-Office-designated systemic risk
|
|
891
|
+
* copVersion: string, // GPAI Code of Practice release label, "YYYY-MM" (default "2025-07")
|
|
892
|
+
* commitments: object[], // [{ article, statement, evidenceHash }] — evidenceHash is SHA3-512 hex
|
|
893
|
+
* trainingDataSummary: object,// Art. 53(1)(d) public-summary pointer (b.compliance.aiAct.gpai.trainingDataSummary)
|
|
894
|
+
* generatedAt: string, // ISO 8601 UTC; defaults to now
|
|
895
|
+
* validityMs: number, // declaration validity window; default 90 days
|
|
896
|
+
*
|
|
897
|
+
* @example
|
|
898
|
+
* var hash = b.crypto.sha3Hash("eval-report-2026.pdf");
|
|
899
|
+
* var form = b.compliance.aiAct.gpai.adherenceForm({
|
|
900
|
+
* modelId: "acme-llm-7b",
|
|
901
|
+
* modelVersion: "1.0",
|
|
902
|
+
* commitments: [{ article: "Art. 53(1)(a)", statement: "Annex XI docs maintained", evidenceHash: hash }],
|
|
903
|
+
* });
|
|
904
|
+
* form.commitments.length; // 4 — the four Art. 53 obligations (no systemic-risk chapter)
|
|
905
|
+
* form.commitments[0].evidenced; // true (Art. 53(1)(a) has a bound commitment)
|
|
906
|
+
* form.commitments[1].evidenced; // false (no commitment supplied yet)
|
|
907
|
+
*/
|
|
908
|
+
function adherenceForm(opts) {
|
|
909
|
+
validateOpts.requireObject(opts, "compliance.aiAct.gpai.adherenceForm",
|
|
910
|
+
ComplianceError, "compliance-ai-act/bad-input");
|
|
911
|
+
validateOpts(opts, DECLARE_ALLOWED_KEYS, "compliance.aiAct.gpai.adherenceForm");
|
|
912
|
+
validateOpts.requireNonEmptyString(opts.modelId, "adherenceForm: modelId",
|
|
913
|
+
ComplianceError, "compliance-ai-act/no-model-id");
|
|
914
|
+
validateOpts.requireNonEmptyString(opts.modelVersion, "adherenceForm: modelVersion",
|
|
915
|
+
ComplianceError, "compliance-ai-act/no-model-version");
|
|
916
|
+
|
|
917
|
+
var copVersion = opts.copVersion || "2025-07";
|
|
918
|
+
if (typeof copVersion !== "string" || copVersion.length > 10 || !COP_VERSION_RE.test(copVersion)) {
|
|
919
|
+
throw new ComplianceError("compliance-ai-act/cop-version-bad",
|
|
920
|
+
"adherenceForm: copVersion must be a YYYY-MM Code-of-Practice release label — got " +
|
|
921
|
+
JSON.stringify(copVersion));
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
var derived = _deriveGpaiObligations(opts);
|
|
925
|
+
if (derived.obligations.length === 0) {
|
|
926
|
+
// Defensive: kind is injected "gpai" so this is unreachable on the
|
|
927
|
+
// happy path, but a future classifier change must not silently emit
|
|
928
|
+
// an empty-obligation declaration that asserts nothing.
|
|
929
|
+
throw new ComplianceError("compliance-ai-act/cop-no-obligations",
|
|
930
|
+
"adherenceForm: derived GPAI obligation set is empty — refusing to sign a declaration that covers no Art. 53 obligation");
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
var requiredArticles = derived.obligations.map(function (o) { return o.article; });
|
|
934
|
+
|
|
935
|
+
// Index operator commitments by article + validate each evidence hash.
|
|
936
|
+
var byArticle = Object.create(null);
|
|
937
|
+
var commitments = Array.isArray(opts.commitments) ? opts.commitments : [];
|
|
938
|
+
for (var i = 0; i < commitments.length; i += 1) {
|
|
939
|
+
var c = commitments[i];
|
|
940
|
+
if (!c || typeof c !== "object" || typeof c.article !== "string") {
|
|
941
|
+
throw new ComplianceError("compliance-ai-act/cop-commitment-shape",
|
|
942
|
+
"adherenceForm: commitments[" + i + "] must be { article, statement, evidenceHash }");
|
|
943
|
+
}
|
|
944
|
+
if (typeof c.evidenceHash !== "string" || c.evidenceHash.length !== 128 || !EVIDENCE_HASH_RE.test(c.evidenceHash)) {
|
|
945
|
+
throw new ComplianceError("compliance-ai-act/cop-evidence-bad-hash",
|
|
946
|
+
"adherenceForm: commitments[" + i + "].evidenceHash must be a SHA3-512 hex digest " +
|
|
947
|
+
"(128 lowercase-hex chars, b.crypto.sha3Hash output) — a 1-char junk hash is a hollow attestation");
|
|
948
|
+
}
|
|
949
|
+
byArticle[c.article] = {
|
|
950
|
+
article: c.article,
|
|
951
|
+
statement: typeof c.statement === "string" ? c.statement : null,
|
|
952
|
+
evidenceHash: c.evidenceHash,
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// Build the per-obligation declaration. Every DERIVED obligation gets
|
|
957
|
+
// an entry; if the operator supplied a matching commitment it binds,
|
|
958
|
+
// otherwise the obligation is recorded as not-yet-evidenced (the
|
|
959
|
+
// verify path surfaces it, the auditor decides).
|
|
960
|
+
var declaredCommitments = derived.obligations.map(function (o) {
|
|
961
|
+
var bound = byArticle[o.article];
|
|
962
|
+
return {
|
|
963
|
+
article: o.article,
|
|
964
|
+
title: o.title,
|
|
965
|
+
description: o.description,
|
|
966
|
+
statement: bound ? bound.statement : null,
|
|
967
|
+
evidenceHash: bound ? bound.evidenceHash : null,
|
|
968
|
+
evidenced: !!bound,
|
|
969
|
+
};
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
var generatedAt = opts.generatedAt || new Date().toISOString();
|
|
973
|
+
var validityMs = opts.validityMs === undefined ? DEFAULT_VALIDITY_MS
|
|
974
|
+
: validateOpts.optionalPositiveFinite(opts.validityMs, "adherenceForm: validityMs",
|
|
975
|
+
ComplianceError, "compliance-ai-act/bad-validity");
|
|
976
|
+
|
|
977
|
+
return {
|
|
978
|
+
"$schema": "https://blamejs.com/schema/ai-act-gpai-cop-adherence-v1.json",
|
|
979
|
+
regulation: "EU Regulation 2024/1689 — AI Act",
|
|
980
|
+
articles: derived.isSystemicRisk ? ["Art. 53", "Art. 55"] : ["Art. 53"],
|
|
981
|
+
instrument: "GPAI Code of Practice (10 July 2025)",
|
|
982
|
+
copVersion: copVersion,
|
|
983
|
+
modelId: opts.modelId,
|
|
984
|
+
modelVersion: opts.modelVersion,
|
|
985
|
+
provider: opts.provider || { name: null, address: null, contact: null },
|
|
986
|
+
isSystemicRisk: derived.isSystemicRisk,
|
|
987
|
+
requiredArticles: requiredArticles,
|
|
988
|
+
commitments: declaredCommitments,
|
|
989
|
+
trainingDataSummary: opts.trainingDataSummary || null,
|
|
990
|
+
// Art. 113 phase-in deadlines are bound INTO the signed payload so a
|
|
991
|
+
// verifier can confirm the declaration was made against the correct
|
|
992
|
+
// applicability calendar (not back-dated to a different regime).
|
|
993
|
+
deadlines: DEADLINES,
|
|
994
|
+
generatedAt: generatedAt,
|
|
995
|
+
validityMs: validityMs,
|
|
996
|
+
note: "Adherence to the GPAI Code of Practice per Reg (EU) 2024/1689 Art. 53" +
|
|
997
|
+
(derived.isSystemicRisk ? " + Art. 55 (systemic risk)." : "."),
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* @primitive b.compliance.aiAct.gpai.declareAdherence
|
|
1003
|
+
* @signature b.compliance.aiAct.gpai.declareAdherence(opts)
|
|
1004
|
+
* @since 0.14.11
|
|
1005
|
+
* @status stable
|
|
1006
|
+
* @compliance eu-ai-act-art-11
|
|
1007
|
+
* @related b.ai.modelManifest.sign, b.compliance.aiAct.gpai.adherenceForm
|
|
1008
|
+
*
|
|
1009
|
+
* Emit a SIGNED, tamper-evident GPAI Code-of-Practice adherence
|
|
1010
|
+
* declaration (Reg (EU) 2024/1689 Art. 53(1)(a-d); Art. 55 when the
|
|
1011
|
+
* model is a systemic-risk model under Art. 51(2)). The Code of
|
|
1012
|
+
* Practice (10 July 2025) is the AI Office's voluntary compliance
|
|
1013
|
+
* instrument; this primitive binds the adherence to the model + the
|
|
1014
|
+
* derived obligation set + the per-commitment evidence hashes inside a
|
|
1015
|
+
* CycloneDX 1.6 ML-BOM signed with ML-DSA-87 (FIPS 204) via
|
|
1016
|
+
* `b.ai.modelManifest.build` + `sign`. There is no unsigned return
|
|
1017
|
+
* path on the happy path: the declaration always ships inside the
|
|
1018
|
+
* signature envelope.
|
|
1019
|
+
*
|
|
1020
|
+
* Two compliance-theater shapes are refused structurally rather than
|
|
1021
|
+
* trusted:
|
|
1022
|
+
*
|
|
1023
|
+
* - Obligation-set downgrade: the in-scope obligations are DERIVED
|
|
1024
|
+
* from the classifier (kind injected as "gpai"), never accepted
|
|
1025
|
+
* from the operator. A 10^25-FLOP+ model that omits the Art. 55
|
|
1026
|
+
* systemic-risk chapter is refused — the classifier puts Art. 55
|
|
1027
|
+
* in scope, the declaration must cover it.
|
|
1028
|
+
* - Hollow attestation: every commitment's `evidenceHash` is checked
|
|
1029
|
+
* against the SHA3-512 hex shape (128 hex chars, `b.crypto.sha3Hash`
|
|
1030
|
+
* output). A junk "hash" cannot bind — CWE-345 (insufficient
|
|
1031
|
+
* verification of data authenticity) / CWE-347 (improper
|
|
1032
|
+
* verification of cryptographic signature).
|
|
1033
|
+
*
|
|
1034
|
+
* The signed envelope is verified with
|
|
1035
|
+
* `b.compliance.aiAct.gpai.verifyAdherence(envelope, publicKeyPem)`,
|
|
1036
|
+
* which re-canonicalizes before trusting any field (never trusts an
|
|
1037
|
+
* embedded signed-bytes value — the xml-crypto signature-substitution
|
|
1038
|
+
* class, CVE-2025-29774 / CVE-2025-29775) and rejects an expired
|
|
1039
|
+
* declaration (`generatedAt + validityMs < now`) so a stale adherence
|
|
1040
|
+
* cannot be replayed past its window.
|
|
1041
|
+
*
|
|
1042
|
+
* @opts
|
|
1043
|
+
* modelId: string, // required
|
|
1044
|
+
* modelVersion: string, // required
|
|
1045
|
+
* provider: object, // { name, address, contact }
|
|
1046
|
+
* trainingFlops: number, // cumulative training compute; Art. 51(2) presumption at >= 1e25 FLOP
|
|
1047
|
+
* isSystemicRisk: boolean, // operator-asserted systemic-risk designation (Art. 51(1)(b))
|
|
1048
|
+
* designatedSystemicRisk: boolean, // AI-Office-designated systemic risk
|
|
1049
|
+
* copVersion: string, // Code of Practice release label "YYYY-MM" (default "2025-07")
|
|
1050
|
+
* commitments: object[], // [{ article, statement, evidenceHash }]; evidenceHash is b.crypto.sha3Hash output
|
|
1051
|
+
* trainingDataSummary: object,// Art. 53(1)(d) public-summary pointer (b.compliance.aiAct.gpai.trainingDataSummary)
|
|
1052
|
+
* validityMs: number, // validity window; default 90 days
|
|
1053
|
+
* privateKeyPem: string, // required — ML-DSA-87 signing key (b.crypto.generateSigningKeyPair)
|
|
1054
|
+
* serialNumber: string, // urn:uuid:...; defaults to a fresh UUIDv4
|
|
1055
|
+
* audit: boolean, // emit compliance.aiact.gpai.declareadherence audit event; default true
|
|
1056
|
+
*
|
|
1057
|
+
* @example
|
|
1058
|
+
* var pair = b.crypto.generateSigningKeyPair("ml-dsa-87");
|
|
1059
|
+
* var hash = b.crypto.sha3Hash("annex-xi-technical-documentation-v1");
|
|
1060
|
+
* var env = b.compliance.aiAct.gpai.declareAdherence({
|
|
1061
|
+
* modelId: "acme-llm-7b",
|
|
1062
|
+
* modelVersion: "1.0",
|
|
1063
|
+
* commitments: [{ article: "Art. 53(1)(a)", statement: "Annex XI docs maintained", evidenceHash: hash }],
|
|
1064
|
+
* privateKeyPem: pair.privateKey,
|
|
1065
|
+
* });
|
|
1066
|
+
* typeof env.signature; // "string"
|
|
1067
|
+
*/
|
|
1068
|
+
function declareAdherence(opts) {
|
|
1069
|
+
validateOpts.requireObject(opts, "compliance.aiAct.gpai.declareAdherence",
|
|
1070
|
+
ComplianceError, "compliance-ai-act/bad-input");
|
|
1071
|
+
validateOpts(opts, DECLARE_ALLOWED_KEYS, "compliance.aiAct.gpai.declareAdherence");
|
|
1072
|
+
validateOpts.requireNonEmptyString(opts.privateKeyPem,
|
|
1073
|
+
"declareAdherence: privateKeyPem", ComplianceError, "compliance-ai-act/no-signing-key");
|
|
1074
|
+
|
|
1075
|
+
var form = adherenceForm(opts);
|
|
1076
|
+
|
|
1077
|
+
// Scope-downgrade refusal: a SIGNED declaration must cover EVERY
|
|
1078
|
+
// derived obligation with bound evidence. The obligations are derived
|
|
1079
|
+
// from the classifier, not the operator, so a systemic-risk model
|
|
1080
|
+
// (>= 1e25 FLOP, Art. 51(2)) puts Art. 55 in scope — signing a
|
|
1081
|
+
// declaration that omits the Art. 55 chapter would be a silent scope
|
|
1082
|
+
// downgrade. adherenceForm() stays an inspection tool that surfaces
|
|
1083
|
+
// `evidenced: false`; the signing path refuses an unevidenced
|
|
1084
|
+
// obligation outright (CWE-345 insufficient verification).
|
|
1085
|
+
var unevidenced = form.commitments
|
|
1086
|
+
.filter(function (c) { return !c.evidenced; })
|
|
1087
|
+
.map(function (c) { return c.article; });
|
|
1088
|
+
if (unevidenced.length > 0) {
|
|
1089
|
+
throw new ComplianceError("compliance-ai-act/cop-obligation-unevidenced",
|
|
1090
|
+
"declareAdherence: cannot sign — required obligation(s) [" + unevidenced.join(", ") +
|
|
1091
|
+
"] have no bound commitment with a valid evidenceHash. A systemic-risk model must cover " +
|
|
1092
|
+
"the Art. 55 chapter; supply a commitment for every required article (" +
|
|
1093
|
+
form.requiredArticles.join(", ") + ").");
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Compose the AIBOM substrate: the adherence form rides as a
|
|
1097
|
+
// property-bag + Art. 53(1)(d) training-data summary on the model
|
|
1098
|
+
// component, signed as one canonical-JSON-1785 byte stream. We do NOT
|
|
1099
|
+
// hand-roll an envelope or call canonicalJson.stringify + crypto.sign
|
|
1100
|
+
// directly — modelManifest.build/sign already provide CycloneDX 1.6
|
|
1101
|
+
// conformance + the signature-substitution defense in verify.
|
|
1102
|
+
var bom = modelManifest().build({
|
|
1103
|
+
model: {
|
|
1104
|
+
name: form.modelId,
|
|
1105
|
+
version: form.modelVersion,
|
|
1106
|
+
modelCard: {
|
|
1107
|
+
properties: [
|
|
1108
|
+
{ name: "ai-act:gpai-cop-adherence", value: JSON.stringify(form) },
|
|
1109
|
+
],
|
|
1110
|
+
},
|
|
1111
|
+
},
|
|
1112
|
+
serialNumber: opts.serialNumber,
|
|
1113
|
+
tool: { name: "@blamejs/core:compliance.aiAct.gpai.declareAdherence" },
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
var envelope = modelManifest().sign(bom, {
|
|
1117
|
+
privateKeyPem: opts.privateKeyPem,
|
|
1118
|
+
audit: false, // emit our own domain-specific event below
|
|
1119
|
+
});
|
|
1120
|
+
|
|
1121
|
+
// Surface the adherence form alongside the signed envelope so callers
|
|
1122
|
+
// don't have to re-parse the BOM property to read what was declared.
|
|
1123
|
+
var out = {
|
|
1124
|
+
bom: envelope.bom,
|
|
1125
|
+
signature: envelope.signature,
|
|
1126
|
+
adherence: form,
|
|
1127
|
+
};
|
|
1128
|
+
|
|
1129
|
+
if (opts.audit !== false) {
|
|
1130
|
+
// Hot-path audit sink — drop-silent by design (rule 5). A throw
|
|
1131
|
+
// here would crash the caller that just produced a valid signed
|
|
1132
|
+
// declaration.
|
|
1133
|
+
try {
|
|
1134
|
+
audit().safeEmit({
|
|
1135
|
+
action: "compliance.aiact.gpai.declareadherence",
|
|
1136
|
+
outcome: "success",
|
|
1137
|
+
metadata: {
|
|
1138
|
+
modelId: form.modelId,
|
|
1139
|
+
modelVersion: form.modelVersion,
|
|
1140
|
+
isSystemicRisk: form.isSystemicRisk,
|
|
1141
|
+
articles: form.articles,
|
|
1142
|
+
serialNumber: envelope.bom.serialNumber,
|
|
1143
|
+
},
|
|
1144
|
+
});
|
|
1145
|
+
} catch (_e) { /* drop-silent — by design */ }
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
return Object.freeze(out);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
/**
|
|
1152
|
+
* @primitive b.compliance.aiAct.gpai.verifyAdherence
|
|
1153
|
+
* @signature b.compliance.aiAct.gpai.verifyAdherence(envelope, publicKeyPem, opts?)
|
|
1154
|
+
* @since 0.14.11
|
|
1155
|
+
* @status stable
|
|
1156
|
+
* @compliance eu-ai-act-art-11
|
|
1157
|
+
* @related b.compliance.aiAct.gpai.declareAdherence, b.ai.modelManifest.verify
|
|
1158
|
+
*
|
|
1159
|
+
* Verify a signed GPAI Code-of-Practice adherence declaration produced
|
|
1160
|
+
* by `declareAdherence`. Delegates the cryptographic check to
|
|
1161
|
+
* `b.ai.modelManifest.verify`, which re-canonicalizes the BOM with
|
|
1162
|
+
* canonical-JSON-1785 before trusting any field and NEVER trusts an
|
|
1163
|
+
* embedded signed-bytes value (the xml-crypto signature-substitution
|
|
1164
|
+
* class, CVE-2025-29774 / CVE-2025-29775). On a valid signature it
|
|
1165
|
+
* additionally enforces the validity window: a declaration whose
|
|
1166
|
+
* `generatedAt + validityMs` is in the past is rejected with
|
|
1167
|
+
* `reason: "expired"` so a stale adherence cannot be replayed past its
|
|
1168
|
+
* auditor-review window. Returns `{ valid, adherence, reason }`; never
|
|
1169
|
+
* throws (the documented contract mirrors b.ai.modelManifest.verify).
|
|
1170
|
+
*
|
|
1171
|
+
* @opts
|
|
1172
|
+
* now: number, // override the comparison clock (ms epoch); default Date.now()
|
|
1173
|
+
* audit: boolean, // emit compliance.aiact.gpai.verifyadherence audit event; default true
|
|
1174
|
+
*
|
|
1175
|
+
* @example
|
|
1176
|
+
* var result = b.compliance.aiAct.gpai.verifyAdherence(env, pair.publicKey);
|
|
1177
|
+
* if (result.valid) result.adherence.requiredArticles; // ["Art. 53(1)(a)", ...]
|
|
1178
|
+
* else result.reason; // "signature-invalid" | "expired" | ...
|
|
1179
|
+
*/
|
|
1180
|
+
function verifyAdherence(envelope, publicKeyPem, opts) {
|
|
1181
|
+
opts = opts || {};
|
|
1182
|
+
var inner = modelManifest().verify(envelope, publicKeyPem, { audit: false });
|
|
1183
|
+
if (!inner.valid) {
|
|
1184
|
+
return { valid: false, adherence: null, reason: inner.reason };
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// Re-read the adherence form from the (now signature-verified) BOM
|
|
1188
|
+
// property — never from a top-level field a caller could swap.
|
|
1189
|
+
var adherence = _extractAdherenceFromBom(inner.bom);
|
|
1190
|
+
if (!adherence) {
|
|
1191
|
+
return { valid: false, adherence: null, reason: "adherence-property-missing" };
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// Anti-replay: reject an expired declaration.
|
|
1195
|
+
if (typeof adherence.generatedAt === "string" &&
|
|
1196
|
+
typeof adherence.validityMs === "number" && isFinite(adherence.validityMs)) {
|
|
1197
|
+
var issuedMs = Date.parse(adherence.generatedAt);
|
|
1198
|
+
if (isFinite(issuedMs)) {
|
|
1199
|
+
var now = typeof opts.now === "number" && isFinite(opts.now) ? opts.now : Date.now();
|
|
1200
|
+
if (issuedMs + adherence.validityMs < now) {
|
|
1201
|
+
return { valid: false, adherence: null, reason: "expired" };
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (opts.audit !== false) {
|
|
1207
|
+
try {
|
|
1208
|
+
audit().safeEmit({
|
|
1209
|
+
action: "compliance.aiact.gpai.verifyadherence",
|
|
1210
|
+
outcome: "success",
|
|
1211
|
+
metadata: {
|
|
1212
|
+
modelId: adherence.modelId,
|
|
1213
|
+
modelVersion: adherence.modelVersion,
|
|
1214
|
+
isSystemicRisk: adherence.isSystemicRisk,
|
|
1215
|
+
serialNumber: inner.bom && inner.bom.serialNumber,
|
|
1216
|
+
},
|
|
1217
|
+
});
|
|
1218
|
+
} catch (_e) { /* drop-silent — by design */ }
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
return { valid: true, adherence: adherence, reason: null };
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
// Pull the adherence form out of the signed BOM's model-card property
|
|
1225
|
+
// bag. Returns null on any shape mismatch — the caller maps that to a
|
|
1226
|
+
// structured verify reason rather than throwing.
|
|
1227
|
+
function _extractAdherenceFromBom(bom) {
|
|
1228
|
+
try {
|
|
1229
|
+
var card = bom && bom.metadata && bom.metadata.component && bom.metadata.component.modelCard;
|
|
1230
|
+
var props = card && card.properties;
|
|
1231
|
+
if (!Array.isArray(props)) return null;
|
|
1232
|
+
for (var i = 0; i < props.length; i += 1) {
|
|
1233
|
+
if (props[i] && props[i].name === "ai-act:gpai-cop-adherence") {
|
|
1234
|
+
return safeJson.parse(props[i].value, { maxBytes: C.BYTES.mib(1) });
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
} catch (_e) { return null; }
|
|
1238
|
+
return null;
|
|
1239
|
+
}
|
|
1240
|
+
|
|
798
1241
|
module.exports = {
|
|
799
1242
|
classify: classify,
|
|
800
1243
|
deployerChecklist: deployerChecklist,
|
|
@@ -806,6 +1249,9 @@ module.exports = {
|
|
|
806
1249
|
classify: gpaiClassify,
|
|
807
1250
|
listObligations: listGpaiObligations,
|
|
808
1251
|
trainingDataSummary: trainingDataSummary,
|
|
1252
|
+
adherenceForm: adherenceForm,
|
|
1253
|
+
declareAdherence: declareAdherence,
|
|
1254
|
+
verifyAdherence: verifyAdherence,
|
|
809
1255
|
OBLIGATIONS: GPAI_OBLIGATIONS,
|
|
810
1256
|
},
|
|
811
1257
|
articleObligations: articleObligations,
|