@zanii/certify 0.6.1 → 0.7.1
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/dist/bundle.d.ts +12 -1
- package/dist/bundle.js +23 -4
- package/dist/certificate.d.ts +7 -1
- package/dist/certificate.js +11 -4
- package/dist/cli-messages.json +30 -2
- package/dist/cli.js +37 -1
- package/dist/formats/status-v3.schema.json +31 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/production.d.ts +90 -0
- package/dist/production.js +117 -0
- package/dist/profile.js +1 -1
- package/dist/schemas/profile.schema.json +1 -1
- package/dist/status.d.ts +8 -3
- package/dist/status.js +28 -22
- package/package.json +1 -1
package/dist/bundle.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type Staff } from "./impartiality.ts";
|
|
2
2
|
import { type CategoryScore } from "./scoring.ts";
|
|
3
3
|
import { type ApplianceExecution, type ApplianceExportIdentity } from "./appliance-execution.ts";
|
|
4
|
+
import { type AssessmentEvidence, type Declaration, type EndpointBinding } from "./production.ts";
|
|
4
5
|
export type { ApplianceExecution, ApplianceExportIdentity } from "./appliance-execution.ts";
|
|
5
6
|
export interface CategoryCount {
|
|
6
7
|
category: string;
|
|
@@ -45,11 +46,20 @@ export interface BundleInput {
|
|
|
45
46
|
} | null;
|
|
46
47
|
/** Section 2a (scheme 0.4): the categories assessed as simulated multi-step workflows, and the agent interface tested. */
|
|
47
48
|
assessment?: Assessment | null;
|
|
49
|
+
/** Section 4b (scheme 0.5): the applicant's signed declaration (production-binding.md section 2). */
|
|
50
|
+
declaration?: Declaration | null;
|
|
48
51
|
}
|
|
49
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* What a 0.4 or 0.5 run assessed: `voice_text` is a voice agent tested through its text interface
|
|
54
|
+
* only, never its audio. Under 0.5 also the production evidence and the endpoint binding.
|
|
55
|
+
*/
|
|
50
56
|
export interface Assessment {
|
|
51
57
|
trajectory_categories: string[];
|
|
52
58
|
interface: "text" | "voice_text";
|
|
59
|
+
/** Scheme 0.5 only: `self_reported` (Level 1 only), `attested` or `blackbox`. */
|
|
60
|
+
evidence?: AssessmentEvidence;
|
|
61
|
+
/** Scheme 0.5 only: the pinned TLS key of the endpoint and whether it answered the run's nonce. */
|
|
62
|
+
endpoint?: EndpointBinding;
|
|
53
63
|
}
|
|
54
64
|
export interface ResultBundle {
|
|
55
65
|
execution?: ApplianceExecution;
|
|
@@ -77,6 +87,7 @@ export interface ResultBundle {
|
|
|
77
87
|
covered_by: string;
|
|
78
88
|
};
|
|
79
89
|
assessment?: Assessment;
|
|
90
|
+
declaration?: Declaration;
|
|
80
91
|
}
|
|
81
92
|
export interface ApplianceExportBundle extends Omit<ResultBundle, "v" | "staff" | "execution" | "partial"> {
|
|
82
93
|
v: 2;
|
package/dist/bundle.js
CHANGED
|
@@ -5,6 +5,7 @@ import { staffRefusals } from "./impartiality.js";
|
|
|
5
5
|
import { scoreCategory } from "./scoring.js";
|
|
6
6
|
import { isDid, isInt, isIso, isSha, ms, SLUG, sortedUnique } from "./shared.js";
|
|
7
7
|
import { applianceExecution, applianceExportIdentity } from "./appliance-execution.js";
|
|
8
|
+
import { ASSESSMENT_EVIDENCE, endpointBindingOk, PRODUCTION_SCHEME, verifyDeclaration } from "./production.js";
|
|
8
9
|
const CATEGORY = /^[a-z][a-z0-9_]{0,63}$/;
|
|
9
10
|
const FINDING = /^[A-Z][A-Z0-9_]{0,63}$/;
|
|
10
11
|
const CASE_ID = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
@@ -71,6 +72,7 @@ function normalizedBundle(input, source) {
|
|
|
71
72
|
if (partial !== null && (Object.keys(partial).length !== 1 || !isSha(partial.covered_by)))
|
|
72
73
|
throw new Error("partial must be {covered_by: <bundle hash>}");
|
|
73
74
|
const assessment = input.assessment == null ? null : assessmentOf(input.assessment, categories, scheme);
|
|
75
|
+
const declaration = input.declaration == null ? null : declarationOf(input.declaration, input.profile, input.level, scheme);
|
|
74
76
|
const execution = input.execution == null ? undefined : applianceExecution(input.execution), machine = source?.appliance ?? execution?.appliance;
|
|
75
77
|
if (machine && [...testers, reviewer, decider].includes(machine))
|
|
76
78
|
throw new Error("an appliance cannot be recorded as human staff");
|
|
@@ -91,6 +93,7 @@ function normalizedBundle(input, source) {
|
|
|
91
93
|
...(partial ? { partial: { covered_by: partial.covered_by } } : {}),
|
|
92
94
|
...(execution ? { execution } : {}),
|
|
93
95
|
...(assessment ? { assessment } : {}),
|
|
96
|
+
...(declaration ? { declaration } : {}),
|
|
94
97
|
};
|
|
95
98
|
if (source) {
|
|
96
99
|
const { partial: ignored, ...base } = bundle;
|
|
@@ -99,15 +102,31 @@ function normalizedBundle(input, source) {
|
|
|
99
102
|
return bundle;
|
|
100
103
|
}
|
|
101
104
|
function assessmentOf(a, categories, scheme) {
|
|
102
|
-
|
|
105
|
+
const v05 = scheme === PRODUCTION_SCHEME;
|
|
106
|
+
if (scheme !== "zanii-certify/0.4" && !v05)
|
|
103
107
|
throw new Error("assessment needs scheme zanii-certify/0.4");
|
|
104
|
-
|
|
108
|
+
const keys = v05 ? "endpoint,evidence,interface,trajectory_categories" : "interface,trajectory_categories";
|
|
109
|
+
if (typeof a !== "object" || Object.keys(a).sort().join() !== keys || !Array.isArray(a.trajectory_categories)
|
|
105
110
|
|| (a.interface !== "text" && a.interface !== "voice_text"))
|
|
106
|
-
throw new Error("assessment must be {trajectory_categories, interface}");
|
|
111
|
+
throw new Error(v05 ? "assessment must be {trajectory_categories, interface, evidence, endpoint}" : "assessment must be {trajectory_categories, interface}");
|
|
107
112
|
const workflow = sortedUnique(a.trajectory_categories);
|
|
108
113
|
if (workflow.length !== a.trajectory_categories.length || !workflow.every((c) => categories.includes(c)))
|
|
109
114
|
throw new Error("assessment.trajectory_categories must be distinct planned categories");
|
|
110
|
-
|
|
115
|
+
if (!v05)
|
|
116
|
+
return { trajectory_categories: workflow, interface: a.interface };
|
|
117
|
+
if (!ASSESSMENT_EVIDENCE.includes(a.evidence))
|
|
118
|
+
throw new Error("assessment.evidence must be self_reported, attested or blackbox");
|
|
119
|
+
if (!endpointBindingOk(a.endpoint))
|
|
120
|
+
throw new Error("assessment.endpoint must be {spki_sha256, proved}");
|
|
121
|
+
return { trajectory_categories: workflow, interface: a.interface, evidence: a.evidence, endpoint: { spki_sha256: a.endpoint.spki_sha256, proved: a.endpoint.proved } };
|
|
122
|
+
}
|
|
123
|
+
function declarationOf(d, profile, level, scheme) {
|
|
124
|
+
if (scheme !== PRODUCTION_SCHEME)
|
|
125
|
+
throw new Error("declaration needs scheme zanii-certify/0.5");
|
|
126
|
+
const check = verifyDeclaration(d, { profile, level, now: "9999-12-31T23:59:59.999Z" });
|
|
127
|
+
if (!check.ok)
|
|
128
|
+
throw new Error(`declaration: ${check.reasons[0]}`);
|
|
129
|
+
return structuredClone(d);
|
|
111
130
|
}
|
|
112
131
|
/** Existing v1 output is unchanged unless explicit verified appliance provenance is supplied. */
|
|
113
132
|
export function buildBundle(input) { return normalizedBundle(input); }
|
package/dist/certificate.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { type StatusDocument, type StatusEntry, type StatusWitness } from "./sta
|
|
|
10
10
|
import { type IssuerPolicy, type ReleaseVerificationPolicy } from "./verification-policy.ts";
|
|
11
11
|
import { type AuditRenewalPolicy } from "./audit-renewal.ts";
|
|
12
12
|
/** scheme.md section 8: every scheme version a certificate or bundle may name. */
|
|
13
|
-
export declare const SCHEMES: readonly ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3", "zanii-certify/0.4"];
|
|
13
|
+
export declare const SCHEMES: readonly ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3", "zanii-certify/0.4", "zanii-certify/0.5"];
|
|
14
14
|
export type Scheme = (typeof SCHEMES)[number];
|
|
15
15
|
/** The current scheme. */
|
|
16
16
|
export declare const SCHEME: Scheme;
|
|
@@ -18,6 +18,8 @@ export declare const LEVELS: readonly [1, 2, 3];
|
|
|
18
18
|
export type Level = (typeof LEVELS)[number];
|
|
19
19
|
/** scheme.md section 2: the levels each scheme version issues. */
|
|
20
20
|
export declare const SCHEME_LEVELS: Record<Scheme, readonly Level[]>;
|
|
21
|
+
/** scheme.md section 8: `scheme` is `min` or a later version (false for an unknown scheme). */
|
|
22
|
+
export declare const schemeFrom: (scheme: unknown, min: Scheme) => boolean;
|
|
21
23
|
/**
|
|
22
24
|
* scheme.md section 8: the scheme a certificate or bundle is issued under. Given, it must issue
|
|
23
25
|
* `level` (and allow 0.2 features when `needs02`, 0.3 features when `needs03`); absent, 0.3 with a
|
|
@@ -28,6 +30,10 @@ export declare const CONFLICTS: readonly ["blackbox_customer", "ledger_customer"
|
|
|
28
30
|
export type Conflict = (typeof CONFLICTS)[number];
|
|
29
31
|
/** scheme.md section 5: the default re-test interval per level. */
|
|
30
32
|
export declare const SURVEILLANCE: Record<Level, string>;
|
|
33
|
+
/** scheme.md section 6: a suspension not cleared within 90 days is due for revocation (a decider revokes, never the lab's surveillance). */
|
|
34
|
+
export declare const SUSPENSION_REVOCATION_DAYS = 90;
|
|
35
|
+
/** scheme.md section 6: the applicant is warned on day 60 of a suspension not cleared. */
|
|
36
|
+
export declare const SUSPENSION_WARNING_DAYS = 60;
|
|
31
37
|
/** scheme.md section 4: validity is 365 days from issue. */
|
|
32
38
|
export declare const VALIDITY_DAYS = 365;
|
|
33
39
|
export declare const DOMAIN: RegExp;
|
package/dist/certificate.js
CHANGED
|
@@ -16,9 +16,9 @@ import { verifyAuditRenewal } from "./audit-renewal.js";
|
|
|
16
16
|
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
17
17
|
import { signingIdentityReasons } from "./signing-identity.js";
|
|
18
18
|
/** scheme.md section 8: every scheme version a certificate or bundle may name. */
|
|
19
|
-
export const SCHEMES = ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3", "zanii-certify/0.4"];
|
|
19
|
+
export const SCHEMES = ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3", "zanii-certify/0.4", "zanii-certify/0.5"];
|
|
20
20
|
/** The current scheme. */
|
|
21
|
-
export const SCHEME = "zanii-certify/0.
|
|
21
|
+
export const SCHEME = "zanii-certify/0.5";
|
|
22
22
|
export const LEVELS = [1, 2, 3];
|
|
23
23
|
/** scheme.md section 2: the levels each scheme version issues. */
|
|
24
24
|
export const SCHEME_LEVELS = {
|
|
@@ -26,7 +26,10 @@ export const SCHEME_LEVELS = {
|
|
|
26
26
|
"zanii-certify/0.2": [1, 2, 3],
|
|
27
27
|
"zanii-certify/0.3": [1, 2, 3],
|
|
28
28
|
"zanii-certify/0.4": [1, 2, 3],
|
|
29
|
+
"zanii-certify/0.5": [1, 2, 3],
|
|
29
30
|
};
|
|
31
|
+
/** scheme.md section 8: `scheme` is `min` or a later version (false for an unknown scheme). */
|
|
32
|
+
export const schemeFrom = (scheme, min) => SCHEMES.includes(scheme) && SCHEMES.indexOf(scheme) >= SCHEMES.indexOf(min);
|
|
30
33
|
/**
|
|
31
34
|
* scheme.md section 8: the scheme a certificate or bundle is issued under. Given, it must issue
|
|
32
35
|
* `level` (and allow 0.2 features when `needs02`, 0.3 features when `needs03`); absent, 0.3 with a
|
|
@@ -40,7 +43,7 @@ export function pickScheme(scheme, level, needs02 = false, needs03 = false) {
|
|
|
40
43
|
throw new Error(`level ${level} is not issued under ${s}`);
|
|
41
44
|
if (needs02 && s === "zanii-certify/0.1")
|
|
42
45
|
throw new Error("a post-quantum binding needs scheme zanii-certify/0.2");
|
|
43
|
-
if (needs03 && s
|
|
46
|
+
if (needs03 && !schemeFrom(s, "zanii-certify/0.3"))
|
|
44
47
|
throw new Error("a licence needs scheme zanii-certify/0.3 or later");
|
|
45
48
|
return s;
|
|
46
49
|
}
|
|
@@ -53,6 +56,10 @@ export const CONFLICTS = [
|
|
|
53
56
|
];
|
|
54
57
|
/** scheme.md section 5: the default re-test interval per level. */
|
|
55
58
|
export const SURVEILLANCE = { 1: "P30D", 2: "P90D", 3: "P90D" };
|
|
59
|
+
/** scheme.md section 6: a suspension not cleared within 90 days is due for revocation (a decider revokes, never the lab's surveillance). */
|
|
60
|
+
export const SUSPENSION_REVOCATION_DAYS = 90;
|
|
61
|
+
/** scheme.md section 6: the applicant is warned on day 60 of a suspension not cleared. */
|
|
62
|
+
export const SUSPENSION_WARNING_DAYS = 60;
|
|
56
63
|
/** scheme.md section 4: validity is 365 days from issue. */
|
|
57
64
|
export const VALIDITY_DAYS = 365;
|
|
58
65
|
export const DOMAIN = /^(?=.{1,253}$)([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63}$/;
|
|
@@ -171,7 +178,7 @@ export function claimReasons(cert) {
|
|
|
171
178
|
out.push("claim audit is an audit attestation hash, required at level 3 and only there");
|
|
172
179
|
if ("pq_binding" in c && (!scheme || scheme === "zanii-certify/0.1" || !isSha(c.pq_binding)))
|
|
173
180
|
out.push("claim pq_binding needs scheme zanii-certify/0.2 and a binding hash");
|
|
174
|
-
if ("licence" in c && ((scheme
|
|
181
|
+
if ("licence" in c && (!schemeFrom(scheme, "zanii-certify/0.3") || !isSha(c.licence)))
|
|
175
182
|
out.push("claim licence needs scheme zanii-certify/0.3 or later and a licence hash");
|
|
176
183
|
return out;
|
|
177
184
|
}
|
package/dist/cli-messages.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"en": {
|
|
3
|
-
"usage_v2": "certify CLI help v2\nusage: certify verify <cert.json> [--status <file>] [--issuer-doc <file>] [--fingerprint <file>] [--online <base url>] [--now <time>] [--lang en|ar] [--pq <file>|off]\n [--audit <file>] [--trusted-bodies <file>] [--body-register <file>] [--status-witness <file>] [--release-policy <file>] [--strict-policy <file>]\n certify fingerprint --model <model> [--prompt-file <file>] [--tools-file <file>] [--policy-file <file>] [--knowledge-file <file>]\n--strict-policy pins issuer keys and PQ requirements independently. --pq off is incompatible with it. See spec/cli.md for policy file schemas. Legacy help: --help; current help: --help-v2.",
|
|
3
|
+
"usage_v2": "certify CLI help v2\nusage: certify verify <cert.json> [--status <file>] [--issuer-doc <file>] [--fingerprint <file>] [--online <base url>] [--now <time>] [--lang en|ar] [--pq <file>|off]\n [--audit <file>] [--trusted-bodies <file>] [--body-register <file>] [--status-witness <file>] [--release-policy <file>] [--strict-policy <file>] [--bundle <file>]\n certify fingerprint --model <model> [--prompt-file <file>] [--tools-file <file>] [--policy-file <file>] [--knowledge-file <file>]\n--strict-policy pins issuer keys and PQ requirements independently. --pq off is incompatible with it. See spec/cli.md for policy file schemas. Legacy help: --help; current help: --help-v2.",
|
|
4
4
|
"bad_policy": "invalid verification input for {0}; see certify --help-v2",
|
|
5
5
|
"issuer_resolution": "issuer resolution failed ({0}); this is not proof that the key is unpublished",
|
|
6
6
|
"audit_ok": "attestation and trusted body verified",
|
|
@@ -68,12 +68,26 @@
|
|
|
68
68
|
"pq_bad": "invalid",
|
|
69
69
|
"pq_missing": "required by the issuer document, not supplied (use --pq)",
|
|
70
70
|
"pq_optout": "not checked: the issuer document requires it, opted out with --pq off",
|
|
71
|
+
"evidence": "Evidence: {0}",
|
|
72
|
+
"ev_attested": "hardware-attested beacon (spec/beacon.md section 5)",
|
|
73
|
+
"ev_blackbox": "verified by Zanii Blackbox live evidence from an enrolled gateway",
|
|
74
|
+
"ev_self_reported": "self-reported configuration (Level 1 only; weaker than attested evidence)",
|
|
75
|
+
"ev_not_stated": "not stated (scheme 0.4 or earlier)",
|
|
76
|
+
"ev_skip": "not checked (use --bundle <file> or --online)",
|
|
77
|
+
"ev_mismatch": "not checked (the result bundle does not match the certificate)",
|
|
78
|
+
"ev_fetch": "not checked (could not fetch {0})",
|
|
79
|
+
"workflows": "Multi-step workflows: {0}",
|
|
80
|
+
"workflows_none": "none",
|
|
81
|
+
"interface": "Interface: {0}",
|
|
82
|
+
"iface_text": "text",
|
|
83
|
+
"iface_voice_text": "voice agent, through its text interface only (audio not assessed)",
|
|
84
|
+
"offline_warning": "warning: checked offline. A certificate file alone does not prove it is valid now; run with --online <base url> to read its live status.",
|
|
71
85
|
"result_valid": "Result: VALID",
|
|
72
86
|
"result_invalid": "Result: INVALID",
|
|
73
87
|
"result_partial": "Result: NOT FULLY CHECKED"
|
|
74
88
|
},
|
|
75
89
|
"ar": {
|
|
76
|
-
"usage_v2": "مساعدة certify، الإصدار 2\nusage: certify verify <cert.json> [--status <file>] [--issuer-doc <file>] [--fingerprint <file>] [--online <base url>] [--now <time>] [--lang en|ar] [--pq <file>|off]\n [--audit <file>] [--trusted-bodies <file>] [--body-register <file>] [--status-witness <file>] [--release-policy <file>] [--strict-policy <file>]\n certify fingerprint --model <model> [--prompt-file <file>] [--tools-file <file>] [--policy-file <file>] [--knowledge-file <file>]\nيثبت --strict-policy مفاتيح الجهة المصدرة ومتطلبات التوقيع المقاوم للحوسبة الكمية بصورة مستقلة، ولا يقبل --pq off. صيغ الملفات في spec/cli.md. المساعدة السابقة: --help؛ المساعدة الحالية: --help-v2.",
|
|
90
|
+
"usage_v2": "مساعدة certify، الإصدار 2\nusage: certify verify <cert.json> [--status <file>] [--issuer-doc <file>] [--fingerprint <file>] [--online <base url>] [--now <time>] [--lang en|ar] [--pq <file>|off]\n [--audit <file>] [--trusted-bodies <file>] [--body-register <file>] [--status-witness <file>] [--release-policy <file>] [--strict-policy <file>] [--bundle <file>]\n certify fingerprint --model <model> [--prompt-file <file>] [--tools-file <file>] [--policy-file <file>] [--knowledge-file <file>]\nيثبت --strict-policy مفاتيح الجهة المصدرة ومتطلبات التوقيع المقاوم للحوسبة الكمية بصورة مستقلة، ولا يقبل --pq off. صيغ الملفات في spec/cli.md. المساعدة السابقة: --help؛ المساعدة الحالية: --help-v2.",
|
|
77
91
|
"bad_policy": "مدخل تحقق غير صالح للخيار {0}؛ راجع certify --help-v2",
|
|
78
92
|
"issuer_resolution": "تعذر التحقق من نطاق الجهة المصدرة ({0})؛ لا يثبت ذلك أن المفتاح غير منشور",
|
|
79
93
|
"audit_ok": "تم التحقق من شهادة التدقيق والجهة الموثوقة",
|
|
@@ -141,6 +155,20 @@
|
|
|
141
155
|
"pq_bad": "غير صالح",
|
|
142
156
|
"pq_missing": "تشترطه وثيقة الجهة المصدرة ولم يقدم (استخدم --pq)",
|
|
143
157
|
"pq_optout": "لم يتحقق منه: تشترطه وثيقة الجهة المصدرة، وتم التنازل عنه بالخيار --pq off",
|
|
158
|
+
"evidence": "الأدلة: {0}",
|
|
159
|
+
"ev_attested": "نبضة موثّقة بشهادة العتاد (spec/beacon.md القسم 5)",
|
|
160
|
+
"ev_blackbox": "تحقّقت منها أدلة Zanii Blackbox الحية من بوابة مسجّلة",
|
|
161
|
+
"ev_self_reported": "إعداد مُعلن ذاتياً (المستوى 1 فقط؛ أضعف من الأدلة الموثّقة)",
|
|
162
|
+
"ev_not_stated": "غير مذكور (المخطط 0.4 أو أقدم)",
|
|
163
|
+
"ev_skip": "لم يفحص (استخدم --bundle <file> أو --online)",
|
|
164
|
+
"ev_mismatch": "لم يفحص (حزمة النتائج لا تطابق الشهادة)",
|
|
165
|
+
"ev_fetch": "لم يفحص (تعذر جلب {0})",
|
|
166
|
+
"workflows": "سير العمل متعدد الخطوات: {0}",
|
|
167
|
+
"workflows_none": "لا يوجد",
|
|
168
|
+
"interface": "الواجهة: {0}",
|
|
169
|
+
"iface_text": "نصية",
|
|
170
|
+
"iface_voice_text": "وكيل صوتي عبر واجهته النصية فقط (لم يُقيَّم الصوت)",
|
|
171
|
+
"offline_warning": "تحذير: فُحصت دون اتصال. ملف الشهادة وحده لا يثبت أنها سارية الآن؛ شغّل الأمر مع --online <base url> لقراءة حالتها الحية.",
|
|
144
172
|
"result_valid": "النتيجة: صالحة",
|
|
145
173
|
"result_invalid": "النتيجة: غير صالحة",
|
|
146
174
|
"result_partial": "النتيجة: لم يكتمل الفحص"
|
package/dist/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { canonicalBytes } from "@zanii/core";
|
|
|
5
5
|
import { credentialHash, resolveIssuer, } from "@zanii/credentials";
|
|
6
6
|
import { claimReasons, verifyCertificate } from "./certificate.js";
|
|
7
7
|
import { verifyAuditAttestation } from "./audit.js";
|
|
8
|
+
import { bundleHash } from "./bundle.js";
|
|
8
9
|
import messages from "./cli-messages.json" with { type: "json" };
|
|
9
10
|
import { buildFingerprint, promptSha256, sha256Bytes, toolsSha256 } from "./fingerprint.js";
|
|
10
11
|
import { pqPolicyOf } from "./pq.js";
|
|
@@ -18,7 +19,7 @@ import { verifyCertificateVc } from "./vc.js";
|
|
|
18
19
|
/** spec/cli.md section 4. */
|
|
19
20
|
export const EXIT = { valid: 0, invalid: 1, partial: 2, usage: 3, online: 4 };
|
|
20
21
|
const OPTIONS = {
|
|
21
|
-
verify: ["--status", "--issuer-doc", "--fingerprint", "--online", "--now", "--lang", "--pq", "--audit", "--trusted-bodies", "--body-register", "--status-witness", "--release-policy", "--strict-policy"],
|
|
22
|
+
verify: ["--status", "--issuer-doc", "--fingerprint", "--online", "--now", "--lang", "--pq", "--audit", "--trusted-bodies", "--body-register", "--status-witness", "--release-policy", "--strict-policy", "--bundle"],
|
|
22
23
|
fingerprint: ["--model", "--prompt-file", "--tools-file", "--policy-file", "--knowledge-file", "--lang"],
|
|
23
24
|
};
|
|
24
25
|
// Control and bidirectional formatting characters from input files are not written to the terminal.
|
|
@@ -351,6 +352,41 @@ export async function main(argv, io = {}) {
|
|
|
351
352
|
else
|
|
352
353
|
check("pq", "fail", t(bad.every((x) => x.endsWith("a post-quantum signature is required")) ? "pq_missing" : "pq_bad"));
|
|
353
354
|
}
|
|
355
|
+
// spec/cli.md section 7: the result bundle's evidence level and scope, informational (no exit code change)
|
|
356
|
+
let bundle = opts["--bundle"] === undefined ? undefined : json(opts["--bundle"]);
|
|
357
|
+
let bundleFailed;
|
|
358
|
+
if (bundle === undefined && base !== undefined) {
|
|
359
|
+
let url = base;
|
|
360
|
+
try {
|
|
361
|
+
url = new URL(`/v1/certificates/${encodeURIComponent(credentialHash(cert))}/bundle`, base).href;
|
|
362
|
+
const response = await fetchJsonBounded(url, { fetch: doFetch, maxBytes: 8_388_608 });
|
|
363
|
+
if (response.ok)
|
|
364
|
+
bundle = response.document;
|
|
365
|
+
else
|
|
366
|
+
bundleFailed = url;
|
|
367
|
+
}
|
|
368
|
+
catch {
|
|
369
|
+
bundleFailed = url;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (bundleFailed !== undefined)
|
|
373
|
+
lines.push(t("evidence", t("ev_fetch", bundleFailed)));
|
|
374
|
+
else if (bundle === undefined)
|
|
375
|
+
lines.push(t("evidence", t("ev_skip")));
|
|
376
|
+
else if (!isObj(bundle) || bundleHash(bundle) !== claim.bundle)
|
|
377
|
+
lines.push(t("evidence", t("ev_mismatch")));
|
|
378
|
+
else {
|
|
379
|
+
const a = isObj(bundle.assessment) ? bundle.assessment : undefined;
|
|
380
|
+
const e = a?.evidence;
|
|
381
|
+
lines.push(t("evidence", t(e === "attested" || e === "blackbox" || e === "self_reported" ? `ev_${e}` : "ev_not_stated")));
|
|
382
|
+
if (a) {
|
|
383
|
+
const cats = Array.isArray(a.trajectory_categories) ? a.trajectory_categories.map(show).join(", ") : "";
|
|
384
|
+
lines.push(t("workflows", cats || t("workflows_none")));
|
|
385
|
+
lines.push(t("interface", t(a.interface === "voice_text" ? "iface_voice_text" : "iface_text")));
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
if (base === undefined)
|
|
389
|
+
err(`${t("offline_warning")}\n`);
|
|
354
390
|
// A reason no line explains (none is expected) still makes the certificate invalid.
|
|
355
391
|
const failed = marks.includes("fail") || (!marks.includes("skip") && !r.ok);
|
|
356
392
|
const code = failed ? EXIT.invalid : marks.includes("skip") ? EXIT.partial : EXIT.valid;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://zanii.agency/certify/formats/status-v3.schema.json",
|
|
4
|
+
"title": "Zanii Certify status version 3",
|
|
5
|
+
"$comment": "Use a complete draft 2020-12 validator for this standalone format schema. SDK verification additionally checks actual DIDs, signatures, cross-field times, trust, PQ and witness policy.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["v", "type", "issuer", "entries", "issued_at", "valid_until", "signer", "sig"],
|
|
8
|
+
"additionalProperties": false,
|
|
9
|
+
"properties": {
|
|
10
|
+
"v": { "const": 3 }, "type": { "const": "certify.status" },
|
|
11
|
+
"issuer": { "$ref": "#/$defs/did" }, "signer": { "$ref": "#/$defs/did" },
|
|
12
|
+
"sig": { "type": "string", "pattern": "^ed25519:[0-9a-f]{128}$", "maxLength": 136 },
|
|
13
|
+
"issued_at": { "$ref": "#/$defs/time" }, "valid_until": { "$ref": "#/$defs/time" },
|
|
14
|
+
"entries": { "type": "object", "maxProperties": 100000, "patternProperties": { "^sha256:[0-9a-f]{64}$": { "$ref": "#/$defs/entry" } }, "additionalProperties": false },
|
|
15
|
+
"seq": { "type": "integer", "minimum": 0, "maximum": 9007199254740991 },
|
|
16
|
+
"prev": { "anyOf": [{ "type": "null" }, { "$ref": "#/$defs/hash" }] },
|
|
17
|
+
"pq_binding": { "$ref": "#/$defs/hash" }
|
|
18
|
+
},
|
|
19
|
+
"dependentRequired": { "seq": ["prev"], "prev": ["seq"] },
|
|
20
|
+
"allOf": [{ "if": { "required": ["seq"], "properties": { "seq": { "const": 0 } } }, "then": { "properties": { "prev": { "type": "null" } } }, "else": { "properties": { "prev": { "$ref": "#/$defs/hash" } } } }],
|
|
21
|
+
"$defs": {
|
|
22
|
+
"did": { "type": "string", "pattern": "^did:key:z[1-9A-HJ-NP-Za-km-z]{40,60}$", "maxLength": 69 },
|
|
23
|
+
"hash": { "type": "string", "pattern": "^sha256:[0-9a-f]{64}$", "maxLength": 71 },
|
|
24
|
+
"time": { "type": "string", "format": "date-time", "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(\\.[0-9]{3})?Z$", "maxLength": 24 },
|
|
25
|
+
"reason": { "enum": ["FINGERPRINT_MISMATCH", "RETEST_OVERDUE", "RETEST_FAILED", "INCIDENT", "APPLICANT_REQUEST", "ISSUER_ERROR", "LICENCE_WITHDRAWN", "AUDIT_EXPIRED", "MARK_MISUSE", "FALSE_DECLARATION", "EVIDENCE_LAPSED"] },
|
|
26
|
+
"entry": { "type": "object", "required": ["status", "reason", "since"], "additionalProperties": false,
|
|
27
|
+
"properties": { "status": { "enum": ["valid", "suspended", "revoked"] }, "reason": { "anyOf": [{ "type": "null" }, { "$ref": "#/$defs/reason" }] }, "since": { "$ref": "#/$defs/time" } },
|
|
28
|
+
"oneOf": [{ "properties": { "status": { "const": "valid" }, "reason": { "type": "null" } } }, { "properties": { "status": { "enum": ["suspended", "revoked"] }, "reason": { "$ref": "#/$defs/reason" } } }]
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,8 +14,9 @@ export { buildFeedPage, type FederatedFeed, type FeedCheck, type FeedConflict, t
|
|
|
14
14
|
export { buildLicence, buildLicenceStatus, buildRoyaltyStatement, certificateListHash, countersignRoyaltyStatement, LICENCE_ACTIONS, LICENCE_REASONS, type Licence, type LicenceAction, type LicenceCheck, type LicenceInput, type LicenceOptions, type LicenceReason, type LicenceScope, type LicenceState, type LicenceStatus, licenceCoverage, licenceReasons, licensedKey, licenceStateAt, MAX_ROYALTY_MINOR, MAX_STATEMENT_CERTIFICATES, type RoyaltyStatement, type RoyaltyStatementBody, type RoyaltyStatementInput, verifyLicence, verifyLicenceLog, verifyRoyaltyStatement, } from "./licence.ts";
|
|
15
15
|
export { type AttestedBeaconCheck, type BeaconAttestation, type BeaconCheck, type BeaconReport, beaconChallenge, buildBeacon, MAX_SKEW_SECONDS, NONCE, verifyAttestedBeacon, verifyBeacon, } from "./beacon.ts";
|
|
16
16
|
export { type BundleInput, type ApplianceExecution, type ApplianceExportIdentity, type ApplianceExportBundle, type ApplianceExportBundleInput, bundleHash, buildBundle, buildApplianceExportBundle, type CategoryCount, type CategoryResult, type Evidence, type ResultBundle, type Assessment, } from "./bundle.ts";
|
|
17
|
+
export { ASSESSMENT_EVIDENCE, type AssessmentEvidence, buildDeclaration, buildEndpointProof, type Declaration, type DeclarationCheck, type DeclarationInput, DECLARATION_STATEMENT, declarationHash, ENDPOINT_NONCE_HEADER, ENDPOINT_PROOF_HEADER, type EndpointBinding, endpointChallenge, EVIDENCE_GRACE_DAYS, EVIDENCE_WINDOW_DAYS, evidenceLevels, PRODUCTION_SCHEME, productionBindingReasons, verifyDeclaration, verifyEndpointProof, } from "./production.ts";
|
|
17
18
|
export { applianceExecution, applianceExportIdentity } from "./appliance-execution.ts";
|
|
18
|
-
export { type CertificateCheck, type CertificateInput, type CertificateVerifyOptions, type CertifyClaim, type PqPolicy, CONFLICTS, type Conflict, certificateScope, claimReasons, issueCertificate, LEVELS, type Level, pickScheme, SCHEME, SCHEME_LEVELS, SCHEMES, type Scheme, SURVEILLANCE, VALIDITY_DAYS, verifyCertificate, } from "./certificate.ts";
|
|
19
|
+
export { type CertificateCheck, type CertificateInput, type CertificateVerifyOptions, type CertifyClaim, type PqPolicy, CONFLICTS, type Conflict, certificateScope, claimReasons, issueCertificate, LEVELS, type Level, pickScheme, SCHEME, SCHEME_LEVELS, schemeFrom, SCHEMES, type Scheme, SURVEILLANCE, SUSPENSION_REVOCATION_DAYS, SUSPENSION_WARNING_DAYS, VALIDITY_DAYS, verifyCertificate, } from "./certificate.ts";
|
|
19
20
|
export { buildFingerprint, checkFingerprint, type Fingerprint, type FingerprintInput, promptSha256, sha256Bytes, toolsSha256, } from "./fingerprint.ts";
|
|
20
21
|
export { COMMITTEE_MIN_EXTERNAL, type CommitteeMember, committeeQuorum, COOLING_OFF_YEARS, type CoolingOff, type Engagement, REFUSALS, type Refusal, type Staff, staffRefusals, } from "./impartiality.ts";
|
|
21
22
|
export { AUDIENCES, type Audience, buildEvidenceShare, discloseEvidenceField, type EvidenceShare, type EvidenceShareInput, GCC_IDENTIFIERS, REDACTION_KINDS, redactEvidence, type RedactionKind, type RedactOptions, type SharedEntry, verifyEvidenceShare, } from "./evidence-share.ts";
|
|
@@ -28,7 +29,7 @@ export { type CategoryScore, type Overall, overallVerdict, type ProfileCategoryR
|
|
|
28
29
|
export { activeRotation, buildRotation, type Rotation, type RotationCheck, type RotationInput, type SuiteRef, verifyRotationLog, verifyRotationRecord, } from "./rotation.ts";
|
|
29
30
|
export { type Check, type CheckContext, type CheckResult, casesOf, DEFAULT_FINDINGS, type Delegation, type DelegationLink, effectiveDelegation, judgementResult, normaliseText, type Rubric, runCheck, type Scenario, type ScenarioLibrary, scenarioCases, scenariosSchema, type Suite, type SubAgent, type SuiteCase, suiteSchema, type ToolCall, suiteSnapshot, validateScenarios, validateSuite, verifyCaseDelegation, } from "./suite.ts";
|
|
30
31
|
export { keySigner, type Signer, signerDid, signWith } from "./signer.ts";
|
|
31
|
-
export { applyStatus, buildStatus, type CertificateStatus, REASONS, REASONS_V2, STATUS_V2_SCHEMA, STATUS_V2_MAX_ENTRIES, type StatusVersion, type Reason, revocationListFromStatus, type StatusAction, type StatusCheck, type StatusDocument, type StatusEntries, type StatusEntry, type StatusVerifyOptions, type StatusWitness, verifyStatus, logPositionOk, } from "./status.ts";
|
|
32
|
+
export { applyStatus, buildStatus, type CertificateStatus, REASONS, REASONS_V2, REASONS_V3, STATUS_V2_SCHEMA, STATUS_V3_SCHEMA, STATUS_V2_MAX_ENTRIES, statusVersionFor, type StatusVersion, type Reason, revocationListFromStatus, type StatusAction, type StatusCheck, type StatusDocument, type StatusEntries, type StatusEntry, type StatusVerifyOptions, type StatusWitness, verifyStatus, logPositionOk, } from "./status.ts";
|
|
32
33
|
export { type CertificateVcCheck, certificateToVc, VC_STATUS_TYPE, VC_TYPE, verifyCertificateVc, } from "./vc.ts";
|
|
33
34
|
export { statusCheckpoint, statusConsistencyProof, statusInclusionProof, verifyStatusLog, } from "./witness.ts";
|
|
34
35
|
export { type AccreditationExport, buildAccreditationExport, buildFilingPack, buildGovernanceOutcome, buildSigningKeyApproval, CASE_KINDS, type CaseKind, type CertifyRiskFacts, type CertifyRiskProfile, certifyRiskProfile, type FilingPack, type FilingPackInput, type GovernanceCheck, type GovernanceOutcome, type GovernanceOutcomeInput, governanceOutcomeBody, OUTCOMES, RISK_NOTE, type RiskProfileInput, type SignedRiskProfile, type SigningKeyApproval, type SigningKeyApprovalInput, signingKeyApprovalBody, signRiskProfile, verifyRiskProfile, verifyAccreditationExport, verifyFilingPack, verifyGovernanceOutcome, verifySigningKeyApproval, } from "./governance.ts";
|
package/dist/index.js
CHANGED
|
@@ -15,8 +15,9 @@ export { buildFeedPage, feedCheckpoint, MAX_FEED_ENTRIES, MAX_FEED_PAGE_BYTES, m
|
|
|
15
15
|
export { buildLicence, buildLicenceStatus, buildRoyaltyStatement, certificateListHash, countersignRoyaltyStatement, LICENCE_ACTIONS, LICENCE_REASONS, licenceCoverage, licenceReasons, licensedKey, licenceStateAt, MAX_ROYALTY_MINOR, MAX_STATEMENT_CERTIFICATES, verifyLicence, verifyLicenceLog, verifyRoyaltyStatement, } from "./licence.js";
|
|
16
16
|
export { beaconChallenge, buildBeacon, MAX_SKEW_SECONDS, NONCE, verifyAttestedBeacon, verifyBeacon, } from "./beacon.js";
|
|
17
17
|
export { bundleHash, buildBundle, buildApplianceExportBundle, } from "./bundle.js";
|
|
18
|
+
export { ASSESSMENT_EVIDENCE, buildDeclaration, buildEndpointProof, DECLARATION_STATEMENT, declarationHash, ENDPOINT_NONCE_HEADER, ENDPOINT_PROOF_HEADER, endpointChallenge, EVIDENCE_GRACE_DAYS, EVIDENCE_WINDOW_DAYS, evidenceLevels, PRODUCTION_SCHEME, productionBindingReasons, verifyDeclaration, verifyEndpointProof, } from "./production.js";
|
|
18
19
|
export { applianceExecution, applianceExportIdentity } from "./appliance-execution.js";
|
|
19
|
-
export { CONFLICTS, certificateScope, claimReasons, issueCertificate, LEVELS, pickScheme, SCHEME, SCHEME_LEVELS, SCHEMES, SURVEILLANCE, VALIDITY_DAYS, verifyCertificate, } from "./certificate.js";
|
|
20
|
+
export { CONFLICTS, certificateScope, claimReasons, issueCertificate, LEVELS, pickScheme, SCHEME, SCHEME_LEVELS, schemeFrom, SCHEMES, SURVEILLANCE, SUSPENSION_REVOCATION_DAYS, SUSPENSION_WARNING_DAYS, VALIDITY_DAYS, verifyCertificate, } from "./certificate.js";
|
|
20
21
|
export { buildFingerprint, checkFingerprint, promptSha256, sha256Bytes, toolsSha256, } from "./fingerprint.js";
|
|
21
22
|
export { COMMITTEE_MIN_EXTERNAL, committeeQuorum, COOLING_OFF_YEARS, REFUSALS, staffRefusals, } from "./impartiality.js";
|
|
22
23
|
export { AUDIENCES, buildEvidenceShare, discloseEvidenceField, GCC_IDENTIFIERS, REDACTION_KINDS, redactEvidence, verifyEvidenceShare, } from "./evidence-share.js";
|
|
@@ -29,7 +30,7 @@ export { overallVerdict, scoreCategory, } from "./scoring.js";
|
|
|
29
30
|
export { activeRotation, buildRotation, verifyRotationLog, verifyRotationRecord, } from "./rotation.js";
|
|
30
31
|
export { casesOf, DEFAULT_FINDINGS, effectiveDelegation, judgementResult, normaliseText, runCheck, scenarioCases, scenariosSchema, suiteSchema, suiteSnapshot, validateScenarios, validateSuite, verifyCaseDelegation, } from "./suite.js";
|
|
31
32
|
export { keySigner, signerDid, signWith } from "./signer.js";
|
|
32
|
-
export { applyStatus, buildStatus, REASONS, REASONS_V2, STATUS_V2_SCHEMA, STATUS_V2_MAX_ENTRIES, revocationListFromStatus, verifyStatus, logPositionOk, } from "./status.js";
|
|
33
|
+
export { applyStatus, buildStatus, REASONS, REASONS_V2, REASONS_V3, STATUS_V2_SCHEMA, STATUS_V3_SCHEMA, STATUS_V2_MAX_ENTRIES, statusVersionFor, revocationListFromStatus, verifyStatus, logPositionOk, } from "./status.js";
|
|
33
34
|
export { certificateToVc, VC_STATUS_TYPE, VC_TYPE, verifyCertificateVc, } from "./vc.js";
|
|
34
35
|
export { statusCheckpoint, statusConsistencyProof, statusInclusionProof, verifyStatusLog, } from "./witness.js";
|
|
35
36
|
export { buildAccreditationExport, buildFilingPack, buildGovernanceOutcome, buildSigningKeyApproval, CASE_KINDS, certifyRiskProfile, governanceOutcomeBody, OUTCOMES, RISK_NOTE, signingKeyApprovalBody, signRiskProfile, verifyRiskProfile, verifyAccreditationExport, verifyFilingPack, verifyGovernanceOutcome, verifySigningKeyApproval, } from "./governance.js";
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type Signer } from "./signer.ts";
|
|
2
|
+
export declare const PRODUCTION_SCHEME = "zanii-certify/0.5";
|
|
3
|
+
/** Section 2: the exact statement every 0.5 declaration signs, in both languages. */
|
|
4
|
+
export declare const DECLARATION_STATEMENT: {
|
|
5
|
+
readonly en: "The agent configuration the Zanii Certify lab tests is the configuration we deploy in production, at the registered endpoint, which is our production endpoint and not a separate test endpoint. We will tell the lab before we change that configuration or endpoint. We accept the lab's audit rights while the certificate is in force: unannounced re-tests and spot checks of the registered endpoint, and review of the production evidence.";
|
|
6
|
+
readonly ar: "إعداد الوكيل الذي يختبره مختبر Zanii Certify هو الإعداد الذي ننشره في بيئة الإنتاج، على نقطة الاتصال المسجلة، وهي نقطة اتصال الإنتاج لدينا وليست نقطة اتصال منفصلة للاختبار. سنبلغ المختبر قبل أن نغير ذلك الإعداد أو نقطة الاتصال. ونقبل حقوق التدقيق للمختبر ما دامت الشهادة سارية: إعادة اختبارات وفحوصا مفاجئة لنقطة الاتصال المسجلة، ومراجعة أدلة الإنتاج.";
|
|
7
|
+
};
|
|
8
|
+
/** Section 4: what production evidence a 0.5 result bundle records (a contract with verifiers and badges). */
|
|
9
|
+
export declare const ASSESSMENT_EVIDENCE: readonly ["self_reported", "attested", "blackbox"];
|
|
10
|
+
export type AssessmentEvidence = (typeof ASSESSMENT_EVIDENCE)[number];
|
|
11
|
+
/** Section 4: evidence counts at a grant when recorded at most this many days before the decision. */
|
|
12
|
+
export declare const EVIDENCE_WINDOW_DAYS = 30;
|
|
13
|
+
/** Section 4: days after evidence is due before the lab suspends with EVIDENCE_LAPSED. */
|
|
14
|
+
export declare const EVIDENCE_GRACE_DAYS = 7;
|
|
15
|
+
/** Section 3: the request header carrying the run's nonce, and the response header carrying the proof. */
|
|
16
|
+
export declare const ENDPOINT_NONCE_HEADER = "zanii-certify-nonce";
|
|
17
|
+
export declare const ENDPOINT_PROOF_HEADER = "zanii-certify-proof";
|
|
18
|
+
export interface Declaration {
|
|
19
|
+
v: 1;
|
|
20
|
+
type: "certify.declaration";
|
|
21
|
+
scheme: typeof PRODUCTION_SCHEME;
|
|
22
|
+
agent: string;
|
|
23
|
+
profile: string;
|
|
24
|
+
level: 1 | 2 | 3;
|
|
25
|
+
endpoint: string;
|
|
26
|
+
statement: typeof DECLARATION_STATEMENT;
|
|
27
|
+
declared_at: string;
|
|
28
|
+
signer: string;
|
|
29
|
+
sig: string;
|
|
30
|
+
}
|
|
31
|
+
export interface DeclarationInput {
|
|
32
|
+
agent: string;
|
|
33
|
+
profile: string;
|
|
34
|
+
level: number;
|
|
35
|
+
endpoint: string;
|
|
36
|
+
declaredAt: string;
|
|
37
|
+
}
|
|
38
|
+
/** Section 2: sign the declaration with the agent's key or the applicant's verified owner key. */
|
|
39
|
+
export declare function buildDeclaration(input: DeclarationInput, signer: Signer): Promise<Declaration>;
|
|
40
|
+
export interface DeclarationCheck {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
reasons: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Section 2: is this an intact 0.5 declaration? With `agent`, `profile`, `level` and `endpoint`
|
|
46
|
+
* it must name them; with `signers` its signer must be one of them (the agent DID and, when
|
|
47
|
+
* verified, the applicant's owner DID); with `now` it is not dated ahead, and with
|
|
48
|
+
* `maxAgeSeconds` not older than that.
|
|
49
|
+
*/
|
|
50
|
+
export declare function verifyDeclaration(declaration: unknown, opts?: {
|
|
51
|
+
agent?: string;
|
|
52
|
+
profile?: string;
|
|
53
|
+
level?: number;
|
|
54
|
+
endpoint?: string;
|
|
55
|
+
signers?: readonly string[];
|
|
56
|
+
now?: string;
|
|
57
|
+
maxAgeSeconds?: number;
|
|
58
|
+
}): DeclarationCheck;
|
|
59
|
+
export declare const declarationHash: (declaration: Declaration) => string;
|
|
60
|
+
/** Section 3: what the agent signs to answer a run's nonce. */
|
|
61
|
+
export declare const endpointChallenge: (f: {
|
|
62
|
+
agent: string;
|
|
63
|
+
nonce: string;
|
|
64
|
+
}) => {
|
|
65
|
+
v: 1;
|
|
66
|
+
type: "certify.endpoint-challenge";
|
|
67
|
+
agent: string;
|
|
68
|
+
nonce: string;
|
|
69
|
+
};
|
|
70
|
+
/** Section 3: the `zanii-certify-proof` header value: `ed25519:<hex>` by the agent over the challenge. */
|
|
71
|
+
export declare function buildEndpointProof(nonce: string, signer: Signer): Promise<string>;
|
|
72
|
+
/** Section 3: does `proof` answer `nonce` with the agent's key? Never throws. */
|
|
73
|
+
export declare function verifyEndpointProof(proof: unknown, f: {
|
|
74
|
+
agent: string;
|
|
75
|
+
nonce: string;
|
|
76
|
+
}): boolean;
|
|
77
|
+
/** Section 4: the levels a certificate with this evidence may be granted at under 0.5. */
|
|
78
|
+
export declare const evidenceLevels: (evidence: AssessmentEvidence) => readonly number[];
|
|
79
|
+
/** The 0.5 production binding a result bundle records (result-bundle.md section 4b). */
|
|
80
|
+
export interface EndpointBinding {
|
|
81
|
+
spki_sha256: string | null;
|
|
82
|
+
proved: boolean;
|
|
83
|
+
}
|
|
84
|
+
export declare const endpointBindingOk: (e: unknown) => e is EndpointBinding;
|
|
85
|
+
/**
|
|
86
|
+
* Section 5: what a verifier checks on a grant bundle under 0.5 (an earlier scheme has nothing to
|
|
87
|
+
* check): the declaration is intact and names the bundle's profile and level, and a Level 2 or 3
|
|
88
|
+
* bundle records attested or Blackbox evidence and a verified endpoint proof.
|
|
89
|
+
*/
|
|
90
|
+
export declare function productionBindingReasons(bundle: unknown): string[];
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// spec/production-binding.md (scheme 0.5): the applicant's signed declaration, the test-time
|
|
2
|
+
// endpoint proof and the production evidence a Level 2 or 3 certificate needs.
|
|
3
|
+
import { jcsHash, verifySignedObject } from "@zanii/core";
|
|
4
|
+
import { NONCE } from "./beacon.js";
|
|
5
|
+
import { isDid, isIso, isObj, isSha, ms, SLUG } from "./shared.js";
|
|
6
|
+
import { signatureOk, signatureOver, signerDid, signWith } from "./signer.js";
|
|
7
|
+
export const PRODUCTION_SCHEME = "zanii-certify/0.5";
|
|
8
|
+
/** Section 2: the exact statement every 0.5 declaration signs, in both languages. */
|
|
9
|
+
export const DECLARATION_STATEMENT = {
|
|
10
|
+
en: "The agent configuration the Zanii Certify lab tests is the configuration we deploy in production, at the registered endpoint, which is our production endpoint and not a separate test endpoint. We will tell the lab before we change that configuration or endpoint. We accept the lab's audit rights while the certificate is in force: unannounced re-tests and spot checks of the registered endpoint, and review of the production evidence.",
|
|
11
|
+
ar: "إعداد الوكيل الذي يختبره مختبر Zanii Certify هو الإعداد الذي ننشره في بيئة الإنتاج، على نقطة الاتصال المسجلة، وهي نقطة اتصال الإنتاج لدينا وليست نقطة اتصال منفصلة للاختبار. سنبلغ المختبر قبل أن نغير ذلك الإعداد أو نقطة الاتصال. ونقبل حقوق التدقيق للمختبر ما دامت الشهادة سارية: إعادة اختبارات وفحوصا مفاجئة لنقطة الاتصال المسجلة، ومراجعة أدلة الإنتاج.",
|
|
12
|
+
};
|
|
13
|
+
/** Section 4: what production evidence a 0.5 result bundle records (a contract with verifiers and badges). */
|
|
14
|
+
export const ASSESSMENT_EVIDENCE = ["self_reported", "attested", "blackbox"];
|
|
15
|
+
/** Section 4: evidence counts at a grant when recorded at most this many days before the decision. */
|
|
16
|
+
export const EVIDENCE_WINDOW_DAYS = 30;
|
|
17
|
+
/** Section 4: days after evidence is due before the lab suspends with EVIDENCE_LAPSED. */
|
|
18
|
+
export const EVIDENCE_GRACE_DAYS = 7;
|
|
19
|
+
/** Section 3: the request header carrying the run's nonce, and the response header carrying the proof. */
|
|
20
|
+
export const ENDPOINT_NONCE_HEADER = "zanii-certify-nonce";
|
|
21
|
+
export const ENDPOINT_PROOF_HEADER = "zanii-certify-proof";
|
|
22
|
+
const PROFILE_REF = new RegExp(`^${SLUG.source.slice(1, -1)}/[1-9][0-9]*$`);
|
|
23
|
+
const SIG = /^ed25519:[0-9a-f]{128}$/;
|
|
24
|
+
const MAX_SKEW_MS = 300_000;
|
|
25
|
+
const endpointOk = (v) => typeof v === "string" && v.length <= 2048 && /^https?:\/\/[^\s]+$/.test(v);
|
|
26
|
+
/** Section 2: sign the declaration with the agent's key or the applicant's verified owner key. */
|
|
27
|
+
export async function buildDeclaration(input, signer) {
|
|
28
|
+
if (!isDid(input.agent))
|
|
29
|
+
throw new Error("agent must be a did:key");
|
|
30
|
+
if (typeof input.profile !== "string" || !PROFILE_REF.test(input.profile))
|
|
31
|
+
throw new Error("profile must be <slug>/<version>");
|
|
32
|
+
if (input.level !== 1 && input.level !== 2 && input.level !== 3)
|
|
33
|
+
throw new Error("level must be 1, 2 or 3");
|
|
34
|
+
if (!endpointOk(input.endpoint))
|
|
35
|
+
throw new Error("endpoint must be the registered http(s) endpoint URL");
|
|
36
|
+
if (!isIso(input.declaredAt))
|
|
37
|
+
throw new Error("declaredAt must be an ISO UTC timestamp");
|
|
38
|
+
return signWith({
|
|
39
|
+
v: 1, type: "certify.declaration", scheme: PRODUCTION_SCHEME, agent: input.agent, profile: input.profile,
|
|
40
|
+
level: input.level, endpoint: input.endpoint, statement: { en: DECLARATION_STATEMENT.en, ar: DECLARATION_STATEMENT.ar }, declared_at: input.declaredAt,
|
|
41
|
+
}, signer);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Section 2: is this an intact 0.5 declaration? With `agent`, `profile`, `level` and `endpoint`
|
|
45
|
+
* it must name them; with `signers` its signer must be one of them (the agent DID and, when
|
|
46
|
+
* verified, the applicant's owner DID); with `now` it is not dated ahead, and with
|
|
47
|
+
* `maxAgeSeconds` not older than that.
|
|
48
|
+
*/
|
|
49
|
+
export function verifyDeclaration(declaration, opts = {}) {
|
|
50
|
+
const d = isObj(declaration) ? declaration : {};
|
|
51
|
+
if (d.v !== 1 || d.type !== "certify.declaration" || Object.keys(d).length !== 11)
|
|
52
|
+
return { ok: false, reasons: ["not a declaration (v1)"] };
|
|
53
|
+
const reasons = [];
|
|
54
|
+
if (d.scheme !== PRODUCTION_SCHEME || !isDid(d.agent) || typeof d.profile !== "string" || !PROFILE_REF.test(d.profile)
|
|
55
|
+
|| (d.level !== 1 && d.level !== 2 && d.level !== 3) || !endpointOk(d.endpoint) || !isIso(d.declared_at) || !isDid(d.signer))
|
|
56
|
+
reasons.push("declaration has a malformed field");
|
|
57
|
+
if (!isObj(d.statement) || Object.keys(d.statement).length !== 2 || d.statement.en !== DECLARATION_STATEMENT.en || d.statement.ar !== DECLARATION_STATEMENT.ar)
|
|
58
|
+
reasons.push("declaration statement is not the scheme 0.5 statement");
|
|
59
|
+
if (!verifySignedObject(d))
|
|
60
|
+
reasons.push("bad declaration signature");
|
|
61
|
+
if (opts.signers !== undefined && !opts.signers.includes(d.signer))
|
|
62
|
+
reasons.push("declaration is not signed by the agent or the applicant's owner key");
|
|
63
|
+
if (opts.agent !== undefined && d.agent !== opts.agent)
|
|
64
|
+
reasons.push("declaration names a different agent");
|
|
65
|
+
if (opts.profile !== undefined && d.profile !== opts.profile)
|
|
66
|
+
reasons.push("declaration names a different profile");
|
|
67
|
+
if (opts.level !== undefined && d.level !== opts.level)
|
|
68
|
+
reasons.push("declaration names a different level");
|
|
69
|
+
if (opts.endpoint !== undefined && d.endpoint !== opts.endpoint)
|
|
70
|
+
reasons.push("declaration names an endpoint other than the registered one");
|
|
71
|
+
const now = opts.now ? ms(opts.now) : Date.now(), at = ms(d.declared_at);
|
|
72
|
+
if (at > now + MAX_SKEW_MS)
|
|
73
|
+
reasons.push("declaration declared_at is in the future");
|
|
74
|
+
if (opts.maxAgeSeconds !== undefined && at < now - opts.maxAgeSeconds * 1000)
|
|
75
|
+
reasons.push("declaration is older than the accepted window");
|
|
76
|
+
return { ok: reasons.length === 0, reasons };
|
|
77
|
+
}
|
|
78
|
+
export const declarationHash = (declaration) => jcsHash(declaration);
|
|
79
|
+
/** Section 3: what the agent signs to answer a run's nonce. */
|
|
80
|
+
export const endpointChallenge = (f) => ({ v: 1, type: "certify.endpoint-challenge", agent: f.agent, nonce: f.nonce });
|
|
81
|
+
/** Section 3: the `zanii-certify-proof` header value: `ed25519:<hex>` by the agent over the challenge. */
|
|
82
|
+
export async function buildEndpointProof(nonce, signer) {
|
|
83
|
+
if (typeof nonce !== "string" || !NONCE.test(nonce))
|
|
84
|
+
throw new Error("nonce must be 16-128 of [A-Za-z0-9_-]");
|
|
85
|
+
return signatureOver(endpointChallenge({ agent: signerDid(signer), nonce }), signer);
|
|
86
|
+
}
|
|
87
|
+
/** Section 3: does `proof` answer `nonce` with the agent's key? Never throws. */
|
|
88
|
+
export function verifyEndpointProof(proof, f) {
|
|
89
|
+
return typeof proof === "string" && SIG.test(proof) && typeof f.nonce === "string" && NONCE.test(f.nonce) && isDid(f.agent)
|
|
90
|
+
&& signatureOk(endpointChallenge(f), proof, f.agent);
|
|
91
|
+
}
|
|
92
|
+
/** Section 4: the levels a certificate with this evidence may be granted at under 0.5. */
|
|
93
|
+
export const evidenceLevels = (evidence) => (evidence === "self_reported" ? [1] : [1, 2, 3]);
|
|
94
|
+
export const endpointBindingOk = (e) => isObj(e) && Object.keys(e).length === 2 && (e.spki_sha256 === null || isSha(e.spki_sha256)) && typeof e.proved === "boolean";
|
|
95
|
+
/**
|
|
96
|
+
* Section 5: what a verifier checks on a grant bundle under 0.5 (an earlier scheme has nothing to
|
|
97
|
+
* check): the declaration is intact and names the bundle's profile and level, and a Level 2 or 3
|
|
98
|
+
* bundle records attested or Blackbox evidence and a verified endpoint proof.
|
|
99
|
+
*/
|
|
100
|
+
export function productionBindingReasons(bundle) {
|
|
101
|
+
const b = isObj(bundle) ? bundle : {};
|
|
102
|
+
if (b.scheme !== PRODUCTION_SCHEME)
|
|
103
|
+
return [];
|
|
104
|
+
const out = [];
|
|
105
|
+
const a = isObj(b.assessment) ? b.assessment : null;
|
|
106
|
+
if (!a || !ASSESSMENT_EVIDENCE.includes(a.evidence) || !endpointBindingOk(a.endpoint))
|
|
107
|
+
out.push("a zanii-certify/0.5 bundle records assessment.evidence and assessment.endpoint");
|
|
108
|
+
const declared = verifyDeclaration(b.declaration, { profile: b.profile, level: b.level, now: typeof b.ended_at === "string" ? b.ended_at : undefined });
|
|
109
|
+
out.push(...declared.reasons.map((r) => `bundle ${r}`));
|
|
110
|
+
if (a && (b.level === 2 || b.level === 3)) {
|
|
111
|
+
if (a.evidence === "self_reported")
|
|
112
|
+
out.push("a Level 2 or 3 certificate under zanii-certify/0.5 needs attested or Blackbox production evidence");
|
|
113
|
+
if (!(isObj(a.endpoint) && a.endpoint.proved === true))
|
|
114
|
+
out.push("a Level 2 or 3 certificate under zanii-certify/0.5 needs a verified endpoint proof");
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
package/dist/profile.js
CHANGED
|
@@ -117,7 +117,7 @@ export function schemaErrors(value, schema) {
|
|
|
117
117
|
* every clause it cites exists in a clause map the profile lists.
|
|
118
118
|
*/
|
|
119
119
|
/** scheme.md section 2a: the scheme versions whose profiles may assess categories as multi-step workflows. */
|
|
120
|
-
const TRAJECTORY_SCHEMES = ["zanii-certify/0.4"];
|
|
120
|
+
const TRAJECTORY_SCHEMES = ["zanii-certify/0.4", "zanii-certify/0.5"];
|
|
121
121
|
export function validateProfile(profile, clauseMaps = []) {
|
|
122
122
|
const errors = schemaErrors(profile, profileSchema);
|
|
123
123
|
if (!errors.length) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"type": { "const": "certify.profile" },
|
|
11
11
|
"slug": { "$ref": "#/$defs/slug" },
|
|
12
12
|
"version": { "type": "integer", "minimum": 1 },
|
|
13
|
-
"scheme": { "enum": ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3", "zanii-certify/0.4"] },
|
|
13
|
+
"scheme": { "enum": ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3", "zanii-certify/0.4", "zanii-certify/0.5"] },
|
|
14
14
|
"title": { "$ref": "#/$defs/text" },
|
|
15
15
|
"description": { "$ref": "#/$defs/text" },
|
|
16
16
|
"levels": {
|
package/dist/status.d.ts
CHANGED
|
@@ -5,16 +5,21 @@ import type { WitnessCosignature } from "@zanii/witness";
|
|
|
5
5
|
import { type PqCosignature } from "./pq.ts";
|
|
6
6
|
import { type Signer } from "./signer.ts";
|
|
7
7
|
import STATUS_V2_SCHEMA from "./formats/status-v2.schema.json";
|
|
8
|
-
|
|
8
|
+
import STATUS_V3_SCHEMA from "./formats/status-v3.schema.json";
|
|
9
|
+
export { STATUS_V2_SCHEMA, STATUS_V3_SCHEMA };
|
|
9
10
|
export declare const REASONS: readonly ["FINGERPRINT_MISMATCH", "RETEST_OVERDUE", "RETEST_FAILED", "INCIDENT", "APPLICANT_REQUEST", "ISSUER_ERROR",
|
|
10
11
|
/** licensing.md section 2a: the issuing licensee's licence was withdrawn or suspended for cause. */
|
|
11
12
|
"LICENCE_WITHDRAWN",
|
|
12
13
|
/** audit.md section 3: a Level 3 certificate's audit attestation lapsed. */
|
|
13
14
|
"AUDIT_EXPIRED"];
|
|
14
15
|
export declare const REASONS_V2: readonly ["FINGERPRINT_MISMATCH", "RETEST_OVERDUE", "RETEST_FAILED", "INCIDENT", "APPLICANT_REQUEST", "ISSUER_ERROR", "LICENCE_WITHDRAWN", "AUDIT_EXPIRED", "MARK_MISUSE"];
|
|
16
|
+
/** Section 7 (scheme 0.5, production-binding.md): a false declaration, and production evidence that lapsed. */
|
|
17
|
+
export declare const REASONS_V3: readonly ["FINGERPRINT_MISMATCH", "RETEST_OVERDUE", "RETEST_FAILED", "INCIDENT", "APPLICANT_REQUEST", "ISSUER_ERROR", "LICENCE_WITHDRAWN", "AUDIT_EXPIRED", "MARK_MISUSE", "FALSE_DECLARATION", "EVIDENCE_LAPSED"];
|
|
15
18
|
export declare const STATUS_V2_MAX_ENTRIES = 100000;
|
|
16
|
-
export type Reason = (typeof
|
|
17
|
-
export type StatusVersion = 1 | 2;
|
|
19
|
+
export type Reason = (typeof REASONS_V3)[number];
|
|
20
|
+
export type StatusVersion = 1 | 2 | 3;
|
|
21
|
+
/** The lowest status (and event) version that can carry `reason`: 3 for the section 7 codes, 2 for MARK_MISUSE, else 1. */
|
|
22
|
+
export declare const statusVersionFor: (reason: unknown) => StatusVersion;
|
|
18
23
|
export type CertificateStatus = "valid" | "suspended" | "revoked";
|
|
19
24
|
export type StatusAction = "grant" | "suspend" | "revoke" | "reinstate";
|
|
20
25
|
export interface StatusEntry {
|
package/dist/status.js
CHANGED
|
@@ -9,7 +9,8 @@ import { witnessReasons } from "./witness.js";
|
|
|
9
9
|
import { signingIdentityReasons } from "./signing-identity.js";
|
|
10
10
|
import { withinVerificationBudget } from "./verification-bounds.js";
|
|
11
11
|
import STATUS_V2_SCHEMA from "./formats/status-v2.schema.json" with { type: "json" };
|
|
12
|
-
|
|
12
|
+
import STATUS_V3_SCHEMA from "./formats/status-v3.schema.json" with { type: "json" };
|
|
13
|
+
export { STATUS_V2_SCHEMA, STATUS_V3_SCHEMA };
|
|
13
14
|
export const REASONS = [
|
|
14
15
|
"FINGERPRINT_MISMATCH",
|
|
15
16
|
"RETEST_OVERDUE",
|
|
@@ -23,18 +24,23 @@ export const REASONS = [
|
|
|
23
24
|
"AUDIT_EXPIRED",
|
|
24
25
|
];
|
|
25
26
|
export const REASONS_V2 = [...REASONS, "MARK_MISUSE"];
|
|
27
|
+
/** Section 7 (scheme 0.5, production-binding.md): a false declaration, and production evidence that lapsed. */
|
|
28
|
+
export const REASONS_V3 = [...REASONS_V2, "FALSE_DECLARATION", "EVIDENCE_LAPSED"];
|
|
26
29
|
export const STATUS_V2_MAX_ENTRIES = 100_000;
|
|
30
|
+
/** The lowest status (and event) version that can carry `reason`: 3 for the section 7 codes, 2 for MARK_MISUSE, else 1. */
|
|
31
|
+
export const statusVersionFor = (reason) => reason === "FALSE_DECLARATION" || reason === "EVIDENCE_LAPSED" ? 3 : reason === "MARK_MISUSE" ? 2 : 1;
|
|
27
32
|
/** A log position is `seq` and `prev` together: seq 0 has prev null, later ones a hash. */
|
|
28
33
|
export const logPositionOk = (seq, prev) => isInt(seq, 0, Number.MAX_SAFE_INTEGER) && (seq === 0 ? prev === null : isSha(prev));
|
|
29
|
-
const isReason = (v, version = 1) => (version === 2 ? REASONS_V2 : REASONS).includes(v);
|
|
34
|
+
const isReason = (v, version = 1) => (version === 3 ? REASONS_V3 : version === 2 ? REASONS_V2 : REASONS).includes(v);
|
|
35
|
+
const knownVersion = (v) => v === 1 || v === 2 || v === 3;
|
|
30
36
|
/**
|
|
31
37
|
* §2: apply one status change and return the new entries (the input is not changed). Throws on a
|
|
32
38
|
* change the rules forbid: revoked is final, only a valid certificate is suspended, only a
|
|
33
39
|
* suspended one is reinstated, and time never goes backwards for one certificate.
|
|
34
40
|
*/
|
|
35
41
|
export function applyStatus(entries, certificate, action, reason, at, opts = {}) {
|
|
36
|
-
if (opts.version !== undefined && opts.version
|
|
37
|
-
throw new Error("status version must be 1 or
|
|
42
|
+
if (opts.version !== undefined && !knownVersion(opts.version))
|
|
43
|
+
throw new Error("status version must be 1, 2 or 3");
|
|
38
44
|
if (!isSha(certificate))
|
|
39
45
|
throw new Error("certificate must be a credential hash");
|
|
40
46
|
if (!isIso(at))
|
|
@@ -81,26 +87,26 @@ function entryOk(e, version = 1) {
|
|
|
81
87
|
}
|
|
82
88
|
function v2Error(d) {
|
|
83
89
|
if (!withinVerificationBudget(d, { maxDepth: 8 }) || !isObj(d.entries) || Object.keys(d.entries).length > STATUS_V2_MAX_ENTRIES)
|
|
84
|
-
return
|
|
90
|
+
return `status v${String(d.v)} exceeds its JSON or entry budget`;
|
|
85
91
|
if ("v type issuer entries issued_at valid_until signer sig".split(" ").some(k => !Object.hasOwn(d, k)) || Object.keys(d).some(k => !"v type issuer entries issued_at valid_until signer sig seq prev pq_binding".split(" ").includes(k)))
|
|
86
|
-
return
|
|
92
|
+
return `status v${String(d.v)} has missing or unknown fields`;
|
|
87
93
|
if (typeof d.issuer !== "string" || d.issuer.length > 69 || !publicKeyFromDid(d.issuer) || d.signer !== d.issuer || typeof d.sig !== "string" || !/^ed25519:[0-9a-f]{128}$/.test(d.sig))
|
|
88
|
-
return
|
|
94
|
+
return `status v${String(d.v)} needs matching Ed25519 issuer and signer`;
|
|
89
95
|
if (!isIso(d.issued_at) || !isIso(d.valid_until) || ms(d.valid_until) <= ms(d.issued_at) || ms(d.valid_until) - ms(d.issued_at) > 3_600_000)
|
|
90
|
-
return
|
|
91
|
-
if (Object.entries(d.entries).some(([hash, entry]) => !isSha(hash) || !entryOk(entry,
|
|
92
|
-
return
|
|
96
|
+
return `status v${String(d.v)} lifetime must be positive and at most one hour`;
|
|
97
|
+
if (Object.entries(d.entries).some(([hash, entry]) => !isSha(hash) || !entryOk(entry, d.v) || ms(entry.since) > ms(d.issued_at)))
|
|
98
|
+
return `status v${String(d.v)} has a malformed or future entry`;
|
|
93
99
|
if ((("seq" in d || "prev" in d) && !logPositionOk(d.seq, d.prev)) || ("pq_binding" in d && !isSha(d.pq_binding)))
|
|
94
|
-
return
|
|
100
|
+
return `status v${String(d.v)} has a malformed log position or PQ binding`;
|
|
95
101
|
return null;
|
|
96
102
|
}
|
|
97
103
|
/** §1: sign a status document. `valid_until` is the freshness bound verifiers enforce. */
|
|
98
104
|
export async function buildStatus(fields, signer) {
|
|
99
105
|
const version = fields.version ?? 1;
|
|
100
|
-
if (version
|
|
101
|
-
throw new Error("status version must be 1 or
|
|
102
|
-
if (version
|
|
103
|
-
throw new Error(
|
|
106
|
+
if (!knownVersion(version))
|
|
107
|
+
throw new Error("status version must be 1, 2 or 3");
|
|
108
|
+
if (version >= 2 && (!isObj(fields.entries) || Object.keys(fields.entries).length > STATUS_V2_MAX_ENTRIES || Object.keys(fields).some(k => !["version", "entries", "issuedAt", "validUntil", "seq", "prev", "pqBinding"].includes(k)) || !withinVerificationBudget(fields, { maxDepth: 10 })))
|
|
109
|
+
throw new Error(`status v${version} exceeds its JSON or entry budget`);
|
|
104
110
|
if (!isIso(fields.issuedAt) || !isIso(fields.validUntil))
|
|
105
111
|
throw new Error("issuedAt and validUntil must be ISO UTC timestamps");
|
|
106
112
|
if (ms(fields.validUntil) < ms(fields.issuedAt))
|
|
@@ -118,13 +124,13 @@ export async function buildStatus(fields, signer) {
|
|
|
118
124
|
v: version,
|
|
119
125
|
type: "certify.status",
|
|
120
126
|
issuer,
|
|
121
|
-
entries: version
|
|
127
|
+
entries: version >= 2 ? structuredClone(fields.entries) : fields.entries,
|
|
122
128
|
issued_at: fields.issuedAt,
|
|
123
129
|
valid_until: fields.validUntil,
|
|
124
130
|
...(logged ? { seq: fields.seq, prev: fields.prev } : {}),
|
|
125
131
|
...(fields.pqBinding ? { pq_binding: bindingHash(fields.pqBinding) } : {}),
|
|
126
132
|
};
|
|
127
|
-
if (version
|
|
133
|
+
if (version >= 2) {
|
|
128
134
|
const error = v2Error({ ...unsigned, signer: issuer, sig: `ed25519:${"0".repeat(128)}` });
|
|
129
135
|
if (error)
|
|
130
136
|
throw new Error(error);
|
|
@@ -139,9 +145,9 @@ export function verifyStatus(doc, opts = {}) {
|
|
|
139
145
|
const d = isObj(doc) ? doc : {};
|
|
140
146
|
const pqPolicy = opts.issuerDoc === undefined ? undefined : pqPolicyOf(opts.issuerDoc, opts.allowNoPq);
|
|
141
147
|
const policy = pqPolicy ? { pqPolicy } : {};
|
|
142
|
-
if ((d.v
|
|
148
|
+
if (!knownVersion(d.v) || d.type !== "certify.status" || !isDid(d.issuer) || !isObj(d.entries))
|
|
143
149
|
return { ok: false, reasons: ["not a status document (v1)"], freshness: null, ...policy };
|
|
144
|
-
const v2 = d.v
|
|
150
|
+
const v2 = d.v !== 1 ? v2Error(d) : null;
|
|
145
151
|
if (v2)
|
|
146
152
|
return { ok: false, reasons: [v2], freshness: null, ...policy };
|
|
147
153
|
const reasons = [];
|
|
@@ -165,9 +171,9 @@ export function verifyStatus(doc, opts = {}) {
|
|
|
165
171
|
if ("pq_binding" in d && !isSha(d.pq_binding))
|
|
166
172
|
reasons.push("status document has a malformed pq_binding");
|
|
167
173
|
const now = opts.now ? ms(opts.now) : Date.now();
|
|
168
|
-
if (d.v
|
|
169
|
-
reasons.push(
|
|
170
|
-
const freshness = (d.v
|
|
174
|
+
if (d.v !== 1 && ((!Number.isFinite(now)) || ms(d.issued_at) > now))
|
|
175
|
+
reasons.push(`status v${String(d.v)} reference time is invalid or issuance is in the future`);
|
|
176
|
+
const freshness = (d.v !== 1 ? ms(d.valid_until) > now : ms(d.valid_until) >= now) ? "fresh" : "stale";
|
|
171
177
|
if (freshness === "stale")
|
|
172
178
|
reasons.push(`status document expired on ${String(d.valid_until)}`);
|
|
173
179
|
if (opts.pq)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zanii/certify",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.1",
|
|
4
4
|
"description": "Build and verify Zanii Certify certificates for AI agents: fingerprints, signed certificates, status documents, test suites and the certify command.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://zanii.agency",
|