// SPDX-License-Identifier: Apache-2.0 // (c) 2026 Lutar, Stephen P. -- SZL Holdings // ORCID: 0009-0001-0110-4173 // // Layer 6 -- severity-indexed witness threshold policy gate. // // Policy rationale: // High-consequence actions should not pass on a flat confidence score. The // evidence threshold rises with severity, and the witness quorum rises from // 2-of-N for ordinary/property actions to 3-of-N for capital/critical actions. // This runtime gate is TypeScript-only: it does not claim Lean closure. // // Policy: allow iff confidence >= severity-adjusted threshold and the attested // unique witness count meets the required quorum. Allowed decisions emit a // deterministic DSSE-shaped HMAC receipt for downstream audit. import { createHmac, createHash } from "node:crypto"; export type SeverityClass = "low" | "medium" | "high" | "critical" | "capital"; export type DecisionClass = "ordinary" | "property" | "capital"; export interface SeverityWitness { id: string; role: string; attested: boolean; } export interface DsseSignature { keyid: string; sig: string; } export interface DsseEnvelope { payloadType: "application/vnd.szl.threshold-policy.v1+json"; payload: string; signatures: DsseSignature[]; } export interface ThresholdPolicySeverityGateConfig { baseThreshold?: number; severitySlope?: number; maxThreshold?: number; standardWitnesses?: number; capitalWitnesses?: number; signingKey?: string; keyId?: string; } export interface ThresholdPolicySeverityGateOpts { actionId: string; severity: SeverityClass; decisionClass?: DecisionClass; confidence: number; witnesses: readonly SeverityWitness[]; } export interface ThresholdPolicySeverityDecision { allow: boolean; rationale: string; formula: "ThresholdPolicySeverity"; claimStatus: "verified-runtime"; leanTheorem: null; leanFile: null; leanCommitSha: null; actionId: string; severity: SeverityClass; decisionClass: DecisionClass; confidence: number; requiredThreshold: number; requiredWitnesses: number; attestedWitnesses: number; witnessIds: readonly string[]; lambdaScore: number; dsseReceipt?: DsseEnvelope; } const DEFAULT_BASE_THRESHOLD = 0.70; const DEFAULT_SEVERITY_SLOPE = 0.20; const DEFAULT_MAX_THRESHOLD = 0.95; const DEFAULT_STANDARD_WITNESSES = 2; const DEFAULT_CAPITAL_WITNESSES = 3; // DEMO ONLY — not non-repudiable. DEFAULT_SIGNING_KEY is a hard-coded symmetric // HMAC secret for local/dev use. HMAC-SHA-256 gives integrity + authenticity to // key-holders but is NOT an asymmetric signature: anyone with read access to the // key can mint receipts and there is zero non-repudiation. In production replace // with an Ed25519 keypair (production signature scheme — non-repudiable) or a // Sigstore keyless flow (https://docs.sigstore.dev/cosign/signing/signing_with_blobs/). // See PhD Crypto Verdict 2026-05-30 Finding A2. const DEFAULT_SIGNING_KEY = "a11oy-threshold-policy-dev-key"; const DEFAULT_KEY_ID = "hmac-sha256:a11oy-threshold-policy-dev"; const SEVERITY_WEIGHT: Record = { low: 0, medium: 0.35, high: 0.65, critical: 0.9, capital: 1, }; function assertUnit(name: string, value: number): void { if (!Number.isFinite(value) || value < 0 || value > 1) { throw new Error(`ThresholdPolicySeverityGate: ${name} must be in [0,1]; got ${value}`); } } function assertPositiveInteger(name: string, value: number): void { if (!Number.isInteger(value) || value < 1) { throw new Error(`ThresholdPolicySeverityGate: ${name} must be a positive integer; got ${value}`); } } function canonicalJson(value: unknown): string { if (value === null) return "null"; if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") { return JSON.stringify(value); } if (Array.isArray(value)) { return `[${value.map(canonicalJson).join(",")}]`; } if (typeof value === "object") { const record = value as Record; return `{${Object.keys(record).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(",")}}`; } throw new Error("ThresholdPolicySeverityGate: unsupported receipt payload value"); } function b64url(input: string): string { return Buffer.from(input, "utf8").toString("base64url"); } /** * Pre-Authentication Encoding for this gate's DSSE-shaped receipt. * * NOTE — this is NOT the canonical DSSE v1 PAE. The DSSE spec * (https://github.com/secure-systems-lab/dsse/blob/master/protocol.md) defines: * PAE = "DSSEv1" SP LEN(type) SP type SP LEN(body) SP body * where "DSSEv1" is a LITERAL prefix (not length-counted) and SP = 0x20. * This implementation instead treats "DSSEv1" as a length-counted piece, emitting * LEN("DSSEv1") SP "DSSEv1" SP LEN(type) SP type SP LEN(payload) SP payload * so receipts from this gate are NOT wire-compatible with an upstream DSSE * verifier or with the canonical encoding being unified in rae1 (a11oy#149). * Documented for the comment sweep; logic intentionally unchanged here. * See PhD Crypto Verdict 2026-05-30 Finding A1 (three incompatible PAE encodings). */ function pae(payloadType: string, payload: string): string { const pieces = ["DSSEv1", payloadType, payload]; return pieces.map((piece) => `${Buffer.byteLength(piece, "utf8")} ${piece}`).join(" "); } function signDsse(payload: Record, signingKey: string, keyid: string): DsseEnvelope { const payloadType = "application/vnd.szl.threshold-policy.v1+json" as const; const encodedPayload = b64url(canonicalJson(payload)); // DEMO ONLY — not non-repudiable: this is a symmetric HMAC-SHA-256 MAC tag // placed in the DSSE `signatures[].sig` field, not an asymmetric signature. // Verifiable only by holders of `signingKey`. See Finding A2 (crypto verdict). const sig = createHmac("sha256", signingKey) .update(pae(payloadType, encodedPayload), "utf8") .digest("base64url"); return { payloadType, payload: encodedPayload, signatures: [{ keyid, sig }], }; } function uniqueAttestedWitnesses(witnesses: readonly SeverityWitness[]): SeverityWitness[] { const seen = new Set(); const unique: SeverityWitness[] = []; for (const witness of witnesses) { if (typeof witness.id !== "string" || witness.id.trim() === "") { throw new Error("ThresholdPolicySeverityGate: witness id is required"); } if (typeof witness.role !== "string" || witness.role.trim() === "") { throw new Error(`ThresholdPolicySeverityGate: witness role is required for ${witness.id}`); } const id = witness.id.normalize("NFC"); if (witness.attested && !seen.has(id)) { seen.add(id); unique.push({ ...witness, id, role: witness.role.normalize("NFC") }); } } return unique; } export function thresholdPolicySeverityGate( config: ThresholdPolicySeverityGateConfig = {} ): (opts: ThresholdPolicySeverityGateOpts) => ThresholdPolicySeverityDecision { const baseThreshold = config.baseThreshold ?? DEFAULT_BASE_THRESHOLD; const severitySlope = config.severitySlope ?? DEFAULT_SEVERITY_SLOPE; const maxThreshold = config.maxThreshold ?? DEFAULT_MAX_THRESHOLD; const standardWitnesses = config.standardWitnesses ?? DEFAULT_STANDARD_WITNESSES; const capitalWitnesses = config.capitalWitnesses ?? DEFAULT_CAPITAL_WITNESSES; const signingKey = config.signingKey ?? DEFAULT_SIGNING_KEY; const keyId = config.keyId ?? DEFAULT_KEY_ID; assertUnit("baseThreshold", baseThreshold); assertUnit("severitySlope", severitySlope); assertUnit("maxThreshold", maxThreshold); assertPositiveInteger("standardWitnesses", standardWitnesses); assertPositiveInteger("capitalWitnesses", capitalWitnesses); if (maxThreshold < baseThreshold) { throw new Error("ThresholdPolicySeverityGate: maxThreshold must be >= baseThreshold"); } return function gate(opts: ThresholdPolicySeverityGateOpts): ThresholdPolicySeverityDecision { if (!opts.actionId || typeof opts.actionId !== "string") { throw new Error("ThresholdPolicySeverityGate: actionId is required"); } if (!(opts.severity in SEVERITY_WEIGHT)) { throw new Error(`ThresholdPolicySeverityGate: unknown severity ${String(opts.severity)}`); } assertUnit("confidence", opts.confidence); if (!Array.isArray(opts.witnesses)) { throw new Error("ThresholdPolicySeverityGate: witnesses must be an array"); } const decisionClass = opts.decisionClass ?? (opts.severity === "capital" ? "capital" : "ordinary"); if (!["ordinary", "property", "capital"].includes(decisionClass)) { throw new Error(`ThresholdPolicySeverityGate: unknown decisionClass ${String(decisionClass)}`); } const requiredThreshold = Math.min( maxThreshold, baseThreshold + severitySlope * SEVERITY_WEIGHT[opts.severity], ); const requiredWitnesses = decisionClass === "capital" || opts.severity === "capital" || opts.severity === "critical" ? capitalWitnesses : standardWitnesses; const attested = uniqueAttestedWitnesses(opts.witnesses); const witnessIds = attested.map((witness) => witness.id).sort(); const confidencePass = opts.confidence >= requiredThreshold; const witnessPass = attested.length >= requiredWitnesses; const allow = confidencePass && witnessPass; const lambdaScore = Math.min(opts.confidence / requiredThreshold, attested.length / requiredWitnesses, 1); const receiptPayload = { actionId: opts.actionId.normalize("NFC"), attestedWitnesses: attested.length, confidence: opts.confidence, decisionClass, formula: "ThresholdPolicySeverity", requiredThreshold, requiredWitnesses, severity: opts.severity, witnessIds, }; const receiptHash = createHash("sha256").update(canonicalJson(receiptPayload), "utf8").digest("hex"); const rationale = allow ? `ThresholdPolicySeverity action ${opts.actionId}: confidence ${opts.confidence.toFixed(3)} >= ` + `${requiredThreshold.toFixed(3)} and ${attested.length}/${requiredWitnesses} witnesses attested. ` + `DSSE receipt sha256=${receiptHash.slice(0, 16)}` : `ThresholdPolicySeverity action ${opts.actionId}: ` + `${confidencePass ? "confidence pass" : `confidence ${opts.confidence.toFixed(3)} < ${requiredThreshold.toFixed(3)}`}; ` + `${witnessPass ? "witness pass" : `${attested.length}/${requiredWitnesses} witnesses attested`} — deny.`; return { allow, rationale, formula: "ThresholdPolicySeverity", claimStatus: "verified-runtime", leanTheorem: null, leanFile: null, leanCommitSha: null, actionId: opts.actionId.normalize("NFC"), severity: opts.severity, decisionClass, confidence: opts.confidence, requiredThreshold, requiredWitnesses, attestedWitnesses: attested.length, witnessIds, lambdaScore, dsseReceipt: allow ? signDsse({ ...receiptPayload, receiptHash }, signingKey, keyId) : undefined, }; }; }