witnora 0.10.0 → 0.10.3
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/README.md +61 -9
- package/dist/assurance-contract.js +312 -0
- package/dist/assurance-loop-demo.js +245 -0
- package/dist/authorization-state-machine.js +35 -0
- package/dist/authorization-v02.js +60 -0
- package/dist/canonical-v02.js +197 -0
- package/dist/canonical.js +31 -0
- package/dist/cli.js +118 -0
- package/dist/command-help.js +76 -0
- package/dist/control-plane.js +3 -3
- package/dist/credentials.js +1 -1
- package/dist/design-partner-pilot.js +402 -0
- package/dist/design-partner-v02.js +449 -0
- package/dist/evidence-v02.js +118 -0
- package/dist/generic-eval.js +160 -0
- package/dist/github-design-partner-v02.js +399 -0
- package/dist/github-live-v02.js +45 -0
- package/dist/guided-setup.js +1 -1
- package/dist/index.js +1 -0
- package/dist/offline-verifier.js +375 -0
- package/dist/onboard.js +133 -0
- package/dist/onboarding-templates.js +30 -15
- package/dist/outcome-evaluator.js +51 -0
- package/dist/privacy-manifest.js +273 -0
- package/dist/probe-protocol-v02.js +101 -0
- package/dist/sandbox.js +1 -1
- package/dist/trust-v02.js +111 -0
- package/dist/try.js +1 -1
- package/dist/vendor/design-partner/failure-exercises.mjs +353 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts +8 -0
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.d.ts.map +1 -1
- package/dist/vendor/onegent-runtime/browser-enforcement-runtime.js +43 -1
- package/dist/vendor/witnora-probe/canonical.d.ts +3 -0
- package/dist/vendor/witnora-probe/canonical.js +39 -0
- package/dist/vendor/witnora-probe/cli.d.ts +2 -0
- package/dist/vendor/witnora-probe/cli.js +36 -0
- package/dist/vendor/witnora-probe/config.d.ts +2 -0
- package/dist/vendor/witnora-probe/config.js +63 -0
- package/dist/vendor/witnora-probe/credential-resolver.d.ts +6 -0
- package/dist/vendor/witnora-probe/credential-resolver.js +17 -0
- package/dist/vendor/witnora-probe/crypto.d.ts +2 -0
- package/dist/vendor/witnora-probe/crypto.js +14 -0
- package/dist/vendor/witnora-probe/emulator.d.ts +51 -0
- package/dist/vendor/witnora-probe/emulator.js +164 -0
- package/dist/vendor/witnora-probe/index.d.ts +11 -0
- package/dist/vendor/witnora-probe/index.js +11 -0
- package/dist/vendor/witnora-probe/logger.d.ts +3 -0
- package/dist/vendor/witnora-probe/logger.js +26 -0
- package/dist/vendor/witnora-probe/metrics.d.ts +12 -0
- package/dist/vendor/witnora-probe/metrics.js +31 -0
- package/dist/vendor/witnora-probe/probe.d.ts +22 -0
- package/dist/vendor/witnora-probe/probe.js +208 -0
- package/dist/vendor/witnora-probe/request-verifier.d.ts +9 -0
- package/dist/vendor/witnora-probe/request-verifier.js +84 -0
- package/dist/vendor/witnora-probe/server.d.ts +9 -0
- package/dist/vendor/witnora-probe/server.js +85 -0
- package/dist/vendor/witnora-probe/storage.d.ts +16 -0
- package/dist/vendor/witnora-probe/storage.js +114 -0
- package/dist/vendor/witnora-probe/types.d.ts +126 -0
- package/dist/vendor/witnora-probe/types.js +2 -0
- package/dist/vendor/witnora-verifier-python/README.md +43 -0
- package/dist/vendor/witnora-verifier-python/pyproject.toml +23 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/__init__.py +33 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/canonical.py +218 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/cli.py +75 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/conformance.py +124 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/crypto.py +139 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/errors.py +15 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/event_chain.py +80 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/evidence.py +336 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/probe.py +188 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/producer.py +127 -0
- package/dist/vendor/witnora-verifier-python/src/witnora_verifier/trust.py +337 -0
- package/package.json +4 -4
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export function evaluateOutcome(effect, observation, options = {}) {
|
|
2
|
+
const empty = { must: effect.must.map((predicate) => ({ predicate, satisfied: false })), mustNot: effect.mustNot.map((predicate) => ({ predicate, violated: false })) };
|
|
3
|
+
if (options.observationFailed)
|
|
4
|
+
return { status: "OBSERVATION_FAILED", ...empty };
|
|
5
|
+
if (options.timedOut)
|
|
6
|
+
return { status: "TIMED_OUT", ...empty };
|
|
7
|
+
if (options.stale)
|
|
8
|
+
return { status: "STALE_OBSERVATION", ...empty };
|
|
9
|
+
if (options.targetAuditConflict)
|
|
10
|
+
return { status: "CONFLICT", ...empty };
|
|
11
|
+
if (!observation || typeof observation !== "object")
|
|
12
|
+
return { status: "INDETERMINATE", ...empty };
|
|
13
|
+
const must = effect.must.map((predicate) => ({ predicate, satisfied: predicateMatches(observation, predicate) }));
|
|
14
|
+
const mustNot = effect.mustNot.map((predicate) => ({ predicate, violated: predicateMatches(observation, predicate) }));
|
|
15
|
+
const positive = must.filter((item) => item.satisfied).length;
|
|
16
|
+
const prohibited = mustNot.some((item) => item.violated);
|
|
17
|
+
if (positive === must.length && !prohibited && options.targetAuditReconciled === true)
|
|
18
|
+
return { status: "VERIFIED", must, mustNot };
|
|
19
|
+
if (positive > 0 && positive < must.length && !prohibited)
|
|
20
|
+
return { status: "PARTIAL", must, mustNot };
|
|
21
|
+
return { status: "NOT_VERIFIED", must, mustNot };
|
|
22
|
+
}
|
|
23
|
+
export function predicateMatches(value, predicate) {
|
|
24
|
+
const resolved = resolvePath(value, predicate.path);
|
|
25
|
+
if (predicate.operator === "exists")
|
|
26
|
+
return resolved.exists;
|
|
27
|
+
if (predicate.operator === "not_exists")
|
|
28
|
+
return !resolved.exists;
|
|
29
|
+
if (!resolved.exists)
|
|
30
|
+
return false;
|
|
31
|
+
if (predicate.operator === "equals")
|
|
32
|
+
return Object.is(resolved.value, predicate.value);
|
|
33
|
+
if (predicate.operator === "not_equals")
|
|
34
|
+
return !Object.is(resolved.value, predicate.value);
|
|
35
|
+
if (predicate.operator === "contains")
|
|
36
|
+
return typeof resolved.value === "string"
|
|
37
|
+
? resolved.value.includes(String(predicate.value))
|
|
38
|
+
: Array.isArray(resolved.value) && resolved.value.some((item) => Object.is(item, predicate.value));
|
|
39
|
+
if (predicate.operator === "count")
|
|
40
|
+
return (Array.isArray(resolved.value) || typeof resolved.value === "string") && resolved.value.length === predicate.value;
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
function resolvePath(value, path) {
|
|
44
|
+
let current = value;
|
|
45
|
+
for (const part of path.split(".")) {
|
|
46
|
+
if (!current || typeof current !== "object" || Array.isArray(current) || !Object.prototype.hasOwnProperty.call(current, part))
|
|
47
|
+
return { exists: false };
|
|
48
|
+
current = current[part];
|
|
49
|
+
}
|
|
50
|
+
return { exists: true, value: current };
|
|
51
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
import { canonicalJson, sha256Canonical } from "./canonical.js";
|
|
3
|
+
export const PRIVACY_MANIFEST_VERSION = "witnora.privacy_manifest.v0.1";
|
|
4
|
+
const SENSITIVE_NAME = /(authorization|api.?key|credential|cookie|password|private.?key|prompt|raw.?arg|raw.?result|secret|source.?code|token)/i;
|
|
5
|
+
const SENSITIVE_VALUE = /(-----BEGIN [A-Z ]*PRIVATE KEY-----|\bBearer\s+[A-Za-z0-9._~+\/-]+=*|\bsk_(?:live|test|proj)_[A-Za-z0-9_-]{8,}|\brk_(?:live|test)_[A-Za-z0-9_-]{8,}|\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,})/;
|
|
6
|
+
export function validatePrivacyManifest(input) {
|
|
7
|
+
const errors = [];
|
|
8
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
9
|
+
throw new Error("Privacy Manifest must be an object.");
|
|
10
|
+
const value = input;
|
|
11
|
+
const allowed = ["schemaVersion", "policyVersion", "tenantKeyId", "defaultDisposition", "fields", "cloudEndpoint", "artifacts", "encryption", "retentionDays", "debugMode", "keyOwnership"];
|
|
12
|
+
for (const key of Object.keys(value))
|
|
13
|
+
if (!allowed.includes(key))
|
|
14
|
+
errors.push(`$.${key} is not allowed.`);
|
|
15
|
+
for (const key of allowed)
|
|
16
|
+
if (!(key in value))
|
|
17
|
+
errors.push(`$.${key} is required.`);
|
|
18
|
+
if (value.schemaVersion !== PRIVACY_MANIFEST_VERSION)
|
|
19
|
+
errors.push(`$.schemaVersion must equal ${PRIVACY_MANIFEST_VERSION}.`);
|
|
20
|
+
if (typeof value.policyVersion !== "string" || !value.policyVersion)
|
|
21
|
+
errors.push("$.policyVersion must be a non-empty string.");
|
|
22
|
+
if (typeof value.tenantKeyId !== "string" || !value.tenantKeyId)
|
|
23
|
+
errors.push("$.tenantKeyId must be a non-empty string.");
|
|
24
|
+
if (value.defaultDisposition !== "deny")
|
|
25
|
+
errors.push("$.defaultDisposition must equal deny.");
|
|
26
|
+
if (!value.fields || typeof value.fields !== "object" || Array.isArray(value.fields))
|
|
27
|
+
errors.push("$.fields must be an object.");
|
|
28
|
+
else
|
|
29
|
+
for (const [path, disposition] of Object.entries(value.fields)) {
|
|
30
|
+
if (!path || path.startsWith(".") || path.endsWith("."))
|
|
31
|
+
errors.push(`Invalid field path ${JSON.stringify(path)}.`);
|
|
32
|
+
if (!["local_only", "disclose", "commit_only"].includes(disposition))
|
|
33
|
+
errors.push(`$.fields.${path} has an invalid disposition.`);
|
|
34
|
+
}
|
|
35
|
+
exactObject(value.cloudEndpoint, ["url", "transport"], "$.cloudEndpoint", errors);
|
|
36
|
+
const endpoint = value.cloudEndpoint;
|
|
37
|
+
if (typeof endpoint?.url !== "string" || !endpoint.url.startsWith("https://"))
|
|
38
|
+
errors.push("$.cloudEndpoint.url must use https.");
|
|
39
|
+
if (endpoint?.transport !== "https")
|
|
40
|
+
errors.push("$.cloudEndpoint.transport must equal https.");
|
|
41
|
+
exactObject(value.artifacts, ["rawUploadAllowed", "localStore", "pointerVisibility"], "$.artifacts", errors);
|
|
42
|
+
const artifacts = value.artifacts;
|
|
43
|
+
if (artifacts?.rawUploadAllowed !== false)
|
|
44
|
+
errors.push("$.artifacts.rawUploadAllowed must be false in v0.1.");
|
|
45
|
+
if (typeof artifacts?.localStore !== "string" || !artifacts.localStore)
|
|
46
|
+
errors.push("$.artifacts.localStore must be a non-empty string.");
|
|
47
|
+
if (artifacts?.pointerVisibility !== "opaque_private")
|
|
48
|
+
errors.push("$.artifacts.pointerVisibility must equal opaque_private.");
|
|
49
|
+
exactObject(value.encryption, ["inTransit", "atRest"], "$.encryption", errors);
|
|
50
|
+
const encryption = value.encryption;
|
|
51
|
+
if (encryption?.inTransit !== "TLS")
|
|
52
|
+
errors.push("$.encryption.inTransit must equal TLS.");
|
|
53
|
+
if (!["provider_managed", "customer_managed"].includes(String(encryption?.atRest)))
|
|
54
|
+
errors.push("$.encryption.atRest is invalid.");
|
|
55
|
+
if (!Number.isSafeInteger(value.retentionDays) || Number(value.retentionDays) < 1)
|
|
56
|
+
errors.push("$.retentionDays must be a positive safe integer.");
|
|
57
|
+
if (value.debugMode !== false)
|
|
58
|
+
errors.push("$.debugMode must be false.");
|
|
59
|
+
exactObject(value.keyOwnership, ["commitmentKey", "hostedDecryptionCapability"], "$.keyOwnership", errors);
|
|
60
|
+
const keys = value.keyOwnership;
|
|
61
|
+
if (keys?.commitmentKey !== "customer")
|
|
62
|
+
errors.push("$.keyOwnership.commitmentKey must equal customer.");
|
|
63
|
+
if (keys?.hostedDecryptionCapability !== "none")
|
|
64
|
+
errors.push("$.keyOwnership.hostedDecryptionCapability must equal none.");
|
|
65
|
+
if (errors.length)
|
|
66
|
+
throw new Error(`Invalid Privacy Manifest:\n${errors.map((error) => `- ${error}`).join("\n")}`);
|
|
67
|
+
return structuredClone(input);
|
|
68
|
+
}
|
|
69
|
+
export function transformForEgress(input, manifestInput, tenantKey) {
|
|
70
|
+
if (!tenantKey.byteLength)
|
|
71
|
+
throw new Error("A non-empty tenant privacy key is required.");
|
|
72
|
+
const manifest = validatePrivacyManifest(manifestInput);
|
|
73
|
+
const flat = flatten(input, "", manifest.fields);
|
|
74
|
+
const disclosed = {};
|
|
75
|
+
const commitments = {};
|
|
76
|
+
const omitted = [];
|
|
77
|
+
const unknown = Object.keys(flat).filter((path) => !(path in manifest.fields));
|
|
78
|
+
if (unknown.length)
|
|
79
|
+
throw new Error(`Privacy egress denied: undeclared fields ${unknown.sort().join(", ")}.`);
|
|
80
|
+
for (const path of Object.keys(flat).sort()) {
|
|
81
|
+
const value = flat[path];
|
|
82
|
+
const disposition = manifest.fields[path];
|
|
83
|
+
if (disposition === "local_only") {
|
|
84
|
+
omitted.push(path);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (disposition === "disclose") {
|
|
88
|
+
if (value && typeof value === "object")
|
|
89
|
+
throw new Error(`Privacy egress denied: ${path} is an object; disclose explicit leaf paths instead.`);
|
|
90
|
+
if (SENSITIVE_NAME.test(path) || containsSecret(value))
|
|
91
|
+
throw new Error(`Privacy egress denied: ${path} appears sensitive and cannot be disclosed.`);
|
|
92
|
+
setPath(disclosed, path, value);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
commitments[path] = {
|
|
96
|
+
algorithm: "HMAC-SHA256",
|
|
97
|
+
keyId: manifest.tenantKeyId,
|
|
98
|
+
digest: purposeHmac(tenantKey, manifest.tenantKeyId, path, value),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
schemaVersion: "witnora.minimal_disclosure.v0.1",
|
|
103
|
+
privacyManifestDigest: sha256Canonical(manifest),
|
|
104
|
+
disclosed,
|
|
105
|
+
commitments,
|
|
106
|
+
omitted,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export function validatePrivacyTransformAgainstManifest(manifestInput, transform) {
|
|
110
|
+
const manifest = validatePrivacyManifest(manifestInput);
|
|
111
|
+
const errors = [];
|
|
112
|
+
if (!transform || typeof transform !== "object" || Array.isArray(transform))
|
|
113
|
+
return ["Privacy transform must be an object."];
|
|
114
|
+
const value = transform;
|
|
115
|
+
exactObject(value, ["schemaVersion", "privacyManifestDigest", "disclosed", "commitments", "omitted"], "$", errors);
|
|
116
|
+
if (value.schemaVersion !== "witnora.minimal_disclosure.v0.1")
|
|
117
|
+
errors.push("Privacy transform schema version is unsupported.");
|
|
118
|
+
if (value.privacyManifestDigest !== sha256Canonical(manifest))
|
|
119
|
+
errors.push("Privacy transform references a different manifest digest.");
|
|
120
|
+
if (!isRecord(value.disclosed))
|
|
121
|
+
errors.push("Privacy transform disclosed output must be an object.");
|
|
122
|
+
if (!isRecord(value.commitments))
|
|
123
|
+
errors.push("Privacy transform commitments must be an object.");
|
|
124
|
+
if (!Array.isArray(value.omitted) || value.omitted.some((path) => typeof path !== "string"))
|
|
125
|
+
errors.push("Privacy transform omitted paths must be a string array.");
|
|
126
|
+
if (errors.length)
|
|
127
|
+
return errors;
|
|
128
|
+
const disclosed = flattenLeaves(value.disclosed);
|
|
129
|
+
const commitments = value.commitments;
|
|
130
|
+
const omitted = value.omitted;
|
|
131
|
+
const expectedDisclosed = pathsFor(manifest, "disclose");
|
|
132
|
+
const expectedCommitted = pathsFor(manifest, "commit_only");
|
|
133
|
+
const expectedOmitted = pathsFor(manifest, "local_only");
|
|
134
|
+
compareExactPaths(Object.keys(disclosed), expectedDisclosed, "disclosed", errors);
|
|
135
|
+
compareExactPaths(Object.keys(commitments), expectedCommitted, "committed", errors);
|
|
136
|
+
compareExactPaths(omitted, expectedOmitted, "omitted", errors);
|
|
137
|
+
for (const [path, disclosedValue] of Object.entries(disclosed)) {
|
|
138
|
+
if (SENSITIVE_NAME.test(path) || containsSecret(disclosedValue))
|
|
139
|
+
errors.push(`Disclosed path ${path} appears sensitive.`);
|
|
140
|
+
}
|
|
141
|
+
for (const [path, entry] of Object.entries(commitments)) {
|
|
142
|
+
exactObject(entry, ["algorithm", "keyId", "digest"], `$.commitments.${path}`, errors);
|
|
143
|
+
if (!isRecord(entry)
|
|
144
|
+
|| entry.algorithm !== "HMAC-SHA256"
|
|
145
|
+
|| entry.keyId !== manifest.tenantKeyId
|
|
146
|
+
|| typeof entry.digest !== "string"
|
|
147
|
+
|| !/^[a-f0-9]{64}$/.test(entry.digest))
|
|
148
|
+
errors.push(`Commitment ${path} is malformed or uses the wrong tenant key.`);
|
|
149
|
+
}
|
|
150
|
+
if (new Set(omitted).size !== omitted.length)
|
|
151
|
+
errors.push("Privacy transform contains duplicate omitted paths.");
|
|
152
|
+
return errors;
|
|
153
|
+
}
|
|
154
|
+
export function purposeHmac(key, keyId, purpose, value) {
|
|
155
|
+
return createHmac("sha256", key)
|
|
156
|
+
.update("witnora.tenant_commitment.v0.1\0")
|
|
157
|
+
.update(keyId)
|
|
158
|
+
.update("\0")
|
|
159
|
+
.update(purpose)
|
|
160
|
+
.update("\0")
|
|
161
|
+
.update(canonicalJson(value))
|
|
162
|
+
.digest("hex");
|
|
163
|
+
}
|
|
164
|
+
export function inspectPrivacy(manifestInput) {
|
|
165
|
+
const manifest = validatePrivacyManifest(manifestInput);
|
|
166
|
+
const groups = { local_only: [], disclose: [], commit_only: [] };
|
|
167
|
+
for (const [path, disposition] of Object.entries(manifest.fields))
|
|
168
|
+
groups[disposition].push(path);
|
|
169
|
+
return [
|
|
170
|
+
`Privacy manifest valid: ${manifest.policyVersion}`,
|
|
171
|
+
`Default egress: ${manifest.defaultDisposition}`,
|
|
172
|
+
`Cloud endpoint: ${manifest.cloudEndpoint.url} (${manifest.cloudEndpoint.transport})`,
|
|
173
|
+
`Disclosed: ${groups.disclose.sort().join(", ") || "none"}`,
|
|
174
|
+
`Committed with tenant HMAC: ${groups.commit_only.sort().join(", ") || "none"}`,
|
|
175
|
+
`Local only: ${groups.local_only.sort().join(", ") || "none"}`,
|
|
176
|
+
`Raw artifacts: remain in ${manifest.artifacts.localStore}; upload allowed: no`,
|
|
177
|
+
`Keys: commitment key held by ${manifest.keyOwnership.commitmentKey}; Hosted decryption: ${manifest.keyOwnership.hostedDecryptionCapability}`,
|
|
178
|
+
`Retention: ${manifest.retentionDays} days; debug mode: disabled`,
|
|
179
|
+
"Unknown egress path: none declared; undeclared fields fail closed.",
|
|
180
|
+
].join("\n") + "\n";
|
|
181
|
+
}
|
|
182
|
+
export function examplePrivacyManifest() {
|
|
183
|
+
return {
|
|
184
|
+
schemaVersion: PRIVACY_MANIFEST_VERSION,
|
|
185
|
+
policyVersion: "privacy:metadata-only:v1",
|
|
186
|
+
tenantKeyId: "local-demo-key-v1",
|
|
187
|
+
defaultDisposition: "deny",
|
|
188
|
+
fields: {
|
|
189
|
+
actionType: "disclose",
|
|
190
|
+
outcomeStatus: "disclose",
|
|
191
|
+
policyDecision: "disclose",
|
|
192
|
+
resourceId: "commit_only",
|
|
193
|
+
actionParameters: "commit_only",
|
|
194
|
+
prompt: "local_only",
|
|
195
|
+
credentials: "local_only",
|
|
196
|
+
rawResult: "local_only",
|
|
197
|
+
},
|
|
198
|
+
cloudEndpoint: { url: "https://witnora.com/v1/evidence/minimal-claims", transport: "https" },
|
|
199
|
+
artifacts: { rawUploadAllowed: false, localStore: ".witnora/local-evidence", pointerVisibility: "opaque_private" },
|
|
200
|
+
encryption: { inTransit: "TLS", atRest: "provider_managed" },
|
|
201
|
+
retentionDays: 90,
|
|
202
|
+
debugMode: false,
|
|
203
|
+
keyOwnership: { commitmentKey: "customer", hostedDecryptionCapability: "none" },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function exactObject(value, allowed, path, errors) {
|
|
207
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
208
|
+
errors.push(`${path} must be an object.`);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
const objectValue = value;
|
|
212
|
+
for (const key of Object.keys(objectValue))
|
|
213
|
+
if (!allowed.includes(key))
|
|
214
|
+
errors.push(`${path}.${key} is not allowed.`);
|
|
215
|
+
for (const key of allowed)
|
|
216
|
+
if (!(key in objectValue))
|
|
217
|
+
errors.push(`${path}.${key} is required.`);
|
|
218
|
+
}
|
|
219
|
+
function flatten(value, prefix = "", declared = {}) {
|
|
220
|
+
const output = {};
|
|
221
|
+
for (const [key, item] of Object.entries(value)) {
|
|
222
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
223
|
+
if (path in declared)
|
|
224
|
+
output[path] = item;
|
|
225
|
+
else if (item && typeof item === "object" && !Array.isArray(item))
|
|
226
|
+
Object.assign(output, flatten(item, path, declared));
|
|
227
|
+
else
|
|
228
|
+
output[path] = item;
|
|
229
|
+
}
|
|
230
|
+
return output;
|
|
231
|
+
}
|
|
232
|
+
function setPath(target, path, value) {
|
|
233
|
+
const parts = path.split(".");
|
|
234
|
+
let cursor = target;
|
|
235
|
+
for (const part of parts.slice(0, -1)) {
|
|
236
|
+
const next = cursor[part];
|
|
237
|
+
if (!next || typeof next !== "object" || Array.isArray(next))
|
|
238
|
+
cursor[part] = {};
|
|
239
|
+
cursor = cursor[part];
|
|
240
|
+
}
|
|
241
|
+
cursor[parts.at(-1)] = value;
|
|
242
|
+
}
|
|
243
|
+
function flattenLeaves(value, prefix = "") {
|
|
244
|
+
const output = {};
|
|
245
|
+
for (const [key, item] of Object.entries(value)) {
|
|
246
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
247
|
+
if (item && typeof item === "object" && !Array.isArray(item))
|
|
248
|
+
Object.assign(output, flattenLeaves(item, path));
|
|
249
|
+
else
|
|
250
|
+
output[path] = item;
|
|
251
|
+
}
|
|
252
|
+
return output;
|
|
253
|
+
}
|
|
254
|
+
function pathsFor(manifest, disposition) {
|
|
255
|
+
return Object.entries(manifest.fields).filter(([, value]) => value === disposition).map(([path]) => path).sort();
|
|
256
|
+
}
|
|
257
|
+
function compareExactPaths(actualInput, expected, label, errors) {
|
|
258
|
+
const actual = [...actualInput].sort();
|
|
259
|
+
const missing = expected.filter((path) => !actual.includes(path));
|
|
260
|
+
const unexpected = actual.filter((path) => !expected.includes(path));
|
|
261
|
+
if (missing.length)
|
|
262
|
+
errors.push(`Privacy transform is missing ${label} paths: ${missing.join(", ")}.`);
|
|
263
|
+
if (unexpected.length)
|
|
264
|
+
errors.push(`Privacy transform contains undeclared ${label} paths: ${unexpected.join(", ")}.`);
|
|
265
|
+
}
|
|
266
|
+
function isRecord(value) {
|
|
267
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
268
|
+
}
|
|
269
|
+
function containsSecret(value) {
|
|
270
|
+
if (typeof value === "string")
|
|
271
|
+
return SENSITIVE_VALUE.test(value);
|
|
272
|
+
return SENSITIVE_VALUE.test(canonicalJson(value));
|
|
273
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createPrivateKey, createPublicKey, randomUUID, sign, verify } from "node:crypto";
|
|
2
|
+
import { canonicalBytesV02, sha256CanonicalV02 } from "./canonical-v02.js";
|
|
3
|
+
import { assertNotRevoked, verifyKeyCertificate, verifyRevocationJournal } from "./trust-v02.js";
|
|
4
|
+
export function signProbeRequest(input, privateKeyPem) {
|
|
5
|
+
const core = { protocolVersion: "witnora.probe_observation_request.v0.2", requestId: input.requestId ?? randomUUID(), ...input };
|
|
6
|
+
return { ...core, signature: sign(null, canonicalBytesV02(core), createPrivateKey(privateKeyPem)).toString("base64") };
|
|
7
|
+
}
|
|
8
|
+
export function verifyProbeRequest(request, publicKeyPem, expected) {
|
|
9
|
+
const { signature, ...core } = request;
|
|
10
|
+
assertRequestShape(request);
|
|
11
|
+
if (!verify(null, canonicalBytesV02(core), createPublicKey(publicKeyPem), Buffer.from(signature, "base64")))
|
|
12
|
+
throw new Error("Probe observation request signature is invalid.");
|
|
13
|
+
assertBinding(request, { ...expected, at: expected.now });
|
|
14
|
+
const now = parseInstant(expected.now, "Probe verification time");
|
|
15
|
+
const notBefore = parseInstant(request.notBefore, "Probe request notBefore");
|
|
16
|
+
const expiresAt = parseInstant(request.expiresAt, "Probe request expiresAt");
|
|
17
|
+
const requestedAt = parseInstant(request.requestedAt, "Probe request requestedAt");
|
|
18
|
+
if (expiresAt <= notBefore || requestedAt < notBefore || requestedAt > expiresAt)
|
|
19
|
+
throw new Error("Probe observation request time window is invalid.");
|
|
20
|
+
if (now < notBefore || now > expiresAt)
|
|
21
|
+
throw new Error("Probe observation request is outside its validity interval.");
|
|
22
|
+
}
|
|
23
|
+
export function signProbeAttestation(input, privateKeyPem) {
|
|
24
|
+
const core = { protocolVersion: "witnora.probe_observation_attestation.v0.2", attestationId: input.attestationId ?? randomUUID(), ...input };
|
|
25
|
+
return { ...core, signature: sign(null, canonicalBytesV02(core), createPrivateKey(privateKeyPem)).toString("base64") };
|
|
26
|
+
}
|
|
27
|
+
export function verifyProbeAttestation(attestation, request, trust, expected) {
|
|
28
|
+
verifyRevocationJournal(trust.revocationJournal, trust.root);
|
|
29
|
+
assertAttestationShape(attestation);
|
|
30
|
+
const observedAtMs = parseInstant(attestation.observedAt, "Probe attestation observedAt");
|
|
31
|
+
const observedAt = new Date(observedAtMs);
|
|
32
|
+
const now = parseInstant(expected.now, "Probe attestation verification time");
|
|
33
|
+
const notBefore = parseInstant(request.notBefore, "Probe request notBefore");
|
|
34
|
+
const expiresAt = parseInstant(request.expiresAt, "Probe request expiresAt");
|
|
35
|
+
if (observedAtMs < notBefore || observedAtMs > expiresAt)
|
|
36
|
+
throw new Error("Probe observation falls outside the request window.");
|
|
37
|
+
if (observedAtMs > now + 30_000)
|
|
38
|
+
throw new Error("Probe observation is future-dated beyond clock skew.");
|
|
39
|
+
verifyKeyCertificate(trust.certificate, trust.root, observedAt, { role: "outcome_probe", tenantRef: expected.tenantRef, projectRef: expected.projectRef, environment: expected.environment });
|
|
40
|
+
assertNotRevoked(trust.certificate, trust.revocationJournal, observedAt);
|
|
41
|
+
const { signature, ...core } = attestation;
|
|
42
|
+
if (attestation.keyCertificateId !== trust.certificate.certificateId)
|
|
43
|
+
throw new Error("Probe attestation certificate ID mismatch.");
|
|
44
|
+
if (!verify(null, canonicalBytesV02(core), createPublicKey(trust.certificate.publicKeyPem), Buffer.from(signature, "base64")))
|
|
45
|
+
throw new Error("Probe observation attestation signature is invalid.");
|
|
46
|
+
if (attestation.requestId !== request.requestId || attestation.requestDigest !== sha256CanonicalV02(request) || attestation.nonce !== request.nonce)
|
|
47
|
+
throw new Error("Probe attestation is not bound to the observation request.");
|
|
48
|
+
assertBinding(attestation, { ...expected, at: observedAt });
|
|
49
|
+
if (!Number.isFinite(expected.maxFreshnessMs) || expected.maxFreshnessMs < 0)
|
|
50
|
+
throw new Error("Probe freshness limit is invalid.");
|
|
51
|
+
if (!Number.isInteger(attestation.dataFreshnessMs) || attestation.dataFreshnessMs < 0 || attestation.dataFreshnessMs > expected.maxFreshnessMs || now - observedAtMs > expected.maxFreshnessMs)
|
|
52
|
+
throw new Error("Probe observation is stale.");
|
|
53
|
+
}
|
|
54
|
+
export class ReplayLedger {
|
|
55
|
+
requests = new Set();
|
|
56
|
+
attestations = new Set();
|
|
57
|
+
acceptRequest(request) {
|
|
58
|
+
if (this.requests.has(request.nonce))
|
|
59
|
+
throw new Error("Probe request nonce was already used.");
|
|
60
|
+
this.requests.add(request.nonce);
|
|
61
|
+
}
|
|
62
|
+
acceptAttestation(attestation) {
|
|
63
|
+
if (this.attestations.has(attestation.attestationId))
|
|
64
|
+
throw new Error("Probe attestation ID was already used.");
|
|
65
|
+
this.attestations.add(attestation.attestationId);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function assertBinding(value, expected) {
|
|
69
|
+
for (const field of ["tenantRef", "projectRef", "environment", "actionDigest", "contractDigest", "resourceCommitment", "predicateDigest", "probeId"]) {
|
|
70
|
+
if (value[field] !== expected[field])
|
|
71
|
+
throw new Error(`Probe ${field} binding mismatch.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function assertRequestShape(request) {
|
|
75
|
+
if (request.protocolVersion !== "witnora.probe_observation_request.v0.2")
|
|
76
|
+
throw new Error("Unsupported Probe request version.");
|
|
77
|
+
for (const field of ["requestId", "nonce", "actionDigest", "contractDigest", "resourceCommitment", "predicateDigest", "probeId", "requestSignerKeyId", "signature"]) {
|
|
78
|
+
if (!request[field])
|
|
79
|
+
throw new Error(`Probe request ${field} is required.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function assertAttestationShape(attestation) {
|
|
83
|
+
if (attestation.protocolVersion !== "witnora.probe_observation_attestation.v0.2")
|
|
84
|
+
throw new Error("Unsupported Probe attestation version.");
|
|
85
|
+
for (const field of ["attestationId", "requestId", "requestDigest", "nonce", "probeId", "probeVersion", "observationSource", "rawObservationCommitment", "targetVersion", "keyCertificateId", "signature"]) {
|
|
86
|
+
if (!attestation[field])
|
|
87
|
+
throw new Error(`Probe attestation ${field} is required.`);
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(attestation.limitations))
|
|
90
|
+
throw new Error("Probe attestation limitations must be an array.");
|
|
91
|
+
if (!["VERIFIED", "NOT_VERIFIED", "INDETERMINATE"].includes(attestation.outcomeStatus))
|
|
92
|
+
throw new Error("Probe attestation outcome status is invalid.");
|
|
93
|
+
}
|
|
94
|
+
function parseInstant(value, field) {
|
|
95
|
+
if (typeof value === "string" && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value))
|
|
96
|
+
throw new Error(`${field} must be an RFC 3339 UTC timestamp.`);
|
|
97
|
+
const parsed = value instanceof Date ? value.getTime() : Date.parse(value);
|
|
98
|
+
if (!Number.isFinite(parsed))
|
|
99
|
+
throw new Error(`${field} must be a valid timestamp.`);
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
package/dist/sandbox.js
CHANGED
|
@@ -191,7 +191,7 @@ Runs the deterministic sandbox adapter conformance suite locally.
|
|
|
191
191
|
witnora sandbox push --adapter ./my-sandbox-adapter.js
|
|
192
192
|
|
|
193
193
|
Runs conformance checks, writes the local report, and uploads it through the saved Witnora connection.
|
|
194
|
-
Use \`
|
|
194
|
+
Use \`witnora onboard --project <project-id>\` first, or pass --server, --project, and --api-key.
|
|
195
195
|
`;
|
|
196
196
|
if (action === "stripe-readonly")
|
|
197
197
|
return `Usage:
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash, createPrivateKey, createPublicKey, sign, verify } from "node:crypto";
|
|
2
|
+
import { canonicalBytesV02, sha256CanonicalV02 } from "./canonical-v02.js";
|
|
3
|
+
export function createTrustRoot(input) {
|
|
4
|
+
const core = { ...input, protocolVersion: "witnora.trust_root.v0.2" };
|
|
5
|
+
return { ...core, rootFingerprint: createHash("sha256").update(createPublicKey(input.publicKeyPem).export({ format: "der", type: "spki" })).digest("hex") };
|
|
6
|
+
}
|
|
7
|
+
export function issueKeyCertificate(input, issuerPrivateKeyPem) {
|
|
8
|
+
const core = { ...input, protocolVersion: "witnora.key_certificate.v0.2" };
|
|
9
|
+
const certificateDigest = sha256CanonicalV02(core);
|
|
10
|
+
return { ...core, certificateDigest, signature: sign(null, canonicalBytesV02({ ...core, certificateDigest }), createPrivateKey(issuerPrivateKeyPem)).toString("base64") };
|
|
11
|
+
}
|
|
12
|
+
export function verifyKeyCertificate(certificate, root, at, expected) {
|
|
13
|
+
if (certificate.protocolVersion !== "witnora.key_certificate.v0.2")
|
|
14
|
+
throw new Error("Unsupported key certificate version.");
|
|
15
|
+
verifyPinnedRootShape(root);
|
|
16
|
+
const eventAt = parseInstant(at, "certificate verification time");
|
|
17
|
+
const rootNotBefore = parseInstant(root.notBefore, "trust root notBefore");
|
|
18
|
+
const rootNotAfter = parseInstant(root.notAfter, "trust root notAfter");
|
|
19
|
+
if (rootNotAfter <= rootNotBefore)
|
|
20
|
+
throw new Error("Pinned trust-root validity interval is invalid.");
|
|
21
|
+
if (eventAt < rootNotBefore || eventAt > rootNotAfter)
|
|
22
|
+
throw new Error("Pinned trust root is outside its validity interval.");
|
|
23
|
+
if (certificate.issuerRootId !== root.rootId)
|
|
24
|
+
throw new Error("Certificate issuer is not the pinned root.");
|
|
25
|
+
if (certificate.role !== expected.role || !root.allowedRoles.includes(certificate.role))
|
|
26
|
+
throw new Error("Certificate role is not authorized by the pinned root.");
|
|
27
|
+
if (certificate.tenantRef !== expected.tenantRef || certificate.projectRef !== expected.projectRef || certificate.environment !== expected.environment)
|
|
28
|
+
throw new Error("Certificate trust scope mismatch.");
|
|
29
|
+
const certificateNotBefore = parseInstant(certificate.notBefore, "certificate notBefore");
|
|
30
|
+
const certificateNotAfter = parseInstant(certificate.notAfter, "certificate notAfter");
|
|
31
|
+
if (certificateNotAfter <= certificateNotBefore)
|
|
32
|
+
throw new Error("Certificate validity interval is invalid.");
|
|
33
|
+
if (eventAt < certificateNotBefore || eventAt > certificateNotAfter)
|
|
34
|
+
throw new Error("Certificate is outside its validity interval.");
|
|
35
|
+
const { signature, certificateDigest, ...core } = certificate;
|
|
36
|
+
if (sha256CanonicalV02(core) !== certificateDigest)
|
|
37
|
+
throw new Error("Certificate digest mismatch.");
|
|
38
|
+
if (!verify(null, canonicalBytesV02({ ...core, certificateDigest }), createPublicKey(root.publicKeyPem), Buffer.from(signature, "base64")))
|
|
39
|
+
throw new Error("Certificate signature is invalid.");
|
|
40
|
+
}
|
|
41
|
+
export function appendRevocation(journal, input, rootPrivateKeyPem) {
|
|
42
|
+
const statements = journal?.statements ?? [];
|
|
43
|
+
const core = {
|
|
44
|
+
sequence: statements.length + 1,
|
|
45
|
+
previousJournalDigest: journal?.checkpointDigest ?? null,
|
|
46
|
+
...input,
|
|
47
|
+
protocolVersion: "witnora.key_revocation_statement.v0.2",
|
|
48
|
+
};
|
|
49
|
+
const statementDigest = sha256CanonicalV02(core);
|
|
50
|
+
const statement = { ...core, statementDigest, signature: sign(null, canonicalBytesV02({ ...core, statementDigest }), createPrivateKey(rootPrivateKeyPem)).toString("base64") };
|
|
51
|
+
const next = [...statements, statement];
|
|
52
|
+
return { protocolVersion: "witnora.revocation_journal.v0.2", rootId: input.issuerRootId, statements: next, checkpointDigest: sha256CanonicalV02(next) };
|
|
53
|
+
}
|
|
54
|
+
export function verifyRevocationJournal(journal, root) {
|
|
55
|
+
if (journal.protocolVersion !== "witnora.revocation_journal.v0.2")
|
|
56
|
+
throw new Error("Unsupported revocation journal version.");
|
|
57
|
+
verifyPinnedRootShape(root);
|
|
58
|
+
let previous = null;
|
|
59
|
+
for (const [index, statement] of journal.statements.entries()) {
|
|
60
|
+
if (statement.protocolVersion !== "witnora.key_revocation_statement.v0.2")
|
|
61
|
+
throw new Error("Unsupported revocation statement version.");
|
|
62
|
+
if (statement.sequence !== index + 1 || statement.previousJournalDigest !== previous || statement.issuerRootId !== root.rootId)
|
|
63
|
+
throw new Error("Revocation journal continuity is invalid.");
|
|
64
|
+
parseInstant(statement.effectiveAt, "revocation effectiveAt");
|
|
65
|
+
const { signature, statementDigest, ...core } = statement;
|
|
66
|
+
if (sha256CanonicalV02(core) !== statementDigest)
|
|
67
|
+
throw new Error("Revocation statement digest mismatch.");
|
|
68
|
+
if (!verify(null, canonicalBytesV02({ ...core, statementDigest }), createPublicKey(root.publicKeyPem), Buffer.from(signature, "base64")))
|
|
69
|
+
throw new Error("Revocation statement signature is invalid.");
|
|
70
|
+
previous = sha256CanonicalV02(journal.statements.slice(0, index + 1));
|
|
71
|
+
}
|
|
72
|
+
if (journal.rootId !== root.rootId || journal.checkpointDigest !== (previous ?? sha256CanonicalV02([])))
|
|
73
|
+
throw new Error("Revocation journal checkpoint mismatch.");
|
|
74
|
+
}
|
|
75
|
+
export function assertNotRevoked(certificate, journal, eventAt) {
|
|
76
|
+
const eventTime = parseInstant(eventAt, "revocation event time");
|
|
77
|
+
for (const revocation of journal.statements) {
|
|
78
|
+
if (revocation.revokedId !== certificate.certificateId && revocation.revokedId !== certificate.subjectKeyId)
|
|
79
|
+
continue;
|
|
80
|
+
const effectiveAt = parseInstant(revocation.effectiveAt, "revocation effectiveAt");
|
|
81
|
+
if (revocation.retroactive || eventTime >= effectiveAt)
|
|
82
|
+
throw new Error("Certificate was revoked for the event time.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export function assertRevocationJournalExtends(packetJournal, latestJournal) {
|
|
86
|
+
if (packetJournal.rootId !== latestJournal.rootId || packetJournal.statements.length > latestJournal.statements.length)
|
|
87
|
+
throw new Error("Revocation rollback detected: packet journal is not a prefix of the latest trusted journal.");
|
|
88
|
+
for (const [index, statement] of packetJournal.statements.entries()) {
|
|
89
|
+
if (statement.statementDigest !== latestJournal.statements[index]?.statementDigest)
|
|
90
|
+
throw new Error("Revocation rollback detected: packet journal diverges from the latest trusted journal.");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function parseInstant(value, field) {
|
|
94
|
+
if (typeof value === "string" && !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value))
|
|
95
|
+
throw new Error(`${field} must be an RFC 3339 UTC timestamp.`);
|
|
96
|
+
const parsed = value instanceof Date ? value.getTime() : Date.parse(value);
|
|
97
|
+
if (!Number.isFinite(parsed))
|
|
98
|
+
throw new Error(`${field} must be a valid timestamp.`);
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
function verifyPinnedRootShape(root) {
|
|
102
|
+
if (root.protocolVersion !== "witnora.trust_root.v0.2" || root.algorithm !== "Ed25519")
|
|
103
|
+
throw new Error("Unsupported pinned trust root.");
|
|
104
|
+
const actualFingerprint = createHash("sha256").update(createPublicKey(root.publicKeyPem).export({ format: "der", type: "spki" })).digest("hex");
|
|
105
|
+
if (root.rootFingerprint !== actualFingerprint)
|
|
106
|
+
throw new Error("Pinned trust-root fingerprint mismatch.");
|
|
107
|
+
const notBefore = parseInstant(root.notBefore, "trust root notBefore");
|
|
108
|
+
const notAfter = parseInstant(root.notAfter, "trust root notAfter");
|
|
109
|
+
if (notAfter <= notBefore)
|
|
110
|
+
throw new Error("Pinned trust-root validity interval is invalid.");
|
|
111
|
+
}
|
package/dist/try.js
CHANGED
|
@@ -92,7 +92,7 @@ export async function writeTryEvidence(options) {
|
|
|
92
92
|
kind: "synthetic_onboarding",
|
|
93
93
|
severity: "info",
|
|
94
94
|
message: `Deterministic ${options.template} sample passed. Replace it with evidence from the real agent boundary before making a release decision.`,
|
|
95
|
-
source: "
|
|
95
|
+
source: "witnora try",
|
|
96
96
|
artifactPath,
|
|
97
97
|
metadata: { synthetic: true, template: options.template, purpose: "onboarding_only" },
|
|
98
98
|
}],
|