witnora 0.10.0 → 0.10.2
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 +54 -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 +132 -0
- package/dist/onboarding-templates.js +3 -3
- 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,9 @@
|
|
|
1
|
+
import { type ProbeBindings, type ProbeObservationRequestV02, type ProbeObserveBodyV02, type TrustedRequestKey } from "./types.js";
|
|
2
|
+
export declare class RequestVerificationError extends Error {
|
|
3
|
+
readonly code: string;
|
|
4
|
+
readonly status: number;
|
|
5
|
+
constructor(code: string, message: string, status?: number);
|
|
6
|
+
}
|
|
7
|
+
export declare function verifyObservationRequest(body: ProbeObserveBodyV02, bindings: ProbeBindings, trustedKeys: Record<string, TrustedRequestKey>, now?: Date): ProbeObservationRequestV02;
|
|
8
|
+
export declare function observationRequestDigest(request: ProbeObservationRequestV02): string;
|
|
9
|
+
export declare function withoutSignature(request: ProbeObservationRequestV02): Omit<ProbeObservationRequestV02, "signature">;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { sha256Canonical } from "./canonical.js";
|
|
2
|
+
import { verifyCanonical } from "./crypto.js";
|
|
3
|
+
import { OBSERVATION_REQUEST_PROTOCOL, } from "./types.js";
|
|
4
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
5
|
+
export class RequestVerificationError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
status;
|
|
8
|
+
constructor(code, message, status = 400) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.status = status;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function verifyObservationRequest(body, bindings, trustedKeys, now = new Date()) {
|
|
15
|
+
const request = body?.request;
|
|
16
|
+
if (!request || body.expected === undefined || body.query === undefined) {
|
|
17
|
+
throw new RequestVerificationError("MALFORMED_REQUEST", "Body must contain request, expected, and query.");
|
|
18
|
+
}
|
|
19
|
+
if (request.protocolVersion !== OBSERVATION_REQUEST_PROTOCOL)
|
|
20
|
+
throw new RequestVerificationError("UNSUPPORTED_PROTOCOL", "Observation request protocol is unsupported.");
|
|
21
|
+
const key = trustedKeys[request.requestSignerKeyId];
|
|
22
|
+
if (!key || key.role !== "probe_observation_request_signer") {
|
|
23
|
+
throw new RequestVerificationError("UNTRUSTED_SIGNER_ROLE", "Observation request signer is not trusted for this role.", 401);
|
|
24
|
+
}
|
|
25
|
+
const unsigned = withoutSignature(request);
|
|
26
|
+
if (!verifyCanonical(unsigned, request.signature, key.publicKeyPem)) {
|
|
27
|
+
throw new RequestVerificationError("INVALID_SIGNATURE", "Observation request signature is invalid.", 401);
|
|
28
|
+
}
|
|
29
|
+
for (const field of ["requestId", "nonce", "requestSignerKeyId"])
|
|
30
|
+
requireText(request[field], field);
|
|
31
|
+
for (const field of ["actionDigest", "contractDigest", "resourceCommitment", "predicateDigest"]) {
|
|
32
|
+
if (!SHA256.test(request[field]))
|
|
33
|
+
throw new RequestVerificationError("INVALID_BINDING_DIGEST", `${field} must be a lowercase SHA-256 digest.`);
|
|
34
|
+
}
|
|
35
|
+
if (sha256Canonical(body.expected) !== request.predicateDigest) {
|
|
36
|
+
throw new RequestVerificationError("PREDICATE_BINDING_MISMATCH", "Expected predicate does not match predicateDigest.", 403);
|
|
37
|
+
}
|
|
38
|
+
if (sha256Canonical(body.query) !== request.resourceCommitment) {
|
|
39
|
+
throw new RequestVerificationError("RESOURCE_BINDING_MISMATCH", "Read query does not match resourceCommitment.", 403);
|
|
40
|
+
}
|
|
41
|
+
const requestedAt = parseTime(request.requestedAt, "requestedAt");
|
|
42
|
+
const notBefore = parseTime(request.notBefore, "notBefore");
|
|
43
|
+
const expiresAt = parseTime(request.expiresAt, "expiresAt");
|
|
44
|
+
const keyValidFrom = parseTime(key.validFrom, "key.validFrom");
|
|
45
|
+
const keyValidUntil = parseTime(key.validUntil, "key.validUntil");
|
|
46
|
+
if (expiresAt <= notBefore || requestedAt < notBefore || requestedAt > expiresAt)
|
|
47
|
+
throw new RequestVerificationError("INVALID_TIME_WINDOW", "Observation request time window is invalid.");
|
|
48
|
+
if (now.getTime() < notBefore)
|
|
49
|
+
throw new RequestVerificationError("REQUEST_NOT_YET_VALID", "Observation request is not yet valid.", 401);
|
|
50
|
+
if (now.getTime() >= expiresAt)
|
|
51
|
+
throw new RequestVerificationError("REQUEST_EXPIRED", "Observation request has expired.", 401);
|
|
52
|
+
if (requestedAt < keyValidFrom || requestedAt >= keyValidUntil)
|
|
53
|
+
throw new RequestVerificationError("SIGNER_KEY_TIME_INVALID", "Request was signed outside the trusted key validity interval.", 401);
|
|
54
|
+
if (key.revokedAt && now.getTime() >= parseTime(key.revokedAt, "key.revokedAt")) {
|
|
55
|
+
throw new RequestVerificationError("SIGNER_KEY_REVOKED", "Request signing key is revoked.", 401);
|
|
56
|
+
}
|
|
57
|
+
const expectedBindings = { ...bindings, probeId: bindings.probeId };
|
|
58
|
+
for (const field of ["tenantRef", "projectRef", "environment", "probeId"]) {
|
|
59
|
+
if (request[field] !== expectedBindings[field])
|
|
60
|
+
throw new RequestVerificationError("BOUNDARY_MISMATCH", `Observation request ${field} does not match this Probe.`, 403);
|
|
61
|
+
}
|
|
62
|
+
return request;
|
|
63
|
+
}
|
|
64
|
+
export function observationRequestDigest(request) {
|
|
65
|
+
return sha256Canonical(request);
|
|
66
|
+
}
|
|
67
|
+
export function withoutSignature(request) {
|
|
68
|
+
const { signature: _signature, ...unsigned } = request;
|
|
69
|
+
return unsigned;
|
|
70
|
+
}
|
|
71
|
+
function parseTime(value, field) {
|
|
72
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(value)) {
|
|
73
|
+
throw new RequestVerificationError("INVALID_TIMESTAMP", `${field} must be an RFC 3339 UTC timestamp.`);
|
|
74
|
+
}
|
|
75
|
+
const parsed = Date.parse(value);
|
|
76
|
+
if (!Number.isFinite(parsed))
|
|
77
|
+
throw new RequestVerificationError("INVALID_TIMESTAMP", `${field} must be an ISO timestamp.`);
|
|
78
|
+
return parsed;
|
|
79
|
+
}
|
|
80
|
+
function requireText(value, field) {
|
|
81
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 512) {
|
|
82
|
+
throw new RequestVerificationError("INVALID_FIELD", `${field} must be a non-empty bounded string.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type IncomingMessage, type ServerResponse } from "node:http";
|
|
2
|
+
import type { AddressInfo } from "node:net";
|
|
3
|
+
import { WitnoraProbe } from "./probe.js";
|
|
4
|
+
import type { StructuredLogger } from "./types.js";
|
|
5
|
+
export declare function createProbeServer(probe: WitnoraProbe, logger: StructuredLogger, bodyLimitBytes: number): {
|
|
6
|
+
server: import("http").Server<typeof IncomingMessage, typeof ServerResponse>;
|
|
7
|
+
listen(host: string, port: number): Promise<AddressInfo>;
|
|
8
|
+
close(): Promise<void>;
|
|
9
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { RequestVerificationError } from "./request-verifier.js";
|
|
3
|
+
export function createProbeServer(probe, logger, bodyLimitBytes) {
|
|
4
|
+
const server = createServer(async (request, response) => {
|
|
5
|
+
response.setHeader("content-type", "application/json; charset=utf-8");
|
|
6
|
+
response.setHeader("cache-control", "no-store");
|
|
7
|
+
response.setHeader("x-content-type-options", "nosniff");
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(request.url ?? "/", "http://probe.local");
|
|
10
|
+
if (request.method === "GET" && url.pathname === "/health")
|
|
11
|
+
return json(response, 200, { status: "healthy" });
|
|
12
|
+
if (request.method === "GET" && url.pathname === "/ready")
|
|
13
|
+
return json(response, probe.isReady() ? 200 : 503, { status: probe.isReady() ? "ready" : "not_ready" });
|
|
14
|
+
if (request.method === "GET" && url.pathname === "/metrics") {
|
|
15
|
+
response.setHeader("content-type", "text/plain; version=0.0.4; charset=utf-8");
|
|
16
|
+
response.statusCode = 200;
|
|
17
|
+
response.end(probe.metrics.render());
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
if (request.method === "POST" && url.pathname === "/v0.2/observe") {
|
|
21
|
+
const body = await readJson(request, bodyLimitBytes);
|
|
22
|
+
return json(response, 200, await probe.observe(body));
|
|
23
|
+
}
|
|
24
|
+
return json(response, 404, { error: { code: "NOT_FOUND", message: "Route not found." } });
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
const status = error instanceof RequestVerificationError ? error.status : error instanceof PayloadError ? error.status : 500;
|
|
28
|
+
const code = error instanceof RequestVerificationError ? error.code : error instanceof PayloadError ? error.code : "INTERNAL_ERROR";
|
|
29
|
+
if (code === "INVALID_SIGNATURE")
|
|
30
|
+
probe.metrics.increment("probe_signature_failures_total");
|
|
31
|
+
if (code === "REQUEST_EXPIRED" || code === "REQUEST_NOT_YET_VALID" || code === "INVALID_TIME_WINDOW")
|
|
32
|
+
probe.metrics.increment("probe_stale_observations_total");
|
|
33
|
+
logger.warn("probe.http_request_rejected", { code, status });
|
|
34
|
+
return json(response, status, { error: { code, message: publicMessage(error) } });
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
server,
|
|
39
|
+
async listen(host, port) {
|
|
40
|
+
await new Promise((resolve, reject) => {
|
|
41
|
+
server.once("error", reject);
|
|
42
|
+
server.listen(port, host, () => resolve());
|
|
43
|
+
});
|
|
44
|
+
return server.address();
|
|
45
|
+
},
|
|
46
|
+
async close() {
|
|
47
|
+
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
async function readJson(request, limit) {
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let size = 0;
|
|
54
|
+
for await (const chunk of request) {
|
|
55
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
56
|
+
size += buffer.byteLength;
|
|
57
|
+
if (size > limit)
|
|
58
|
+
throw new PayloadError("PAYLOAD_TOO_LARGE", "Request body exceeds the configured limit.", 413);
|
|
59
|
+
chunks.push(buffer);
|
|
60
|
+
}
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
throw new PayloadError("INVALID_JSON", "Request body must contain valid JSON.", 400);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function json(response, status, value) {
|
|
69
|
+
response.statusCode = status;
|
|
70
|
+
response.end(JSON.stringify(value));
|
|
71
|
+
}
|
|
72
|
+
class PayloadError extends Error {
|
|
73
|
+
code;
|
|
74
|
+
status;
|
|
75
|
+
constructor(code, message, status) {
|
|
76
|
+
super(message);
|
|
77
|
+
this.code = code;
|
|
78
|
+
this.status = status;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function publicMessage(error) {
|
|
82
|
+
if (error instanceof RequestVerificationError || error instanceof PayloadError)
|
|
83
|
+
return error.message;
|
|
84
|
+
return "Probe could not complete the request.";
|
|
85
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class EncryptedJsonStore {
|
|
2
|
+
private readonly root;
|
|
3
|
+
private readonly key;
|
|
4
|
+
constructor(root: string, key: Buffer);
|
|
5
|
+
put(category: string, id: string, value: unknown): Promise<string>;
|
|
6
|
+
get<T>(category: string, id: string): Promise<T | undefined>;
|
|
7
|
+
list(category: string): Promise<string[]>;
|
|
8
|
+
delete(category: string, id: string): Promise<void>;
|
|
9
|
+
private path;
|
|
10
|
+
}
|
|
11
|
+
export declare class PersistentNonceCache {
|
|
12
|
+
private readonly root;
|
|
13
|
+
constructor(root: string);
|
|
14
|
+
claim(nonce: string, expiresAt: string): Promise<boolean>;
|
|
15
|
+
private prune;
|
|
16
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
|
2
|
+
import { mkdir, open, readFile, readdir, rename, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { canonicalJson, sha256Canonical } from "./canonical.js";
|
|
5
|
+
export class EncryptedJsonStore {
|
|
6
|
+
root;
|
|
7
|
+
key;
|
|
8
|
+
constructor(root, key) {
|
|
9
|
+
this.root = root;
|
|
10
|
+
this.key = key;
|
|
11
|
+
if (key.byteLength !== 32)
|
|
12
|
+
throw new Error("Local storage encryption key must contain exactly 32 bytes.");
|
|
13
|
+
}
|
|
14
|
+
async put(category, id, value) {
|
|
15
|
+
const path = this.path(category, id);
|
|
16
|
+
await mkdir(dirname(path), { recursive: true });
|
|
17
|
+
const iv = randomBytes(12);
|
|
18
|
+
const cipher = createCipheriv("aes-256-gcm", this.key, iv);
|
|
19
|
+
const ciphertext = Buffer.concat([cipher.update(canonicalJson(value), "utf8"), cipher.final()]);
|
|
20
|
+
const envelope = {
|
|
21
|
+
version: "witnora.local_aes_256_gcm.v0.1",
|
|
22
|
+
iv: iv.toString("base64"),
|
|
23
|
+
tag: cipher.getAuthTag().toString("base64"),
|
|
24
|
+
ciphertext: ciphertext.toString("base64"),
|
|
25
|
+
};
|
|
26
|
+
const temporary = `${path}.${process.pid}.${randomBytes(4).toString("hex")}.tmp`;
|
|
27
|
+
await writeFile(temporary, JSON.stringify(envelope), { mode: 0o600 });
|
|
28
|
+
await rename(temporary, path);
|
|
29
|
+
return `${category}/${safeId(id)}.enc.json`;
|
|
30
|
+
}
|
|
31
|
+
async get(category, id) {
|
|
32
|
+
try {
|
|
33
|
+
const envelope = JSON.parse(await readFile(this.path(category, id), "utf8"));
|
|
34
|
+
const decipher = createDecipheriv("aes-256-gcm", this.key, Buffer.from(envelope.iv, "base64"));
|
|
35
|
+
decipher.setAuthTag(Buffer.from(envelope.tag, "base64"));
|
|
36
|
+
const plaintext = Buffer.concat([
|
|
37
|
+
decipher.update(Buffer.from(envelope.ciphertext, "base64")),
|
|
38
|
+
decipher.final(),
|
|
39
|
+
]);
|
|
40
|
+
return JSON.parse(plaintext.toString("utf8"));
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error.code === "ENOENT")
|
|
44
|
+
return undefined;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async list(category) {
|
|
49
|
+
try {
|
|
50
|
+
return (await readdir(join(this.root, category)))
|
|
51
|
+
.filter((name) => name.endsWith(".enc.json"))
|
|
52
|
+
.map((name) => name.slice(0, -".enc.json".length));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error.code === "ENOENT")
|
|
56
|
+
return [];
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async delete(category, id) {
|
|
61
|
+
try {
|
|
62
|
+
await unlink(this.path(category, id));
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (error.code !== "ENOENT")
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
path(category, id) {
|
|
70
|
+
return join(this.root, safeId(category), `${safeId(id)}.enc.json`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export class PersistentNonceCache {
|
|
74
|
+
root;
|
|
75
|
+
constructor(root) {
|
|
76
|
+
this.root = root;
|
|
77
|
+
}
|
|
78
|
+
async claim(nonce, expiresAt) {
|
|
79
|
+
await this.prune();
|
|
80
|
+
await mkdir(this.root, { recursive: true });
|
|
81
|
+
const digest = sha256Canonical(nonce);
|
|
82
|
+
try {
|
|
83
|
+
const handle = await open(join(this.root, `${digest}.nonce`), "wx", 0o600);
|
|
84
|
+
await handle.writeFile(expiresAt);
|
|
85
|
+
await handle.close();
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
if (error.code === "EEXIST")
|
|
90
|
+
return false;
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async prune() {
|
|
95
|
+
try {
|
|
96
|
+
const now = Date.now();
|
|
97
|
+
for (const name of await readdir(this.root)) {
|
|
98
|
+
if (!name.endsWith(".nonce"))
|
|
99
|
+
continue;
|
|
100
|
+
const path = join(this.root, name);
|
|
101
|
+
const expiry = Date.parse(await readFile(path, "utf8"));
|
|
102
|
+
if (!Number.isFinite(expiry) || expiry < now)
|
|
103
|
+
await unlink(path);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (error.code !== "ENOENT")
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function safeId(value) {
|
|
113
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 160);
|
|
114
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
export declare const OBSERVATION_REQUEST_PROTOCOL: "witnora.probe_observation_request.v0.2";
|
|
2
|
+
export declare const OBSERVATION_ATTESTATION_PROTOCOL: "witnora.probe_observation_attestation.v0.2";
|
|
3
|
+
export interface ProbeObservationRequestV02 {
|
|
4
|
+
protocolVersion: typeof OBSERVATION_REQUEST_PROTOCOL;
|
|
5
|
+
requestId: string;
|
|
6
|
+
nonce: string;
|
|
7
|
+
tenantRef: string;
|
|
8
|
+
projectRef: string;
|
|
9
|
+
environment: string;
|
|
10
|
+
actionDigest: string;
|
|
11
|
+
contractDigest: string;
|
|
12
|
+
resourceCommitment: string;
|
|
13
|
+
predicateDigest: string;
|
|
14
|
+
probeId: string;
|
|
15
|
+
notBefore: string;
|
|
16
|
+
expiresAt: string;
|
|
17
|
+
requestedAt: string;
|
|
18
|
+
requestSignerKeyId: string;
|
|
19
|
+
signature: string;
|
|
20
|
+
}
|
|
21
|
+
export interface GitHubPullRequestReadQueryV02 {
|
|
22
|
+
type: "github.pull_request.read.v0.2";
|
|
23
|
+
repository: string;
|
|
24
|
+
pullRequestNumber: number;
|
|
25
|
+
}
|
|
26
|
+
export interface GitHubPullRequestExpectedV02 {
|
|
27
|
+
type: "github.pull_request.outcome.v0.2";
|
|
28
|
+
merged: boolean;
|
|
29
|
+
headSha: string;
|
|
30
|
+
baseBranch: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ProbeObserveBodyV02<Expected = unknown, Query = unknown> {
|
|
33
|
+
request: ProbeObservationRequestV02;
|
|
34
|
+
expected: Expected;
|
|
35
|
+
query: Query;
|
|
36
|
+
}
|
|
37
|
+
export interface ProbeObservationAttestationV02 {
|
|
38
|
+
protocolVersion: typeof OBSERVATION_ATTESTATION_PROTOCOL;
|
|
39
|
+
attestationId: string;
|
|
40
|
+
requestId: string;
|
|
41
|
+
requestDigest: string;
|
|
42
|
+
nonce: string;
|
|
43
|
+
tenantRef: string;
|
|
44
|
+
projectRef: string;
|
|
45
|
+
environment: string;
|
|
46
|
+
actionDigest: string;
|
|
47
|
+
contractDigest: string;
|
|
48
|
+
resourceCommitment: string;
|
|
49
|
+
predicateDigest: string;
|
|
50
|
+
probeId: string;
|
|
51
|
+
probeVersion: string;
|
|
52
|
+
observationSource: string;
|
|
53
|
+
observedAt: string;
|
|
54
|
+
dataFreshnessMs: number;
|
|
55
|
+
normalizedObservation: Record<string, unknown>;
|
|
56
|
+
rawObservationCommitment: string;
|
|
57
|
+
targetVersion: string;
|
|
58
|
+
targetAuditReference?: string;
|
|
59
|
+
outcomeStatus: "VERIFIED" | "NOT_VERIFIED";
|
|
60
|
+
limitations: string[];
|
|
61
|
+
keyCertificateId: string;
|
|
62
|
+
signature: string;
|
|
63
|
+
}
|
|
64
|
+
export interface AdapterObservation {
|
|
65
|
+
normalizedObservation: Record<string, unknown>;
|
|
66
|
+
rawObservation: unknown;
|
|
67
|
+
observationSource: string;
|
|
68
|
+
sourceObservedAt: string;
|
|
69
|
+
targetVersion: string;
|
|
70
|
+
targetAuditReference?: string;
|
|
71
|
+
limitations: string[];
|
|
72
|
+
}
|
|
73
|
+
export interface ObservationTargetAdapter {
|
|
74
|
+
readonly id: string;
|
|
75
|
+
supports(query: unknown): boolean;
|
|
76
|
+
observe(input: {
|
|
77
|
+
query: unknown;
|
|
78
|
+
credential?: string;
|
|
79
|
+
signal: AbortSignal;
|
|
80
|
+
}): Promise<AdapterObservation>;
|
|
81
|
+
}
|
|
82
|
+
export interface ProbeBindings {
|
|
83
|
+
tenantRef: string;
|
|
84
|
+
projectRef: string;
|
|
85
|
+
environment: string;
|
|
86
|
+
probeId: string;
|
|
87
|
+
}
|
|
88
|
+
export interface TrustedRequestKey {
|
|
89
|
+
publicKeyPem: string;
|
|
90
|
+
role: "probe_observation_request_signer";
|
|
91
|
+
validFrom: string;
|
|
92
|
+
validUntil: string;
|
|
93
|
+
revokedAt?: string;
|
|
94
|
+
}
|
|
95
|
+
export interface ProbeConfig {
|
|
96
|
+
host: string;
|
|
97
|
+
port: number;
|
|
98
|
+
stateDirectory: string;
|
|
99
|
+
requestBodyLimitBytes: number;
|
|
100
|
+
observationTimeoutMs: number;
|
|
101
|
+
shutdownTimeoutMs: number;
|
|
102
|
+
retry: {
|
|
103
|
+
maxAttempts: number;
|
|
104
|
+
baseDelayMs: number;
|
|
105
|
+
scanIntervalMs: number;
|
|
106
|
+
};
|
|
107
|
+
bindings: ProbeBindings;
|
|
108
|
+
requestVerificationKeys: Record<string, TrustedRequestKey>;
|
|
109
|
+
probeSigningKey: {
|
|
110
|
+
keyCertificateId: string;
|
|
111
|
+
privateKeyPem: string;
|
|
112
|
+
};
|
|
113
|
+
probeVersion: string;
|
|
114
|
+
storageEncryptionKey: Buffer;
|
|
115
|
+
adapters: ObservationTargetAdapter[];
|
|
116
|
+
credentialReference?: string;
|
|
117
|
+
credentialResolver: CredentialResolver;
|
|
118
|
+
}
|
|
119
|
+
export interface CredentialResolver {
|
|
120
|
+
resolve(reference: string): Promise<string>;
|
|
121
|
+
}
|
|
122
|
+
export interface StructuredLogger {
|
|
123
|
+
info(event: string, fields?: Record<string, unknown>): void;
|
|
124
|
+
warn(event: string, fields?: Record<string, unknown>): void;
|
|
125
|
+
error(event: string, fields?: Record<string, unknown>): void;
|
|
126
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Witnora verifier for Python
|
|
2
|
+
|
|
3
|
+
This package independently verifies the strict `witnora.canonical_json.v0.2`
|
|
4
|
+
profile and the v0.2 trust, Probe, and event-chain primitives without contacting
|
|
5
|
+
Witnora or any customer system.
|
|
6
|
+
|
|
7
|
+
The verifier is deliberately fail-closed. Trust roots must be pinned by the
|
|
8
|
+
caller. A root carried inside an evidence packet cannot authorize itself.
|
|
9
|
+
|
|
10
|
+
## Supported slice
|
|
11
|
+
|
|
12
|
+
- deterministic UTF-8 canonical bytes and SHA-256;
|
|
13
|
+
- strict JSON parsing with duplicate-key and ambiguous-number rejection;
|
|
14
|
+
- Ed25519 signatures using raw base64url public keys;
|
|
15
|
+
- role-scoped subordinate key certificates;
|
|
16
|
+
- signed, hash-linked revocation journals with event-time semantics;
|
|
17
|
+
- signed Probe observation requests and bound attestations;
|
|
18
|
+
- structural event hash-chain verification;
|
|
19
|
+
- offline verification of the shared v0.2 conformance corpus.
|
|
20
|
+
|
|
21
|
+
```powershell
|
|
22
|
+
python -m unittest discover -s tests -v
|
|
23
|
+
witnora-verify conformance ../../test-vectors/witnora-v0.2-conformance
|
|
24
|
+
witnora-verify canonical input.json
|
|
25
|
+
witnora-verify event-chain events.json
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Strict canonical profile
|
|
29
|
+
|
|
30
|
+
This is a Witnora-specific profile, not a claim of RFC 8785 conformance.
|
|
31
|
+
|
|
32
|
+
- object keys are sorted by Unicode code point;
|
|
33
|
+
- strings are emitted as Unicode and encoded as strict UTF-8;
|
|
34
|
+
- array order is preserved;
|
|
35
|
+
- integers must be within `[-(2^53-1), 2^53-1]`;
|
|
36
|
+
- JSON numbers must be safe integers; decimal values must be represented by an
|
|
37
|
+
application-level normalized string;
|
|
38
|
+
- negative zero, non-finite numbers, duplicate keys, unpaired surrogates, and
|
|
39
|
+
unsupported Python values are rejected;
|
|
40
|
+
- nesting depth is limited to 64 containers.
|
|
41
|
+
|
|
42
|
+
The package does not preserve or alter v0.1 behavior. It accepts only explicit
|
|
43
|
+
v0.2 protocol identifiers.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "witnora-verifier"
|
|
7
|
+
version = "0.2.0.dev0"
|
|
8
|
+
description = "Offline verifier for the strict Witnora canonical JSON v0.2 profile"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "Apache-2.0"
|
|
12
|
+
dependencies = ["cryptography>=43"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
16
|
+
"Topic :: Security :: Cryptography",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
witnora-verify = "witnora_verifier.cli:main"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
where = ["src"]
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Offline verification primitives for Witnora v0.2."""
|
|
2
|
+
|
|
3
|
+
from .canonical import (
|
|
4
|
+
CANONICAL_VERSION,
|
|
5
|
+
canonical_bytes,
|
|
6
|
+
canonical_json,
|
|
7
|
+
loads_strict,
|
|
8
|
+
sha256_canonical,
|
|
9
|
+
)
|
|
10
|
+
from .crypto import sign_value, verify_signed_value
|
|
11
|
+
from .errors import VerificationError
|
|
12
|
+
from .event_chain import make_event, verify_event_chain
|
|
13
|
+
from .evidence import verify_evidence_packet
|
|
14
|
+
from .probe import verify_probe_exchange
|
|
15
|
+
from .trust import PinnedTrustStore, verify_key_certificate, verify_revocation_journal
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CANONICAL_VERSION",
|
|
19
|
+
"PinnedTrustStore",
|
|
20
|
+
"VerificationError",
|
|
21
|
+
"canonical_bytes",
|
|
22
|
+
"canonical_json",
|
|
23
|
+
"loads_strict",
|
|
24
|
+
"make_event",
|
|
25
|
+
"sha256_canonical",
|
|
26
|
+
"sign_value",
|
|
27
|
+
"verify_event_chain",
|
|
28
|
+
"verify_evidence_packet",
|
|
29
|
+
"verify_key_certificate",
|
|
30
|
+
"verify_probe_exchange",
|
|
31
|
+
"verify_revocation_journal",
|
|
32
|
+
"verify_signed_value",
|
|
33
|
+
]
|