From 1ede9d47d3ce85c36d204172008f5e0b41297df8 Mon Sep 17 00:00:00 2001 From: Antonio Antenore Date: Mon, 20 Jul 2026 04:02:12 +0200 Subject: [PATCH 1/4] feat: compose authenticated passport verification --- src/core/authenticated-passport.ts | 193 ++++++++++++++++++++++++++++ src/index.ts | 1 + test/authenticated-passport.test.ts | 178 +++++++++++++++++++++++++ 3 files changed, 372 insertions(+) create mode 100644 src/core/authenticated-passport.ts create mode 100644 test/authenticated-passport.test.ts diff --git a/src/core/authenticated-passport.ts b/src/core/authenticated-passport.ts new file mode 100644 index 0000000..ee53e19 --- /dev/null +++ b/src/core/authenticated-passport.ts @@ -0,0 +1,193 @@ +import { createHash } from "node:crypto"; + +import type { + Digest, + ValidationIssue, + ValidationResult +} from "../contracts/types.js"; +import { canonicalize } from "./canonical.js"; +import { + verifyExecutionPassport, + type ExecutionPassportVerificationResult, + type VerifyExecutionPassportInput +} from "./passport.js"; + +export const EXECUTION_PASSPORT_DSSE_PAYLOAD_TYPE = + "application/vnd.in-toto+json" as const; + +export interface AuthenticatedSignerIdentity { + issuer: string; + subjectAlternativeName: string; +} + +export interface EnvelopeVerificationResult extends ValidationResult { + authenticatedPayloadDigest?: Digest; + signer?: AuthenticatedSignerIdentity; +} + +export interface EnvelopeVerifier { + readonly provider: string; + verify(input: { + envelope: unknown; + expectedPayload: Uint8Array; + expectedPayloadType: typeof EXECUTION_PASSPORT_DSSE_PAYLOAD_TYPE; + }): Promise; +} + +export type AuthenticationVerification = + | { + status: "not-evaluated"; + provider: string; + issues: []; + } + | { + status: "failed"; + provider: string; + issues: ValidationIssue[]; + } + | { + status: "verified"; + provider: string; + authenticatedPayloadDigest: Digest; + signer: AuthenticatedSignerIdentity; + issues: []; + }; + +export interface VerifyAuthenticatedExecutionPassportInput + extends VerifyExecutionPassportInput { + envelope: unknown; + verifier: EnvelopeVerifier; +} + +export interface AuthenticatedExecutionPassportVerificationResult + extends ValidationResult { + trustLevel: "authenticated" | "unverified"; + artifactSet: ExecutionPassportVerificationResult; + authentication: AuthenticationVerification; +} + +function digestBytes(value: Uint8Array): Digest { + return createHash("sha256").update(value).digest("hex"); +} + +function failedAuthentication( + provider: string, + issues: ValidationIssue[] +): AuthenticationVerification { + return { status: "failed", provider, issues }; +} + +/** + * Verify a complete Execution Passport artifact set and require an external, + * authenticated envelope over the exact canonical Statement bytes. The core + * owns composition and downgrade prevention; the injected verifier owns the + * trust system, signer policy, and signature verification. + */ +export async function verifyAuthenticatedExecutionPassport( + input: VerifyAuthenticatedExecutionPassportInput +): Promise { + const artifactSet = verifyExecutionPassport(input); + if (!artifactSet.valid) { + return { + valid: false, + issues: artifactSet.issues, + trustLevel: "unverified", + artifactSet, + authentication: { + status: "not-evaluated", + provider: input.verifier.provider, + issues: [] + } + }; + } + + const expectedPayload = new TextEncoder().encode(canonicalize(input.passport)); + const expectedPayloadDigest = digestBytes(expectedPayload); + + let verified: EnvelopeVerificationResult; + try { + verified = await input.verifier.verify({ + envelope: input.envelope, + expectedPayload, + expectedPayloadType: EXECUTION_PASSPORT_DSSE_PAYLOAD_TYPE + }); + } catch { + const issues: ValidationIssue[] = [ + { + path: "/authentication", + code: "envelope-verifier-error", + message: "The authenticated envelope verifier did not complete successfully." + } + ]; + return { + valid: false, + issues, + trustLevel: "unverified", + artifactSet, + authentication: failedAuthentication(input.verifier.provider, issues) + }; + } + + if (!verified.valid) { + return { + valid: false, + issues: verified.issues, + trustLevel: "unverified", + artifactSet, + authentication: failedAuthentication(input.verifier.provider, verified.issues) + }; + } + + if ( + verified.signer === undefined || + verified.authenticatedPayloadDigest === undefined + ) { + const issues: ValidationIssue[] = [ + { + path: "/authentication", + code: "envelope-verifier-result", + message: + "The authenticated envelope verifier did not return a complete signer and payload binding." + } + ]; + return { + valid: false, + issues, + trustLevel: "unverified", + artifactSet, + authentication: failedAuthentication(input.verifier.provider, issues) + }; + } + + if (verified.authenticatedPayloadDigest !== expectedPayloadDigest) { + const issues: ValidationIssue[] = [ + { + path: "/authentication/payload", + code: "envelope-payload-binding", + message: + "The authenticated envelope payload digest does not match the canonical Execution Passport." + } + ]; + return { + valid: false, + issues, + trustLevel: "unverified", + artifactSet, + authentication: failedAuthentication(input.verifier.provider, issues) + }; + } + + return { + valid: true, + issues: [], + trustLevel: "authenticated", + artifactSet, + authentication: { + status: "verified", + provider: input.verifier.provider, + authenticatedPayloadDigest: expectedPayloadDigest, + signer: verified.signer, + issues: [] + } + }; +} diff --git a/src/index.ts b/src/index.ts index a92b363..3c0dcbb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export * from "./conformance/config.js"; export * from "./conformance/engine.js"; export * from "./conformance/manifest.js"; export * from "./core/authority.js"; +export * from "./core/authenticated-passport.js"; export * from "./core/canonical.js"; export * from "./core/explain.js"; export * from "./core/fingerprint.js"; diff --git a/test/authenticated-passport.test.ts b/test/authenticated-passport.test.ts new file mode 100644 index 0000000..b262273 --- /dev/null +++ b/test/authenticated-passport.test.ts @@ -0,0 +1,178 @@ +import { createHash } from "node:crypto"; + +import { describe, expect, it, vi } from "vitest"; + +import { + canonicalize, + createExecutionPassport, + runConformance, + runDemo, + verifyAuthenticatedExecutionPassport +} from "../src/index.js"; +import type { + EnvelopeVerifier, + VerifyAuthenticatedExecutionPassportInput +} from "../src/index.js"; + +const GENERATED_AT = "2026-07-17T12:00:00.000Z"; +const ISSUED_AT = "2026-07-17T12:00:01.000Z"; + +function digestBytes(value: Uint8Array): string { + return createHash("sha256").update(value).digest("hex"); +} + +function fixture(): Omit { + const bundle = runDemo().bundle; + const report = runConformance(bundle, "enterprise", { generatedAt: GENERATED_AT }); + const oasfRecord = { externalRecord: "validated-by-owner" }; + const passport = createExecutionPassport({ + bundle, + report, + oasfRecord, + oasfMediaType: "application/json", + issuedAt: ISSUED_AT + }); + return { bundle, report, oasfRecord, passport, envelope: { signed: true } }; +} + +function acceptingVerifier(): { + verifier: EnvelopeVerifier; + verify: ReturnType>; + observedPayload: () => Uint8Array | undefined; +} { + let observedPayload: Uint8Array | undefined; + const verify = vi.fn( + ({ expectedPayload, expectedPayloadType }) => { + observedPayload = expectedPayload; + expect(expectedPayloadType).toBe("application/vnd.in-toto+json"); + return Promise.resolve({ + valid: true, + issues: [], + authenticatedPayloadDigest: digestBytes(expectedPayload), + signer: { + issuer: "https://issuer.example", + subjectAlternativeName: "https://identity.example/workload" + } + }); + } + ); + return { + verifier: { provider: "test-envelope", verify }, + verify, + observedPayload: () => observedPayload + }; +} + +describe("authenticated Execution Passport verification", () => { + it("authenticates the exact canonical Statement after artifact verification", async () => { + const input = fixture(); + const envelope = acceptingVerifier(); + const result = await verifyAuthenticatedExecutionPassport({ + ...input, + verifier: envelope.verifier + }); + + expect(result).toMatchObject({ + valid: true, + trustLevel: "authenticated", + authentication: { + status: "verified", + provider: "test-envelope", + signer: { + issuer: "https://issuer.example", + subjectAlternativeName: "https://identity.example/workload" + } + } + }); + expect(new TextDecoder().decode(envelope.observedPayload())).toBe( + canonicalize(input.passport) + ); + }); + + it("does not evaluate authentication for an inconsistent artifact set", async () => { + const input = fixture(); + const envelope = acceptingVerifier(); + const result = await verifyAuthenticatedExecutionPassport({ + ...input, + oasfRecord: { externalRecord: "replaced" }, + verifier: envelope.verifier + }); + + expect(result.valid).toBe(false); + expect(result.authentication.status).toBe("not-evaluated"); + expect(envelope.verify).not.toHaveBeenCalled(); + }); + + it("fails closed when the envelope is rejected or the verifier throws", async () => { + const input = fixture(); + const rejected: EnvelopeVerifier = { + provider: "test-envelope", + verify: () => Promise.resolve({ + valid: false, + issues: [ + { + path: "/authentication/signature", + code: "signature-invalid", + message: "The authenticated envelope signature is invalid." + } + ] + }) + }; + const rejectedResult = await verifyAuthenticatedExecutionPassport({ + ...input, + verifier: rejected + }); + expect(rejectedResult).toMatchObject({ + valid: false, + trustLevel: "unverified", + authentication: { status: "failed" } + }); + + const throwing: EnvelopeVerifier = { + provider: "test-envelope", + verify: () => Promise.reject(new Error("provider detail must not escape")) + }; + const throwingResult = await verifyAuthenticatedExecutionPassport({ + ...input, + verifier: throwing + }); + expect(throwingResult.issues).toEqual([ + { + path: "/authentication", + code: "envelope-verifier-error", + message: "The authenticated envelope verifier did not complete successfully." + } + ]); + }); + + it("rejects incomplete or mismatched successful verifier results", async () => { + const input = fixture(); + const incomplete: EnvelopeVerifier = { + provider: "test-envelope", + verify: () => Promise.resolve({ valid: true, issues: [] }) + }; + const incompleteResult = await verifyAuthenticatedExecutionPassport({ + ...input, + verifier: incomplete + }); + expect(incompleteResult.issues[0]?.code).toBe("envelope-verifier-result"); + + const mismatched: EnvelopeVerifier = { + provider: "test-envelope", + verify: () => Promise.resolve({ + valid: true, + issues: [], + authenticatedPayloadDigest: "0".repeat(64), + signer: { + issuer: "https://issuer.example", + subjectAlternativeName: "https://identity.example/workload" + } + }) + }; + const mismatchedResult = await verifyAuthenticatedExecutionPassport({ + ...input, + verifier: mismatched + }); + expect(mismatchedResult.issues[0]?.code).toBe("envelope-payload-binding"); + }); +}); From 7377b5641214c4034ab98511e2d4bcd3ab0a2790 Mon Sep 17 00:00:00 2001 From: Antonio Antenore Date: Mon, 20 Jul 2026 04:07:11 +0200 Subject: [PATCH 2/4] feat: add optional Sigstore trust adapter --- package-lock.json | 701 +++++++++++++++++++++++++- package.json | 20 +- schemas/v1/agentic-strata.schema.json | 2 +- scripts/package-smoke.mjs | 2 +- src/adapters/sigstore.ts | 297 +++++++++++ src/contracts/types.ts | 2 +- test/sigstore-adapter.test.ts | 322 ++++++++++++ 7 files changed, 1340 insertions(+), 6 deletions(-) create mode 100644 src/adapters/sigstore.ts create mode 100644 test/sigstore-adapter.test.ts diff --git a/package-lock.json b/package-lock.json index 33e5632..fae5f43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "agentic-strata", - "version": "0.3.0-alpha.1", + "version": "0.4.0-alpha.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "agentic-strata", - "version": "0.3.0-alpha.1", + "version": "0.4.0-alpha.1", "license": "Apache-2.0", "dependencies": { "ajv": "^8.17.1", @@ -21,17 +21,31 @@ "devDependencies": { "@arethetypeswrong/cli": "^0.18.5", "@eslint/js": "^9.31.0", + "@sigstore/bundle": "^5.0.0", "@types/node": "^22.17.0", "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.31.0", "globals": "^16.3.0", "publint": "^0.3.21", + "sigstore": "^5.0.0", "typescript": "^5.9.2", "typescript-eslint": "^8.38.0", "vitest": "^3.2.4" }, "engines": { "node": ">=22" + }, + "peerDependencies": { + "@sigstore/bundle": "^5.0.0", + "sigstore": "^5.0.0" + }, + "peerDependenciesMeta": { + "@sigstore/bundle": { + "optional": true + }, + "sigstore": { + "optional": true + } } }, "node_modules/@ampproject/remapping": { @@ -791,6 +805,16 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@gar/promise-retry": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gar/promise-retry/-/promise-retry-1.0.3.tgz", + "integrity": "sha512-GmzA9ckNokPypTg10pgpeHNQe7ph+iIKKmhKu3Ob9ANkswreCx7R3cKmY781K8QK3AqVL3xVh9A42JvIAbkkSA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "dev": true, @@ -912,6 +936,56 @@ "@braidai/lang": "^1.0.0" } }, + "node_modules/@npmcli/agent": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-5.0.2.tgz", + "integrity": "sha512-EkzGmEsgbQ1rqWkRJe2P0oQHx/ylZozDUNPMXCklLuSFL3GY+QyEfBUjhjCsgGXzh4OGpnHvkboSQgczjP/jJg==", + "dev": true, + "license": "ISC", + "dependencies": { + "agent-base": "^9.0.0", + "http-proxy-agent": "^9.0.0", + "https-proxy-agent": "^9.0.0", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^10.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@npmcli/fs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-6.0.0.tgz", + "integrity": "sha512-AheOs4swKka/XLtht6xxJDPezlQ7K2IYQ9Y8lST4JLDjnralnWuMM9AE2CdVcgQJ5omrXhsRzM7F7aYmeZBvKQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-5.0.0.tgz", + "integrity": "sha512-3zcN5Q3yEmeyxXBzqB6fXPQFzYa2ROsGFSr69W0ArXIAGJqxl/aFECOVPD2kbkYPm0U/EHxFKgclK3UA9WQg5A==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "dev": true, @@ -1334,6 +1408,86 @@ "win32" ] }, + "node_modules/@sigstore/bundle": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-5.0.0.tgz", + "integrity": "sha512-wefjygudENbzbQMks1t5u34EP0fFoD0XvaEP7DOUP/sXKvogzEJYFw5E6pegGyp3onGWzVEYKVa3bNZWyTYX+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@sigstore/core": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-4.0.1.tgz", + "integrity": "sha512-9v5hRjujn5NXq8o7XFEUgLyAtdr5Iisb4pzM05u3K61IS5q3hP3luWAndk0RkPPLTUFoTbg7Vb84UQ1ZQeajWQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.5.1.tgz", + "integrity": "sha512-/ScWUhhoFasJsSRGTVBwId1loQjjnjAfE4djL6ZhrXRpNCmPTnUKF5Jokd58ILseOMjzET3UrMOtJPS9sYeI0g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-5.0.0.tgz", + "integrity": "sha512-DSFivqz9/i5AkwZ5fq0YdjaJlc4o1WeS2Zffon0kqtChx0vy4W9NOjkEet9bF2vkzOufX72eVH8kZBIGtcBp1w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^5.0.0", + "@sigstore/core": "^4.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^16.0.0", + "proc-log": "^7.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-5.0.0.tgz", + "integrity": "sha512-Zyqg9tcHps3uRAlKHLNmsW4ohsUZAjb9G+31r7lg0ICh/JOcadzmJsIRdjKljlRHpaR0K4aJ2kXXIdywdcdMlA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^6.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-4.1.0.tgz", + "integrity": "sha512-p/s720RiWxLG8XtmfdPfEJOlATA6H/2knFqmtQbFkHKN3IrhWGUwPfpQAf1UnQIEES9IaH6zzhfjkrhTfeSdZw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^5.0.0", + "@sigstore/core": "^4.0.1", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/@sindresorhus/is": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", @@ -1347,6 +1501,69 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-5.0.0.tgz", + "integrity": "sha512-U4mVcdFGOi6pt8n38LdWZp67Svn7ppnU1Pj8SGOVaBi1X4gm+G4ztQlLfkoJbKSHfjA6WeaiJp2A4V83AJF6nQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.2.1" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@types/chai": { "version": "5.2.3", "dev": true, @@ -1783,6 +2000,16 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", + "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, "node_modules/ajv": { "version": "8.20.0", "license": "MIT", @@ -1905,6 +2132,112 @@ "node": ">=8" } }, + "node_modules/cacache": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-21.0.1.tgz", + "integrity": "sha512-pTwz/uj3Jyp6WXdJ6fWhR+7LVxVs6RyroQSn7KJwHsSxXuyGSp0pcMVcwSwTpCFq1X2YG8QBe0W+vN+cr0SwzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^6.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^14.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/cacache/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/callsites": { "version": "3.1.0", "dev": true, @@ -2587,6 +2920,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/fsevents": { "version": "2.3.3", "dev": true, @@ -2695,6 +3041,61 @@ "dev": true, "license": "MIT" }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", + "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/https-proxy-agent": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", + "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "proxy-agent-negotiate": "1.1.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/ignore": { "version": "5.3.2", "dev": true, @@ -2726,6 +3127,16 @@ "node": ">=0.8.19" } }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "dev": true, @@ -2939,6 +3350,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-fetch-happen": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-16.0.1.tgz", + "integrity": "sha512-uUv1yxHzaKVVEPfcFeGSNov/Cehjv08ovlY8ImTljgL7Q+SiA0dAYLQ6SYVa2kkKqNj4Y3aZEI7xv2teadie0A==", + "dev": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^5.0.0", + "@npmcli/redact": "^5.0.0", + "cacache": "^21.0.0", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^6.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^7.0.0", + "ssri": "^14.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/marked": { "version": "9.1.6", "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", @@ -3006,6 +3441,115 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-6.0.0.tgz", + "integrity": "sha512-AWI8bKapGmgx/J0E6IGYSKj8TiHebZkmKWSs8raPSw8KXwgEAJ+Bw3+LSdXHR6T/RHKAWCOYk2MiLrYluaUU6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.7.tgz", + "integrity": "sha512-TbqTz9cUwWyHS2Dy89P3ocAGUGxKjjLuR9z8w4WUTGAVgEj17/4nhgo2Du56i0Fm3Pm30g4iA8Lcqctc76jCzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-2.0.0.tgz", + "integrity": "sha512-zSsHhto5BcUVM2m1LurnXY6M//cGhVaegT71OfOXoprxT6o780GZd792ea6FfrQkuU4usHZIUczAQMRUE2plzA==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -3055,6 +3599,16 @@ "dev": true, "license": "MIT" }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/node-emoji": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", @@ -3125,6 +3679,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-map": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.5.tgz", + "integrity": "sha512-e8vJF4XdVkzqqSHguEMz41mQO1wKwxKm5ENrUJQUu9kLDCtn83cxbyHZcszr4QC5zEA7WffRRC4gsTecC7J9oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "dev": true, @@ -3267,6 +3834,34 @@ "node": ">= 0.8.0" } }, + "node_modules/proc-log": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-7.0.0.tgz", + "integrity": "sha512-FYgfaA69XZ93zaXLoMNQ+ViDXGGBgR8aLh03txzcFhV+9xOXx7+8DLCULrKKpR9+GsH9ZfHm82aSUPpozX0Ztg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/proxy-agent-negotiate": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", + "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "kerberos": "^2.0.0" + }, + "peerDependenciesMeta": { + "kerberos": { + "optional": true + } + } + }, "node_modules/publint": { "version": "0.3.21", "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.21.tgz", @@ -3378,6 +3973,14 @@ "node": ">=6" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/semver": { "version": "7.8.5", "dev": true, @@ -3424,6 +4027,24 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/sigstore": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-5.0.0.tgz", + "integrity": "sha512-hJqJfoG/e4qFQaauQL00c6J6FrHLBGKtkFvW3JbTSIEFOhLrSjdSM/gWd/yUOfYo/gsERehTXGC1VZWX+9X4Dg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^5.0.0", + "@sigstore/core": "^4.0.0", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^5.0.0", + "@sigstore/tuf": "^5.0.0", + "@sigstore/verify": "^4.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/skin-tone": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", @@ -3437,6 +4058,47 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.9.tgz", + "integrity": "sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz", + "integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "9.0.0", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "dev": true, @@ -3445,6 +4107,19 @@ "node": ">=0.10.0" } }, + "node_modules/ssri": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-14.0.0.tgz", + "integrity": "sha512-jQxKI0yx0ZnTKrqjKkLDV2DXkBQn3k49JVmVqDGcDwKDtGDbImD/GXsq04KD0VVzCQQ9wZJYal3RwR1GzWTSow==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/stackback": { "version": "0.0.2", "dev": true, @@ -3727,6 +4402,21 @@ "typescript": ">=4.8.4" } }, + "node_modules/tuf-js": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-6.0.0.tgz", + "integrity": "sha512-zlJVOIO68hmgo1//X4ENEcTGfuOTAtDPi8PsTsG+FyxD85E/ww1ZnwBbWo/yCEExGpI+Kilg7Z3qCdHX2BoJTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@gar/promise-retry": "^1.0.3", + "@tufjs/models": "5.0.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, "node_modules/type-check": { "version": "0.4.0", "dev": true, @@ -4098,6 +4788,13 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/yaml": { "version": "2.9.0", "license": "ISC", diff --git a/package.json b/package.json index a1d600c..1c890b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agentic-strata", - "version": "0.3.0-alpha.1", + "version": "0.4.0-alpha.1", "description": "Executable, contract-first reference architecture for enterprise agentic applications.", "type": "module", "license": "Apache-2.0", @@ -38,6 +38,10 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./sigstore": { + "types": "./dist/adapters/sigstore.d.ts", + "import": "./dist/adapters/sigstore.js" } }, "files": [ @@ -73,14 +77,28 @@ "commander": "^14.0.0", "yaml": "^2.8.1" }, + "peerDependencies": { + "@sigstore/bundle": "^5.0.0", + "sigstore": "^5.0.0" + }, + "peerDependenciesMeta": { + "@sigstore/bundle": { + "optional": true + }, + "sigstore": { + "optional": true + } + }, "devDependencies": { "@arethetypeswrong/cli": "^0.18.5", + "@sigstore/bundle": "^5.0.0", "@eslint/js": "^9.31.0", "@types/node": "^22.17.0", "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.31.0", "globals": "^16.3.0", "publint": "^0.3.21", + "sigstore": "^5.0.0", "typescript": "^5.9.2", "typescript-eslint": "^8.38.0", "vitest": "^3.2.4" diff --git a/schemas/v1/agentic-strata.schema.json b/schemas/v1/agentic-strata.schema.json index 58c43c0..51e84b9 100644 --- a/schemas/v1/agentic-strata.schema.json +++ b/schemas/v1/agentic-strata.schema.json @@ -1069,7 +1069,7 @@ "additionalProperties": false, "properties": { "name": { "type": "string", "const": "agentic-strata" }, - "version": { "type": "string", "const": "0.3.0-alpha.1" }, + "version": { "type": "string", "const": "0.4.0-alpha.1" }, "revision": { "type": "string", "const": "conformance-2026-07-19.1" diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index daa5ffa..f861218 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -57,7 +57,7 @@ try { process.platform === "win32" ? "agentic-strata.cmd" : "agentic-strata" ); const version = run(executable, ["--version"], temporary).trim(); - if (version !== "0.3.0-alpha.1") { + if (version !== "0.4.0-alpha.1") { throw new Error(`Installed CLI reported unexpected version: ${version}`); } const cliOutput = join(temporary, "cli-demo"); diff --git a/src/adapters/sigstore.ts b/src/adapters/sigstore.ts new file mode 100644 index 0000000..1391b2f --- /dev/null +++ b/src/adapters/sigstore.ts @@ -0,0 +1,297 @@ +import { createHash } from "node:crypto"; + +import { + bundleFromJSON, + bundleToJSON, + isBundleWithDsseEnvelope +} from "@sigstore/bundle"; +import { createVerifier } from "sigstore"; +import type { BundleVerifier, VerifyOptions } from "sigstore"; + +import type { ValidationIssue } from "../contracts/types.js"; +import { + EXECUTION_PASSPORT_DSSE_PAYLOAD_TYPE, + type EnvelopeVerificationResult, + type EnvelopeVerifier +} from "../core/authenticated-passport.js"; + +export interface SigstoreAllowedSigner { + issuer: string; + subjectAlternativeName: string; +} + +export interface SigstoreIdentityPolicy { + allowedSigners: readonly SigstoreAllowedSigner[]; +} + +export interface SigstoreVerificationThresholds { + ctLog: number; + tlog: number; +} + +export const DEFAULT_SIGSTORE_VERIFICATION_THRESHOLDS = { + ctLog: 1, + tlog: 1 +} as const satisfies SigstoreVerificationThresholds; + +export interface CreateSigstoreEnvelopeVerifierInput { + bundleVerifier: BundleVerifier; + identityPolicy: SigstoreIdentityPolicy; + /** + * Thresholds already enforced by the injected BundleVerifier. Values below + * one are rejected so authenticated-required mode cannot disable Sigstore's + * certificate or transparency verification by configuration. + */ + thresholds?: SigstoreVerificationThresholds; +} + +export type PublicSigstoreTufOptions = Pick< + VerifyOptions, + | "tufMirrorURL" + | "tufRootPath" + | "tufCachePath" + | "tufForceCache" + | "timeout" +>; + +export interface CreatePublicSigstoreEnvelopeVerifierInput { + identityPolicy: SigstoreIdentityPolicy; + thresholds?: SigstoreVerificationThresholds; + tuf?: PublicSigstoreTufOptions; +} + +export interface SigstoreEnvelopeVerifier extends EnvelopeVerifier { + readonly thresholds: Readonly; +} + +const MAX_ALLOWED_SIGNERS = 32; +const MAX_IDENTITY_LENGTH = 2048; + +function issue(path: string, code: string, message: string): ValidationIssue { + return { path, code, message }; +} + +function failure(path: string, code: string, message: string): EnvelopeVerificationResult { + return { valid: false, issues: [issue(path, code, message)] }; +} + +function requireExactIdentityValue(label: string, value: string): void { + if ( + value.length === 0 || + value.length > MAX_IDENTITY_LENGTH || + value.trim() !== value + ) { + throw new TypeError( + `Sigstore ${label} must be a non-empty exact value without surrounding whitespace.` + ); + } +} + +function copyIdentityPolicy(policy: SigstoreIdentityPolicy): SigstoreIdentityPolicy { + if ( + policy.allowedSigners.length === 0 || + policy.allowedSigners.length > MAX_ALLOWED_SIGNERS + ) { + throw new TypeError( + `Sigstore identity policy must contain between 1 and ${MAX_ALLOWED_SIGNERS} exact allowed signers.` + ); + } + + const seen = new Set(); + const allowedSigners = policy.allowedSigners.map((signer) => { + requireExactIdentityValue("issuer", signer.issuer); + requireExactIdentityValue( + "subject alternative name", + signer.subjectAlternativeName + ); + const key = `${signer.issuer}\u0000${signer.subjectAlternativeName}`; + if (seen.has(key)) { + throw new TypeError( + "Sigstore identity policy must not contain duplicate allowed signers." + ); + } + seen.add(key); + return { + issuer: signer.issuer, + subjectAlternativeName: signer.subjectAlternativeName + }; + }); + + return { allowedSigners }; +} + +function copyThresholds( + thresholds: SigstoreVerificationThresholds | undefined +): SigstoreVerificationThresholds { + const effective = thresholds ?? DEFAULT_SIGSTORE_VERIFICATION_THRESHOLDS; + for (const [name, value] of Object.entries(effective)) { + if (!Number.isSafeInteger(value) || value < 1) { + throw new TypeError(`Sigstore ${name} threshold must be a positive integer.`); + } + } + return { ctLog: effective.ctLog, tlog: effective.tlog }; +} + +function rawDsseSignatureCount(value: unknown): number | undefined { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return undefined; + } + const envelope = (value as Record).dsseEnvelope; + if (envelope === null || typeof envelope !== "object" || Array.isArray(envelope)) { + return undefined; + } + const signatures = (envelope as Record).signatures; + return Array.isArray(signatures) ? signatures.length : undefined; +} + +function bytesEqual(left: Uint8Array, right: Uint8Array): boolean { + return Buffer.from(left).equals(Buffer.from(right)); +} + +function digestBytes(value: Uint8Array): string { + return createHash("sha256").update(value).digest("hex"); +} + +function verifySigstoreEnvelope( + bundleVerifier: BundleVerifier, + identityPolicy: SigstoreIdentityPolicy, + input: Parameters[0] +): EnvelopeVerificationResult { + const rawSignatureCount = rawDsseSignatureCount(input.envelope); + if (rawSignatureCount !== undefined && rawSignatureCount !== 1) { + return failure( + "/authentication/envelope/dsseEnvelope/signatures", + "sigstore-signature-count", + "The Sigstore DSSE envelope must contain exactly one signature." + ); + } + + let bundle; + try { + bundle = bundleFromJSON(input.envelope); + } catch { + return failure( + "/authentication/envelope", + "sigstore-bundle-invalid", + "The supplied authenticated envelope is not a valid Sigstore bundle." + ); + } + + if (!isBundleWithDsseEnvelope(bundle)) { + return failure( + "/authentication/envelope", + "sigstore-dsse-required", + "The Sigstore bundle must contain a DSSE envelope." + ); + } + + const dsse = bundle.content.dsseEnvelope; + if (dsse.signatures.length !== 1) { + return failure( + "/authentication/envelope/dsseEnvelope/signatures", + "sigstore-signature-count", + "The Sigstore DSSE envelope must contain exactly one signature." + ); + } + if ( + input.expectedPayloadType !== EXECUTION_PASSPORT_DSSE_PAYLOAD_TYPE || + dsse.payloadType !== input.expectedPayloadType + ) { + return failure( + "/authentication/envelope/dsseEnvelope/payloadType", + "sigstore-payload-type", + "The Sigstore DSSE payload type must be application/vnd.in-toto+json." + ); + } + if (!bytesEqual(dsse.payload, input.expectedPayload)) { + return failure( + "/authentication/envelope/dsseEnvelope/payload", + "sigstore-payload-binding", + "The Sigstore DSSE payload must exactly match the canonical Execution Passport bytes." + ); + } + + let signer; + try { + signer = bundleVerifier.verify(bundleToJSON(bundle)); + } catch { + return failure( + "/authentication/envelope/dsseEnvelope/signatures/0", + "sigstore-verification-failed", + "The Sigstore bundle did not pass cryptographic and trust verification." + ); + } + + const issuer = signer.identity?.extensions?.issuer; + const subjectAlternativeName = signer.identity?.subjectAlternativeName; + if (issuer === undefined || subjectAlternativeName === undefined) { + return failure( + "/authentication/signer", + "sigstore-identity-missing", + "The verified Sigstore signer does not contain both issuer and subject alternative name." + ); + } + + const allowed = identityPolicy.allowedSigners.some( + (candidate) => + candidate.issuer === issuer && + candidate.subjectAlternativeName === subjectAlternativeName + ); + if (!allowed) { + return failure( + "/authentication/signer", + "sigstore-identity-denied", + "The verified Sigstore signer is not allowed by the exact identity policy." + ); + } + + return { + valid: true, + issues: [], + authenticatedPayloadDigest: digestBytes(dsse.payload), + signer: { issuer, subjectAlternativeName } + }; +} + +/** + * Wrap an enterprise, offline, or test BundleVerifier. The caller configures + * its trust material and must ensure it enforces the declared thresholds. + */ +export function createSigstoreEnvelopeVerifier( + input: CreateSigstoreEnvelopeVerifierInput +): SigstoreEnvelopeVerifier { + const identityPolicy = copyIdentityPolicy(input.identityPolicy); + const thresholds = copyThresholds(input.thresholds); + return { + provider: "sigstore", + thresholds, + verify: (verificationInput) => + Promise.resolve( + verifySigstoreEnvelope( + input.bundleVerifier, + identityPolicy, + verificationInput + ) + ) + }; +} + +/** + * Create a verifier backed by the Sigstore public client and its TUF trust + * configuration. Exact signer authorization remains AgenticStrata-owned. + */ +export async function createPublicSigstoreEnvelopeVerifier( + input: CreatePublicSigstoreEnvelopeVerifierInput +): Promise { + const thresholds = copyThresholds(input.thresholds); + const bundleVerifier = await createVerifier({ + ...(input.tuf ?? {}), + ctLogThreshold: thresholds.ctLog, + tlogThreshold: thresholds.tlog + }); + return createSigstoreEnvelopeVerifier({ + bundleVerifier, + identityPolicy: input.identityPolicy, + thresholds + }); +} diff --git a/src/contracts/types.ts b/src/contracts/types.ts index a297bb3..1527a47 100644 --- a/src/contracts/types.ts +++ b/src/contracts/types.ts @@ -1,6 +1,6 @@ export const API_VERSION = "agenticstrata.dev/v1" as const; export const EVALUATOR_NAME = "agentic-strata" as const; -export const EVALUATOR_VERSION = "0.3.0-alpha.1" as const; +export const EVALUATOR_VERSION = "0.4.0-alpha.1" as const; export const EVALUATOR_REVISION = "conformance-2026-07-19.1" as const; export const IN_TOTO_STATEMENT_V1_TYPE = "https://in-toto.io/Statement/v1" as const; diff --git a/test/sigstore-adapter.test.ts b/test/sigstore-adapter.test.ts new file mode 100644 index 0000000..e806d96 --- /dev/null +++ b/test/sigstore-adapter.test.ts @@ -0,0 +1,322 @@ +import { + createHash, + generateKeyPairSync, + sign, + verify as verifySignature +} from "node:crypto"; + +import type { SerializedBundle } from "@sigstore/bundle"; +import { describe, expect, it } from "vitest"; +import type { BundleVerifier } from "sigstore"; + +import { + createSigstoreEnvelopeVerifier, + type SigstoreIdentityPolicy +} from "../src/adapters/sigstore.js"; +import { + canonicalize, + createExecutionPassport, + runConformance, + runDemo, + verifyAuthenticatedExecutionPassport +} from "../src/index.js"; + +const PAYLOAD_TYPE = "application/vnd.in-toto+json"; +const ISSUER = "https://issuer.example"; +const SAN = "https://identity.example/workload"; +const KEY_HINT = "agentic-strata-test-ed25519"; +const GENERATED_AT = "2026-07-17T12:00:00.000Z"; +const ISSUED_AT = "2026-07-17T12:00:01.000Z"; + +function pae(payloadType: string, payload: Uint8Array): Buffer { + const type = Buffer.from(payloadType, "utf8"); + const body = Buffer.from(payload); + return Buffer.concat([ + Buffer.from(`DSSEv1 ${type.length} `, "utf8"), + type, + Buffer.from(` ${body.length} `, "utf8"), + body + ]); +} + +function dsseBundle( + payload: Uint8Array, + privateKey: ReturnType["privateKey"], + payloadType = PAYLOAD_TYPE +): SerializedBundle { + const signature = sign(null, pae(payloadType, payload), privateKey); + return { + mediaType: "application/vnd.dev.sigstore.bundle.v0.3+json", + verificationMaterial: { + x509CertificateChain: undefined, + publicKey: { hint: KEY_HINT }, + certificate: undefined, + tlogEntries: [], + timestampVerificationData: { rfc3161Timestamps: [] } + }, + dsseEnvelope: { + payload: Buffer.from(payload).toString("base64"), + payloadType, + signatures: [{ keyid: KEY_HINT, sig: signature.toString("base64") }] + }, + messageSignature: undefined + }; +} + +function ed25519BundleVerifier( + publicKey: ReturnType["publicKey"], + identity: { issuer?: string; subjectAlternativeName?: string } = { + issuer: ISSUER, + subjectAlternativeName: SAN + } +): BundleVerifier { + return { + verify: (bundle) => { + const envelope = bundle.dsseEnvelope; + const signature = envelope?.signatures[0]; + if ( + envelope === undefined || + signature === undefined || + !verifySignature( + null, + pae(envelope.payloadType, Buffer.from(envelope.payload, "base64")), + publicKey, + Buffer.from(signature.sig, "base64") + ) + ) { + throw new Error("invalid test signature"); + } + return { + key: publicKey, + identity: { + ...(identity.subjectAlternativeName === undefined + ? {} + : { subjectAlternativeName: identity.subjectAlternativeName }), + ...(identity.issuer === undefined + ? {} + : { extensions: { issuer: identity.issuer } }) + } + }; + } + }; +} + +function exactPolicy( + issuer = ISSUER, + subjectAlternativeName = SAN +): SigstoreIdentityPolicy { + return { allowedSigners: [{ issuer, subjectAlternativeName }] }; +} + +function passportFixture() { + const bundle = runDemo().bundle; + const report = runConformance(bundle, "enterprise", { generatedAt: GENERATED_AT }); + const oasfRecord = { externalRecord: "validated-by-owner" }; + const passport = createExecutionPassport({ + bundle, + report, + oasfRecord, + oasfMediaType: "application/json", + issuedAt: ISSUED_AT + }); + const payload = Buffer.from(canonicalize(passport), "utf8"); + return { bundle, report, oasfRecord, passport, payload }; +} + +function flipSignature(bundle: SerializedBundle): SerializedBundle { + const changed = structuredClone(bundle); + const signature = changed.dsseEnvelope?.signatures[0]; + if (signature === undefined) { + throw new Error("test fixture is not DSSE"); + } + const bytes = Buffer.from(signature.sig, "base64"); + bytes[0] = (bytes[0] ?? 0) ^ 1; + signature.sig = bytes.toString("base64"); + return changed; +} + +describe("optional Sigstore adapter", () => { + it("verifies an Ed25519 DSSE envelope and exact signer policy without network", async () => { + const fixture = passportFixture(); + const keys = generateKeyPairSync("ed25519"); + const envelope = dsseBundle(fixture.payload, keys.privateKey); + const verifier = createSigstoreEnvelopeVerifier({ + bundleVerifier: ed25519BundleVerifier(keys.publicKey), + identityPolicy: exactPolicy(), + thresholds: { ctLog: 1, tlog: 1 } + }); + + const result = await verifyAuthenticatedExecutionPassport({ + ...fixture, + envelope, + verifier + }); + + expect(result).toMatchObject({ + valid: true, + trustLevel: "authenticated", + authentication: { + status: "verified", + provider: "sigstore", + signer: { issuer: ISSUER, subjectAlternativeName: SAN } + } + }); + }); + + it("rejects different payload bytes and payload types before trust verification", async () => { + const fixture = passportFixture(); + const keys = generateKeyPairSync("ed25519"); + const verifier = createSigstoreEnvelopeVerifier({ + bundleVerifier: ed25519BundleVerifier(keys.publicKey), + identityPolicy: exactPolicy() + }); + + const changedPayload = await verifier.verify({ + envelope: dsseBundle(Buffer.from("different", "utf8"), keys.privateKey), + expectedPayload: fixture.payload, + expectedPayloadType: PAYLOAD_TYPE + }); + expect(changedPayload.issues[0]?.code).toBe("sigstore-payload-binding"); + + const changedType = await verifier.verify({ + envelope: dsseBundle(fixture.payload, keys.privateKey, "application/json"), + expectedPayload: fixture.payload, + expectedPayloadType: PAYLOAD_TYPE + }); + expect(changedType.issues[0]?.code).toBe("sigstore-payload-type"); + }); + + it("rejects signature tampering and any signature count other than one", async () => { + const fixture = passportFixture(); + const keys = generateKeyPairSync("ed25519"); + const valid = dsseBundle(fixture.payload, keys.privateKey); + const verifier = createSigstoreEnvelopeVerifier({ + bundleVerifier: ed25519BundleVerifier(keys.publicKey), + identityPolicy: exactPolicy() + }); + + const tampered = await verifier.verify({ + envelope: flipSignature(valid), + expectedPayload: fixture.payload, + expectedPayloadType: PAYLOAD_TYPE + }); + expect(tampered.issues[0]?.code).toBe("sigstore-verification-failed"); + + for (const signatures of [ + [], + [ + ...(valid.dsseEnvelope?.signatures ?? []), + ...(valid.dsseEnvelope?.signatures ?? []) + ] + ]) { + const changed = structuredClone(valid); + if (changed.dsseEnvelope === undefined) { + throw new Error("test fixture is not DSSE"); + } + changed.dsseEnvelope.signatures = signatures; + const result = await verifier.verify({ + envelope: changed, + expectedPayload: fixture.payload, + expectedPayloadType: PAYLOAD_TYPE + }); + expect(result.issues[0]?.code).toBe("sigstore-signature-count"); + } + }); + + it("requires exact issuer and SAN values and complete verified identity", async () => { + const fixture = passportFixture(); + const keys = generateKeyPairSync("ed25519"); + const envelope = dsseBundle(fixture.payload, keys.privateKey); + const literalWildcard = createSigstoreEnvelopeVerifier({ + bundleVerifier: ed25519BundleVerifier(keys.publicKey), + identityPolicy: exactPolicy(ISSUER, "https://identity.example/*") + }); + const denied = await literalWildcard.verify({ + envelope, + expectedPayload: fixture.payload, + expectedPayloadType: PAYLOAD_TYPE + }); + expect(denied.issues[0]?.code).toBe("sigstore-identity-denied"); + + const missing = createSigstoreEnvelopeVerifier({ + bundleVerifier: ed25519BundleVerifier(keys.publicKey, { + subjectAlternativeName: SAN + }), + identityPolicy: exactPolicy() + }); + const missingResult = await missing.verify({ + envelope, + expectedPayload: fixture.payload, + expectedPayloadType: PAYLOAD_TYPE + }); + expect(missingResult.issues[0]?.code).toBe("sigstore-identity-missing"); + }); + + it("rejects disabled trust thresholds and a bare-Statement downgrade", async () => { + const fixture = passportFixture(); + const keys = generateKeyPairSync("ed25519"); + expect(() => + createSigstoreEnvelopeVerifier({ + bundleVerifier: ed25519BundleVerifier(keys.publicKey), + identityPolicy: exactPolicy(), + thresholds: { ctLog: 0, tlog: 1 } + }) + ).toThrow("Sigstore ctLog threshold must be a positive integer."); + + const verifier = createSigstoreEnvelopeVerifier({ + bundleVerifier: ed25519BundleVerifier(keys.publicKey), + identityPolicy: exactPolicy() + }); + const result = await verifyAuthenticatedExecutionPassport({ + ...fixture, + envelope: fixture.passport, + verifier + }); + expect(result).toMatchObject({ + valid: false, + trustLevel: "unverified", + authentication: { status: "failed" }, + issues: [{ code: "sigstore-bundle-invalid" }] + }); + }); + + it("rejects empty and duplicate exact signer policies", () => { + const keys = generateKeyPairSync("ed25519"); + const bundleVerifier = ed25519BundleVerifier(keys.publicKey); + const allowedSigner = exactPolicy().allowedSigners[0]; + if (allowedSigner === undefined) { + throw new Error("test policy has no signer"); + } + expect(() => + createSigstoreEnvelopeVerifier({ + bundleVerifier, + identityPolicy: { allowedSigners: [] } + }) + ).toThrow("Sigstore identity policy must contain between 1 and 32 exact allowed signers."); + expect(() => + createSigstoreEnvelopeVerifier({ + bundleVerifier, + identityPolicy: { + allowedSigners: [allowedSigner, allowedSigner] + } + }) + ).toThrow("Sigstore identity policy must not contain duplicate allowed signers."); + }); + + it("uses SHA-256 payload digests compatible with the provider-neutral core", async () => { + const fixture = passportFixture(); + const keys = generateKeyPairSync("ed25519"); + const verifier = createSigstoreEnvelopeVerifier({ + bundleVerifier: ed25519BundleVerifier(keys.publicKey), + identityPolicy: exactPolicy() + }); + const result = await verifier.verify({ + envelope: dsseBundle(fixture.payload, keys.privateKey), + expectedPayload: fixture.payload, + expectedPayloadType: PAYLOAD_TYPE + }); + expect(result.authenticatedPayloadDigest).toBe( + createHash("sha256").update(fixture.payload).digest("hex") + ); + }); +}); From 5d64288d757391b8d0829c3c44023de643d15054 Mon Sep 17 00:00:00 2001 From: Antonio Antenore Date: Mon, 20 Jul 2026 04:10:27 +0200 Subject: [PATCH 3/4] docs: define authenticated passport trust boundary --- README.md | 53 ++++++++++++++++++- ...006-authenticated-passport-verification.md | 53 +++++++++++++++++++ docs/architecture.md | 11 +++- docs/contracts-and-conformance.md | 22 ++++++++ docs/interoperability.md | 8 ++- .../execution-passport/v2/README.md | 9 ++-- docs/threat-model.md | 6 ++- scripts/package-smoke.mjs | 32 ++++++++++- 8 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 docs/adr/0006-authenticated-passport-verification.md diff --git a/README.md b/README.md index 12b7fbc..4fc446e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ | Prepare, commit, verify, and compensate lifecycle | Side effects become explicit, approval-aware, and checkable instead of hidden inside an agent loop. | | Configurable conformance profiles | The same application can apply stricter evidence rules to enterprise, regulated, local, or distributed runs. | | Plain-language explanation and Execution Passport | Technical run evidence can be shared with non-specialists and bound to external attestations without embedding the full payload. | +| Optional authenticated Passport verification | A relying party can require an exact trusted signer and reject a removed, altered, or substituted DSSE envelope without coupling the core to one trust provider. | > **Maturity and limits:** this is an executable alpha reference architecture, > not a hosted agent platform or regulatory certification. Digital fingerprints @@ -76,6 +77,7 @@ Logical strata may share one process. The rule is a dependency and responsibilit - An RFC 8785 JSON Canonicalization Scheme implementation with cross-language golden vectors, strict duplicate-member rejection at the JSON boundary, and a hash-linked receipt chain with tamper and missing-evidence detection. - Runtime conformance checks for rooted authority, causal evidence, actual budget consumption, exact-action idempotency, approval, read-back or compensation, predeclared criterion evidence bindings, layer direction, and model/authority separation. - An [Execution Passport v2](docs/spec/attestations/execution-passport/v2/README.md) that binds one run bundle, its complete conformance report, one externally validated OASF record, and optional run-bound observations in a strict in-toto Statement without embedding those documents. +- Provider-neutral authenticated consumer composition plus an optional `agentic-strata/sigstore` adapter that verifies DSSE structure, exact canonical Passport bytes, cryptographic trust, and an exact issuer-plus-SAN allowlist. - Mapping-only adapter documents for AG-UI, A2A, MCP, OpenTelemetry GenAI, CloudEvents, an external policy engine, and OASF, plus an executable StageFabric evidence-reduction adapter. - A neutral prepare/commit/read-back demo with exact-action approval and duplicate suppression. @@ -139,6 +141,54 @@ semantics. Repeat `--execution-evidence` and `bind-stagefabric` accepts only the exact canonical JSON plus trailing LF emitted by the StageFabric evidence writer. Pretty-printed JSON, YAML, or a reserialized object is rejected because the resulting descriptor digest identifies the retrievable file bytes, not merely an equivalent object. +### Optional authenticated verification + +The root package has no runtime dependency on Sigstore. Install the optional +peers only in a relying party that needs Sigstore verification: + +```bash +npm install agentic-strata sigstore@5 @sigstore/bundle@5 +``` + +```js +import { verifyAuthenticatedExecutionPassport } from "agentic-strata"; +import { + createPublicSigstoreEnvelopeVerifier +} from "agentic-strata/sigstore"; + +const verifier = await createPublicSigstoreEnvelopeVerifier({ + identityPolicy: { + allowedSigners: [{ + issuer: "https://token.actions.githubusercontent.com", + subjectAlternativeName: "https://github.com/example/project/.github/workflows/release.yml@refs/heads/main" + }] + }, + thresholds: { ctLog: 1, tlog: 1 } +}); + +const result = await verifyAuthenticatedExecutionPassport({ + passport, + bundle, + report, + oasfRecord, + executionEvidenceResources, + envelope: sigstoreBundle, + verifier +}); +``` + +The identity values are literal exact matches, never regular expressions or +wildcards. An enterprise or offline deployment can instead pass its own +threshold-configured Sigstore `BundleVerifier` to +`createSigstoreEnvelopeVerifier`. A bare Statement, non-DSSE bundle, second +signature, different payload type, non-canonical payload bytes, failed trust +verification, or different signer fails closed. The adapter verifies only; it +does not obtain OIDC tokens or sign Passports. + +Sigstore 5 currently requires Node.js `^22.22.2 || ^24.15.0 || >=26.0.0`. +This narrower requirement applies only when importing `agentic-strata/sigstore`; +the root package remains usable on Node.js 22 or newer. + All commands accept JSON; `validate` and `lint` also accept YAML. Build once with `npm run build` before invoking the repository-local CLI. Input documents are bounded to 16 MiB, JSON member names must be unique, and YAML alias expansion is limited. ## Profiles @@ -179,6 +229,7 @@ Read [Architecture](docs/architecture.md), [Contracts and conformance](docs/cont - Run status and conformance status are separate: a failed run can still have an intact, conformant evidence record, but it is never explained as a completed outcome. - Reports bind the evaluator name, package version, semantic revision, selected rule set, and effective profile configuration. The evaluator digest identifies the declared implementation revision; it is not a code signature. - An unsigned Execution Passport proves deterministic integrity and cross-artifact binding only. Authenticity requires an externally verified DSSE envelope or equivalent trusted channel, and consumers must make that requirement explicit so removing a signature cannot silently downgrade policy. +- `verifyAuthenticatedExecutionPassport` composes complete artifact-set verification with one injected envelope verifier. The optional Sigstore subpath enforces DSSE-only input, one signature, exact canonical payload bytes, positive CT/Rekor thresholds, and literal issuer-plus-SAN authorization; trust-root lifecycle and revocation policy remain deployment responsibilities. - Consumer verification requires the complete supplied subject set and exact bytes for every execution-evidence resource; validating the Passport schema alone does not verify those bindings. - The OASF subject digest proves which opaque record was bound; it does not prove that the record is semantically valid OASF. Validate it before passport creation with the specification owner’s tooling. - Execution-placement evidence enters the predicate only as a strict, run-bound, observation-only descriptor with a normalized observation time and no payload or `content` field. A StageFabric descriptor hashes the exact canonical JSON file bytes, including its trailing LF. Descriptor metadata must come from a trusted producer and be redacted or pseudonymized when sensitive; its optional stable URI may be omitted. Full provider result objects are outside the Passport contract. @@ -193,6 +244,6 @@ Read [Architecture](docs/architecture.md), [Contracts and conformance](docs/cont npm run release:check ``` -The release gate runs strict lint and types, 80+ tests with coverage thresholds, build, repository hygiene, production audit, `publint`, and Are the Types Wrong. +The release gate runs strict lint and types, 100+ tests with coverage thresholds, build, repository hygiene, production audit, `publint`, and Are the Types Wrong. Apache-2.0 licensed. See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md). diff --git a/docs/adr/0006-authenticated-passport-verification.md b/docs/adr/0006-authenticated-passport-verification.md new file mode 100644 index 0000000..fee5c17 --- /dev/null +++ b/docs/adr/0006-authenticated-passport-verification.md @@ -0,0 +1,53 @@ +# ADR 0006: Provider-neutral authenticated Passport verification + +Status: accepted for v0.4 + +## Context + +Execution Passport v2 and `verifyExecutionPassport` establish deterministic +binding across the supplied Statement, run bundle, conformance report, OASF +record, and external execution-evidence bytes. They intentionally do not prove +who produced a wholly self-consistent artifact set. Consumers that require +authenticity need a fail-closed composition without coupling the contract core +to Sigstore, a public trust root, or one identity provider. + +## Decision + +The root export provides `verifyAuthenticatedExecutionPassport` and a generic +`EnvelopeVerifier` port. Artifact-set verification always runs first. The port +receives the exact RFC 8785 UTF-8 Passport bytes and fixed payload type +`application/vnd.in-toto+json`; a final authenticated result requires a complete +verified signer and a matching authenticated-payload digest. Exceptions, +missing evidence, failed verification, and a bare Statement remain unverified. +There is no automatic unsigned fallback. + +Sigstore support is isolated behind the optional `agentic-strata/sigstore` +subpath. `sigstore` and `@sigstore/bundle` are optional peer dependencies and +the root index never imports the adapter. The adapter supports either an +injected, threshold-configured `BundleVerifier` for enterprise or offline trust +material, or a factory using the public Sigstore client and TUF options. + +The Sigstore boundary requires: + +- DSSE rather than a message signature; +- exactly one signature; +- exact payload type and byte equality with the canonical Passport; +- CT-log and Rekor thresholds of at least one; +- a cryptographically verified signer containing both issuer and SAN; and +- literal equality with one configured issuer-plus-SAN pair. + +Identity policy has no regular-expression, wildcard, or issuer-only mode. + +## Consequences + +- Core consumers can integrate other authenticated-envelope systems without a + Sigstore dependency. +- Installing and importing the root package does not install or load Sigstore. +- Removing or replacing a DSSE envelope cannot silently downgrade an + authenticated-required decision. +- Private trust material and public TUF trust can use the same adapter surface. +- The adapter performs verification only. Signing, OIDC token acquisition, key + custody, root distribution and refresh, revocation, and trusted timestamping + remain deployment responsibilities. +- Sigstore 5 currently supports one signature in a bundle; multi-party or + threshold-signature policy requires a future, separately versioned design. diff --git a/docs/architecture.md b/docs/architecture.md index fd48d67..430436e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -86,7 +86,16 @@ expected Statement, checks the complete bundle/report relationship again, and requires a one-to-one digest match for external evidence. Schema validation by itself is deliberately insufficient. -The core emits an unsigned Statement. A replaceable external adapter may wrap its exact RFC 8785 bytes in DSSE and use Sigstore or another trust system. Signature verification, signer identity, transparency-log policy, and trusted time remain consumer-owned controls. +The core emits an unsigned Statement. `verifyAuthenticatedExecutionPassport` +first verifies the complete artifact set, then delegates the envelope to an +injected provider-neutral verifier over the exact RFC 8785 Statement bytes. It +never falls back to unsigned acceptance. The optional `agentic-strata/sigstore` +subpath admits only one-signature DSSE bundles with payload type +`application/vnd.in-toto+json`, exact payload bytes, positive CT/Rekor +thresholds, and an exact issuer-plus-SAN allowlist. Public Sigstore TUF loading +is one factory; enterprise and offline trust stores inject their own compatible +verifier. Signing, key custody, trust-root lifecycle, revocation, and stronger +trusted-time policy remain outside the core. ## Safe semantic caching diff --git a/docs/contracts-and-conformance.md b/docs/contracts-and-conformance.md index 371c3c7..c3570ba 100644 --- a/docs/contracts-and-conformance.md +++ b/docs/contracts-and-conformance.md @@ -63,6 +63,28 @@ unreferenced files fail verification. The CLI exposes the same operation as `verify-passport`. This verifies an unsigned artifact set, not producer identity, OASF semantics, trusted time, or an external DSSE envelope. +Authenticated verification is a separate composition, not a new Passport +schema. `verifyAuthenticatedExecutionPassport` first runs the complete unsigned +artifact-set verification. Only after that succeeds does it pass the exact UTF-8 +RFC 8785 Statement bytes and fixed in-toto payload type to an injected envelope +verifier. The final result is authenticated only when the external verifier +returns a complete signer and the digest of those same bytes. A failed or +removed envelope cannot be reinterpreted as unsigned success. + +The optional `agentic-strata/sigstore` export supplies two constructors: + +- `createSigstoreEnvelopeVerifier` wraps a trust- and threshold-configured + Sigstore `BundleVerifier` for private, offline, or test environments; +- `createPublicSigstoreEnvelopeVerifier` obtains the public Sigstore verifier + using explicit TUF options and thresholds. + +Both require at least one CT log and one Rekor entry, DSSE rather than a message +signature, exactly one signature, payload type `application/vnd.in-toto+json`, +byte equality with the canonical Passport, and one literal issuer-plus-SAN +allowlist match. Identity inputs are not regular expressions. Root loading, +cache freshness, revocation decisions, and any policy stronger than the +underlying Sigstore client remain consumer responsibilities. + ## Evidence, not file detection A conformance run consumes a `RunBundle`. It does not infer architecture from directory names, imports, or keywords. This avoids declaring success simply because a project contains a policy file or telemetry package. Conversely, a source scanner remains useful before runtime evidence exists; its result should be treated as a discovery signal rather than runtime proof. diff --git a/docs/interoperability.md b/docs/interoperability.md index 5168807..945a289 100644 --- a/docs/interoperability.md +++ b/docs/interoperability.md @@ -19,7 +19,7 @@ Adapter YAML files are declarative mappings with an explicit `lossPolicy`. They ## Attestation exchange -The Execution Passport uses the project-controlled predicate namespace documented at [`docs/spec/attestations/execution-passport/v2`](spec/attestations/execution-passport/v2/README.md). The core writes exact RFC 8785 Statement bytes. A deployment may pass those bytes to an external DSSE/Sigstore adapter using payload type `application/vnd.in-toto+json`; no signing key, certificate flow, or Sigstore dependency enters AgenticStrata. +The Execution Passport uses the project-controlled predicate namespace documented at [`docs/spec/attestations/execution-passport/v2`](spec/attestations/execution-passport/v2/README.md). The core writes exact RFC 8785 Statement bytes and has no Sigstore import. A deployment may verify a DSSE envelope through the provider-neutral authenticated-verifier interface. `agentic-strata/sigstore` is a separately exported optional subpath whose Sigstore packages are optional peers; importing the root package does not load or install them. No signing key, OIDC flow, or certificate issuance enters AgenticStrata. Consumers choose one of two explicit policies: @@ -28,6 +28,12 @@ Consumers choose one of two explicit policies: There is no implicit fallback from the second policy to the first. This prevents signature removal from becoming a silent trust downgrade. +The Sigstore adapter supports either an injected `BundleVerifier` with +enterprise or offline trust material, or the public client factory with TUF +options. It deliberately accepts only exact issuer and subject-alternative-name +pairs. Regular-expression identity policy, signing, key management, and +multi-signature semantics are outside this adapter. + ## Agent and flow specifications Portable agent specifications answer questions such as “what is this agent, which tools may it request, and how is a flow assembled?” AgenticStrata answers “what did this application run actually request, authorize, change, verify, and prove?” diff --git a/docs/spec/attestations/execution-passport/v2/README.md b/docs/spec/attestations/execution-passport/v2/README.md index b382ed3..92a4c7c 100644 --- a/docs/spec/attestations/execution-passport/v2/README.md +++ b/docs/spec/attestations/execution-passport/v2/README.md @@ -80,15 +80,16 @@ the exact bytes of every execution-evidence resource. The verifier: This is unsigned artifact-set verification. Producer-contract semantics are validated by the adapter that creates a binding; OASF semantics remain with its -owner. Authenticity, signer identity, trusted time, revocation, and protection -against wholesale replacement still require an externally verified envelope or -trusted store. +owner. `verifyAuthenticatedExecutionPassport` can compose this complete check +with an injected external envelope verifier without changing the Statement +schema. Authenticated-required mode never falls back to unsigned acceptance. +Trusted time, revocation, and trust-root lifecycle remain consumer policy. ## OASF, DSSE, and signing boundaries OASF input is an opaque I-JSON object. Validate it with the owning ecosystem before Passport creation; its subject digest proves which canonical bytes were bound, not semantic OASF validity. -For authenticity, wrap the exact RFC 8785 Statement bytes in a DSSE envelope with payload type `application/vnd.in-toto+json`, then enforce signer identity, trust material, freshness, revocation, and transparency policy outside AgenticStrata. Consumers explicitly choose either unsigned integrity or authenticated-required policy; there is no automatic downgrade. +For authenticity, wrap the exact RFC 8785 Statement bytes in a DSSE envelope with payload type `application/vnd.in-toto+json`, then enforce signer identity, trust material, freshness, revocation, and transparency policy. The optional `agentic-strata/sigstore` verifier requires one DSSE signature, byte equality with the canonical Statement, positive CT/Rekor thresholds, and a literal issuer-plus-SAN allowlist. Its Sigstore dependencies are optional peers and are not imported by the root package. Consumers explicitly choose either unsigned integrity or authenticated-required policy; there is no automatic downgrade. ## Versioning and v1 migration diff --git a/docs/threat-model.md b/docs/threat-model.md index 2ce5501..35e230d 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -42,8 +42,10 @@ Model output, retrieved content, external messages, tool descriptions, and impor | A report claims success after checks are removed, reordered, duplicated, or changed | Creation requires unique ordered check identifiers, binds that order to `rulesDigest`, and derives report status from the checks. | Hash consistency does not authenticate the evaluator or its implementation. | | A report or Passport appears to predate evidence it assesses | Creation requires report generation after every included observed evidence timestamp and issuance after report generation. | These are self-asserted consistency checks, not trusted timestamps; non-repudiation requires external infrastructure. | | An OASF digest is mistaken for semantic validation | The record is treated as opaque I-JSON and documentation requires validation by the owning ecosystem before binding. | A caller can still bind an invalid record if it ignores that prerequisite. | -| A DSSE envelope is removed before consumption | Authenticated consumer policy rejects a bare Statement; unsigned acceptance is a separate explicit mode. | Trust still depends on external verifier configuration, signer identity constraints, and trust material. | -| Statement bytes and digest semantics diverge | Subject digests use RFC 8785 semantic JSON canonicalization, and the CLI emits those exact canonical Statement bytes for an external signer. | Other tooling must preserve the documented payload type and verify the exact DSSE payload bytes. | +| A DSSE envelope is removed before consumption | `verifyAuthenticatedExecutionPassport` requires successful artifact-set and envelope verification and never falls back to the unsigned path. | A consumer can still deliberately call the unsigned API; deployments must select the authenticated entry point for trust decisions. | +| Statement bytes and digest semantics diverge | The authenticated core supplies exact RFC 8785 UTF-8 bytes; the Sigstore adapter requires byte equality and payload type `application/vnd.in-toto+json` before trust verification. | Other signing tooling must preserve those exact bytes and type. | +| A trusted signature is accepted from the wrong workload | The Sigstore adapter requires one literal issuer-plus-SAN pair after cryptographic verification; wildcard and regular-expression policy are not interpreted. | Identity lifecycle, protected workflow configuration, and trust-root updates remain operational controls. | +| Signature removal, type substitution, or extra signatures exploit verifier ambiguity | The adapter admits DSSE only, requires exactly one signature and positive CT/Rekor thresholds, and rejects a bare Statement or message-signature bundle. | Sigstore 5 does not provide multi-signature threshold semantics in one bundle. | ## Non-goals of the alpha diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index f861218..6b99151 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -1,5 +1,5 @@ import { execFileSync } from "node:child_process"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; @@ -34,6 +34,12 @@ try { ["install", "--ignore-scripts", "--no-audit", "--no-fund", join(temporary, filename)], temporary ); + if ( + existsSync(join(temporary, "node_modules", "sigstore")) || + existsSync(join(temporary, "node_modules", "@sigstore", "bundle")) + ) { + throw new Error("Optional Sigstore peers were installed with the core package."); + } const probe = [ 'import { createExecutionPassport, createStageFabricExecutionEvidenceBinding, digestValue, runConformance, runDemo, validateDocument } from "agentic-strata";', @@ -188,6 +194,30 @@ try { cliPassportVerification: "pass" }) ); + + run( + "npm", + [ + "install", + "--ignore-scripts", + "--no-audit", + "--no-fund", + "sigstore@5", + "@sigstore/bundle@5" + ], + temporary + ); + const sigstoreProbe = [ + 'import { createSigstoreEnvelopeVerifier } from "agentic-strata/sigstore";', + 'const bundleVerifier = { verify: () => { throw new Error("not invoked"); } };', + 'let rejected = false;', + 'try { createSigstoreEnvelopeVerifier({ bundleVerifier, identityPolicy: { allowedSigners: [{ issuer: "https://issuer.example", subjectAlternativeName: "https://identity.example/workload" }] }, thresholds: { ctLog: 0, tlog: 1 } }); } catch (error) { rejected = error instanceof Error && error.message === "Sigstore ctLog threshold must be a positive integer."; }', + 'if (!rejected) process.exit(2);', + 'console.log(JSON.stringify({ optionalSigstoreSubpath: "pass", failClosedThresholds: "pass" }));' + ].join("\n"); + process.stdout.write( + run(process.execPath, ["--input-type=module", "--eval", sigstoreProbe], temporary) + ); } finally { rmSync(temporary, { recursive: true, force: true }); } From 22e7b687ea0b4ad8f6c2b03eced130316a4e8efe Mon Sep 17 00:00:00 2001 From: Antonio Antenore Date: Mon, 20 Jul 2026 04:22:12 +0200 Subject: [PATCH 4/4] fix: make injected trust guarantees explicit --- README.md | 10 ++-- ...006-authenticated-passport-verification.md | 8 ++- docs/architecture.md | 11 ++-- docs/contracts-and-conformance.md | 19 ++++--- .../execution-passport/v2/README.md | 2 +- docs/threat-model.md | 2 +- scripts/package-smoke.mjs | 5 +- src/adapters/sigstore.ts | 20 ++----- src/core/authenticated-passport.ts | 57 ++++++++++++++++++- test/authenticated-passport.test.ts | 36 ++++++++++++ test/sigstore-adapter.test.ts | 13 ++--- 11 files changed, 133 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 4fc446e..0030943 100644 --- a/README.md +++ b/README.md @@ -178,9 +178,11 @@ const result = await verifyAuthenticatedExecutionPassport({ ``` The identity values are literal exact matches, never regular expressions or -wildcards. An enterprise or offline deployment can instead pass its own -threshold-configured Sigstore `BundleVerifier` to -`createSigstoreEnvelopeVerifier`. A bare Statement, non-DSSE bundle, second +wildcards. An enterprise or offline deployment can instead pass its own trusted +Sigstore `BundleVerifier` to `createSigstoreEnvelopeVerifier`. That injected +verifier owns its certificate, log, timestamp, revocation, and private-root +policy; AgenticStrata does not claim to introspect its configuration. A bare +Statement, non-DSSE bundle, second signature, different payload type, non-canonical payload bytes, failed trust verification, or different signer fails closed. The adapter verifies only; it does not obtain OIDC tokens or sign Passports. @@ -229,7 +231,7 @@ Read [Architecture](docs/architecture.md), [Contracts and conformance](docs/cont - Run status and conformance status are separate: a failed run can still have an intact, conformant evidence record, but it is never explained as a completed outcome. - Reports bind the evaluator name, package version, semantic revision, selected rule set, and effective profile configuration. The evaluator digest identifies the declared implementation revision; it is not a code signature. - An unsigned Execution Passport proves deterministic integrity and cross-artifact binding only. Authenticity requires an externally verified DSSE envelope or equivalent trusted channel, and consumers must make that requirement explicit so removing a signature cannot silently downgrade policy. -- `verifyAuthenticatedExecutionPassport` composes complete artifact-set verification with one injected envelope verifier. The optional Sigstore subpath enforces DSSE-only input, one signature, exact canonical payload bytes, positive CT/Rekor thresholds, and literal issuer-plus-SAN authorization; trust-root lifecycle and revocation policy remain deployment responsibilities. +- `verifyAuthenticatedExecutionPassport` composes complete artifact-set verification with one injected envelope verifier. The optional Sigstore subpath enforces DSSE-only input, one signature, exact canonical payload bytes, and literal issuer-plus-SAN authorization. Its public-TUF factory also wires positive CT/Rekor thresholds; an injected enterprise verifier owns and must enforce its own trust thresholds, root lifecycle, and revocation policy. - Consumer verification requires the complete supplied subject set and exact bytes for every execution-evidence resource; validating the Passport schema alone does not verify those bindings. - The OASF subject digest proves which opaque record was bound; it does not prove that the record is semantically valid OASF. Validate it before passport creation with the specification owner’s tooling. - Execution-placement evidence enters the predicate only as a strict, run-bound, observation-only descriptor with a normalized observation time and no payload or `content` field. A StageFabric descriptor hashes the exact canonical JSON file bytes, including its trailing LF. Descriptor metadata must come from a trusted producer and be redacted or pseudonymized when sensitive; its optional stable URI may be omitted. Full provider result objects are outside the Passport contract. diff --git a/docs/adr/0006-authenticated-passport-verification.md b/docs/adr/0006-authenticated-passport-verification.md index fee5c17..7bd46e2 100644 --- a/docs/adr/0006-authenticated-passport-verification.md +++ b/docs/adr/0006-authenticated-passport-verification.md @@ -24,15 +24,17 @@ There is no automatic unsigned fallback. Sigstore support is isolated behind the optional `agentic-strata/sigstore` subpath. `sigstore` and `@sigstore/bundle` are optional peer dependencies and the root index never imports the adapter. The adapter supports either an -injected, threshold-configured `BundleVerifier` for enterprise or offline trust -material, or a factory using the public Sigstore client and TUF options. +injected, caller-trusted `BundleVerifier` for enterprise or offline trust +material, or a factory using the public Sigstore client and TUF options. Only +the public factory owns and wires configured CT/Rekor thresholds; an injected +verifier's trust enforcement is explicitly caller-owned. The Sigstore boundary requires: - DSSE rather than a message signature; - exactly one signature; - exact payload type and byte equality with the canonical Passport; -- CT-log and Rekor thresholds of at least one; +- CT-log and Rekor thresholds of at least one when using the public factory; - a cryptographically verified signer containing both issuer and SAN; and - literal equality with one configured issuer-plus-SAN pair. diff --git a/docs/architecture.md b/docs/architecture.md index 430436e..6eb7149 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -91,11 +91,12 @@ first verifies the complete artifact set, then delegates the envelope to an injected provider-neutral verifier over the exact RFC 8785 Statement bytes. It never falls back to unsigned acceptance. The optional `agentic-strata/sigstore` subpath admits only one-signature DSSE bundles with payload type -`application/vnd.in-toto+json`, exact payload bytes, positive CT/Rekor -thresholds, and an exact issuer-plus-SAN allowlist. Public Sigstore TUF loading -is one factory; enterprise and offline trust stores inject their own compatible -verifier. Signing, key custody, trust-root lifecycle, revocation, and stronger -trusted-time policy remain outside the core. +`application/vnd.in-toto+json`, exact payload bytes, and an exact issuer-plus-SAN +allowlist. The public Sigstore TUF factory additionally wires positive CT/Rekor +thresholds. Enterprise and offline trust stores inject their own compatible, +trusted verifier and own its certificate, log, timestamp, and revocation policy. +Signing, key custody, trust-root lifecycle, and stronger trusted-time policy +remain outside the core. ## Safe semantic caching diff --git a/docs/contracts-and-conformance.md b/docs/contracts-and-conformance.md index c3570ba..f17d119 100644 --- a/docs/contracts-and-conformance.md +++ b/docs/contracts-and-conformance.md @@ -73,17 +73,20 @@ removed envelope cannot be reinterpreted as unsigned success. The optional `agentic-strata/sigstore` export supplies two constructors: -- `createSigstoreEnvelopeVerifier` wraps a trust- and threshold-configured - Sigstore `BundleVerifier` for private, offline, or test environments; +- `createSigstoreEnvelopeVerifier` wraps a caller-trusted Sigstore + `BundleVerifier` for private, offline, or test environments; the caller owns + its certificate, log, timestamp, revocation, and trust-root policy; - `createPublicSigstoreEnvelopeVerifier` obtains the public Sigstore verifier using explicit TUF options and thresholds. -Both require at least one CT log and one Rekor entry, DSSE rather than a message -signature, exactly one signature, payload type `application/vnd.in-toto+json`, -byte equality with the canonical Passport, and one literal issuer-plus-SAN -allowlist match. Identity inputs are not regular expressions. Root loading, -cache freshness, revocation decisions, and any policy stronger than the -underlying Sigstore client remain consumer responsibilities. +Both require DSSE rather than a message signature, exactly one signature, +payload type `application/vnd.in-toto+json`, byte equality with the canonical +Passport, and one literal issuer-plus-SAN allowlist match. The public factory +also rejects CT or Rekor thresholds below one and passes accepted values to +Sigstore. The injected factory cannot introspect its trusted `BundleVerifier` +and therefore makes no threshold claim on its behalf. Identity inputs are not +regular expressions. Root loading, cache freshness, revocation decisions, and +any policy stronger than the underlying verifier remain consumer responsibilities. ## Evidence, not file detection diff --git a/docs/spec/attestations/execution-passport/v2/README.md b/docs/spec/attestations/execution-passport/v2/README.md index 92a4c7c..2b54391 100644 --- a/docs/spec/attestations/execution-passport/v2/README.md +++ b/docs/spec/attestations/execution-passport/v2/README.md @@ -89,7 +89,7 @@ Trusted time, revocation, and trust-root lifecycle remain consumer policy. OASF input is an opaque I-JSON object. Validate it with the owning ecosystem before Passport creation; its subject digest proves which canonical bytes were bound, not semantic OASF validity. -For authenticity, wrap the exact RFC 8785 Statement bytes in a DSSE envelope with payload type `application/vnd.in-toto+json`, then enforce signer identity, trust material, freshness, revocation, and transparency policy. The optional `agentic-strata/sigstore` verifier requires one DSSE signature, byte equality with the canonical Statement, positive CT/Rekor thresholds, and a literal issuer-plus-SAN allowlist. Its Sigstore dependencies are optional peers and are not imported by the root package. Consumers explicitly choose either unsigned integrity or authenticated-required policy; there is no automatic downgrade. +For authenticity, wrap the exact RFC 8785 Statement bytes in a DSSE envelope with payload type `application/vnd.in-toto+json`, then enforce signer identity, trust material, freshness, revocation, and transparency policy. The optional `agentic-strata/sigstore` verifier requires one DSSE signature, byte equality with the canonical Statement, and a literal issuer-plus-SAN allowlist. Its public-TUF factory also wires positive CT/Rekor thresholds; a caller-injected verifier owns its own threshold and trust policy. Sigstore dependencies are optional peers and are not imported by the root package. Consumers explicitly choose either unsigned integrity or authenticated-required policy; there is no automatic downgrade. ## Versioning and v1 migration diff --git a/docs/threat-model.md b/docs/threat-model.md index 35e230d..4586c28 100644 --- a/docs/threat-model.md +++ b/docs/threat-model.md @@ -45,7 +45,7 @@ Model output, retrieved content, external messages, tool descriptions, and impor | A DSSE envelope is removed before consumption | `verifyAuthenticatedExecutionPassport` requires successful artifact-set and envelope verification and never falls back to the unsigned path. | A consumer can still deliberately call the unsigned API; deployments must select the authenticated entry point for trust decisions. | | Statement bytes and digest semantics diverge | The authenticated core supplies exact RFC 8785 UTF-8 bytes; the Sigstore adapter requires byte equality and payload type `application/vnd.in-toto+json` before trust verification. | Other signing tooling must preserve those exact bytes and type. | | A trusted signature is accepted from the wrong workload | The Sigstore adapter requires one literal issuer-plus-SAN pair after cryptographic verification; wildcard and regular-expression policy are not interpreted. | Identity lifecycle, protected workflow configuration, and trust-root updates remain operational controls. | -| Signature removal, type substitution, or extra signatures exploit verifier ambiguity | The adapter admits DSSE only, requires exactly one signature and positive CT/Rekor thresholds, and rejects a bare Statement or message-signature bundle. | Sigstore 5 does not provide multi-signature threshold semantics in one bundle. | +| Signature removal, type substitution, or extra signatures exploit verifier ambiguity | The adapter admits DSSE only, requires exactly one signature, and rejects a bare Statement or message-signature bundle. The public-TUF factory also enforces positive CT/Rekor threshold configuration. | An injected `BundleVerifier` is trusted code and owns its own certificate, log, timestamp, and revocation policy. Sigstore 5 does not provide multi-signature threshold semantics in one bundle. | ## Non-goals of the alpha diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index 6b99151..62d82af 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -208,10 +208,9 @@ try { temporary ); const sigstoreProbe = [ - 'import { createSigstoreEnvelopeVerifier } from "agentic-strata/sigstore";', - 'const bundleVerifier = { verify: () => { throw new Error("not invoked"); } };', + 'import { createPublicSigstoreEnvelopeVerifier } from "agentic-strata/sigstore";', 'let rejected = false;', - 'try { createSigstoreEnvelopeVerifier({ bundleVerifier, identityPolicy: { allowedSigners: [{ issuer: "https://issuer.example", subjectAlternativeName: "https://identity.example/workload" }] }, thresholds: { ctLog: 0, tlog: 1 } }); } catch (error) { rejected = error instanceof Error && error.message === "Sigstore ctLog threshold must be a positive integer."; }', + 'try { await createPublicSigstoreEnvelopeVerifier({ identityPolicy: { allowedSigners: [{ issuer: "https://issuer.example", subjectAlternativeName: "https://identity.example/workload" }] }, thresholds: { ctLog: 0, tlog: 1 } }); } catch (error) { rejected = error instanceof Error && error.message === "Sigstore ctLog threshold must be a positive integer."; }', 'if (!rejected) process.exit(2);', 'console.log(JSON.stringify({ optionalSigstoreSubpath: "pass", failClosedThresholds: "pass" }));' ].join("\n"); diff --git a/src/adapters/sigstore.ts b/src/adapters/sigstore.ts index 1391b2f..dd4f665 100644 --- a/src/adapters/sigstore.ts +++ b/src/adapters/sigstore.ts @@ -37,12 +37,6 @@ export const DEFAULT_SIGSTORE_VERIFICATION_THRESHOLDS = { export interface CreateSigstoreEnvelopeVerifierInput { bundleVerifier: BundleVerifier; identityPolicy: SigstoreIdentityPolicy; - /** - * Thresholds already enforced by the injected BundleVerifier. Values below - * one are rejected so authenticated-required mode cannot disable Sigstore's - * certificate or transparency verification by configuration. - */ - thresholds?: SigstoreVerificationThresholds; } export type PublicSigstoreTufOptions = Pick< @@ -60,9 +54,7 @@ export interface CreatePublicSigstoreEnvelopeVerifierInput { tuf?: PublicSigstoreTufOptions; } -export interface SigstoreEnvelopeVerifier extends EnvelopeVerifier { - readonly thresholds: Readonly; -} +export type SigstoreEnvelopeVerifier = EnvelopeVerifier; const MAX_ALLOWED_SIGNERS = 32; const MAX_IDENTITY_LENGTH = 2048; @@ -254,17 +246,16 @@ function verifySigstoreEnvelope( } /** - * Wrap an enterprise, offline, or test BundleVerifier. The caller configures - * its trust material and must ensure it enforces the declared thresholds. + * Wrap an enterprise, offline, or test BundleVerifier. The injected verifier + * is trusted code: its trust material, log/timestamp thresholds, and revocation + * policy are caller-owned and are not inferred by this adapter. */ export function createSigstoreEnvelopeVerifier( input: CreateSigstoreEnvelopeVerifierInput ): SigstoreEnvelopeVerifier { const identityPolicy = copyIdentityPolicy(input.identityPolicy); - const thresholds = copyThresholds(input.thresholds); return { provider: "sigstore", - thresholds, verify: (verificationInput) => Promise.resolve( verifySigstoreEnvelope( @@ -291,7 +282,6 @@ export async function createPublicSigstoreEnvelopeVerifier( }); return createSigstoreEnvelopeVerifier({ bundleVerifier, - identityPolicy: input.identityPolicy, - thresholds + identityPolicy: input.identityPolicy }); } diff --git a/src/core/authenticated-passport.ts b/src/core/authenticated-passport.ts index ee53e19..dbf1722 100644 --- a/src/core/authenticated-passport.ts +++ b/src/core/authenticated-passport.ts @@ -77,6 +77,28 @@ function failedAuthentication( return { status: "failed", provider, issues }; } +function isEnvelopeVerificationResult( + value: unknown +): value is EnvelopeVerificationResult { + if (value === null || typeof value !== "object" || Array.isArray(value)) { + return false; + } + const candidate = value as Record; + if (typeof candidate.valid !== "boolean" || !Array.isArray(candidate.issues)) { + return false; + } + const issuesAreStructured = candidate.issues.every( + (item) => + item !== null && + typeof item === "object" && + !Array.isArray(item) && + typeof (item as Record).path === "string" && + typeof (item as Record).code === "string" && + typeof (item as Record).message === "string" + ); + return issuesAreStructured && (candidate.valid || candidate.issues.length > 0); +} + /** * Verify a complete Execution Passport artifact set and require an external, * authenticated envelope over the exact canonical Statement bytes. The core @@ -128,6 +150,24 @@ export async function verifyAuthenticatedExecutionPassport( }; } + if (!isEnvelopeVerificationResult(verified)) { + const issues: ValidationIssue[] = [ + { + path: "/authentication", + code: "envelope-verifier-result", + message: + "The authenticated envelope verifier did not return a well-formed verification result." + } + ]; + return { + valid: false, + issues, + trustLevel: "unverified", + artifactSet, + authentication: failedAuthentication(input.verifier.provider, issues) + }; + } + if (!verified.valid) { return { valid: false, @@ -138,9 +178,17 @@ export async function verifyAuthenticatedExecutionPassport( }; } + const signer = verified.signer; if ( - verified.signer === undefined || - verified.authenticatedPayloadDigest === undefined + verified.issues.length !== 0 || + signer === undefined || + signer === null || + typeof signer !== "object" || + typeof signer.issuer !== "string" || + signer.issuer.length === 0 || + typeof signer.subjectAlternativeName !== "string" || + signer.subjectAlternativeName.length === 0 || + typeof verified.authenticatedPayloadDigest !== "string" ) { const issues: ValidationIssue[] = [ { @@ -186,7 +234,10 @@ export async function verifyAuthenticatedExecutionPassport( status: "verified", provider: input.verifier.provider, authenticatedPayloadDigest: expectedPayloadDigest, - signer: verified.signer, + signer: { + issuer: signer.issuer, + subjectAlternativeName: signer.subjectAlternativeName + }, issues: [] } }; diff --git a/test/authenticated-passport.test.ts b/test/authenticated-passport.test.ts index b262273..d88b39b 100644 --- a/test/authenticated-passport.test.ts +++ b/test/authenticated-passport.test.ts @@ -10,6 +10,7 @@ import { verifyAuthenticatedExecutionPassport } from "../src/index.js"; import type { + EnvelopeVerificationResult, EnvelopeVerifier, VerifyAuthenticatedExecutionPassportInput } from "../src/index.js"; @@ -157,6 +158,41 @@ describe("authenticated Execution Passport verification", () => { }); expect(incompleteResult.issues[0]?.code).toBe("envelope-verifier-result"); + const contradictory: EnvelopeVerifier = { + provider: "test-envelope", + verify: ({ expectedPayload }) => Promise.resolve({ + valid: true, + issues: [ + { + path: "/authentication/signature", + code: "signature-invalid", + message: "A valid result cannot retain a failure issue." + } + ], + authenticatedPayloadDigest: digestBytes(expectedPayload), + signer: null + } as unknown as EnvelopeVerificationResult) + }; + const contradictoryResult = await verifyAuthenticatedExecutionPassport({ + ...input, + verifier: contradictory + }); + expect(contradictoryResult).toMatchObject({ + valid: false, + trustLevel: "unverified", + issues: [{ code: "envelope-verifier-result" }] + }); + + const malformed: EnvelopeVerifier = { + provider: "test-envelope", + verify: () => Promise.resolve(null as unknown as EnvelopeVerificationResult) + }; + const malformedResult = await verifyAuthenticatedExecutionPassport({ + ...input, + verifier: malformed + }); + expect(malformedResult.issues[0]?.code).toBe("envelope-verifier-result"); + const mismatched: EnvelopeVerifier = { provider: "test-envelope", verify: () => Promise.resolve({ diff --git a/test/sigstore-adapter.test.ts b/test/sigstore-adapter.test.ts index e806d96..825a672 100644 --- a/test/sigstore-adapter.test.ts +++ b/test/sigstore-adapter.test.ts @@ -10,6 +10,7 @@ import { describe, expect, it } from "vitest"; import type { BundleVerifier } from "sigstore"; import { + createPublicSigstoreEnvelopeVerifier, createSigstoreEnvelopeVerifier, type SigstoreIdentityPolicy } from "../src/adapters/sigstore.js"; @@ -142,8 +143,7 @@ describe("optional Sigstore adapter", () => { const envelope = dsseBundle(fixture.payload, keys.privateKey); const verifier = createSigstoreEnvelopeVerifier({ bundleVerifier: ed25519BundleVerifier(keys.publicKey), - identityPolicy: exactPolicy(), - thresholds: { ctLog: 1, tlog: 1 } + identityPolicy: exactPolicy() }); const result = await verifyAuthenticatedExecutionPassport({ @@ -252,16 +252,15 @@ describe("optional Sigstore adapter", () => { expect(missingResult.issues[0]?.code).toBe("sigstore-identity-missing"); }); - it("rejects disabled trust thresholds and a bare-Statement downgrade", async () => { + it("rejects disabled public-trust thresholds and a bare-Statement downgrade", async () => { const fixture = passportFixture(); const keys = generateKeyPairSync("ed25519"); - expect(() => - createSigstoreEnvelopeVerifier({ - bundleVerifier: ed25519BundleVerifier(keys.publicKey), + await expect( + createPublicSigstoreEnvelopeVerifier({ identityPolicy: exactPolicy(), thresholds: { ctLog: 0, tlog: 1 } }) - ).toThrow("Sigstore ctLog threshold must be a positive integer."); + ).rejects.toThrow("Sigstore ctLog threshold must be a positive integer."); const verifier = createSigstoreEnvelopeVerifier({ bundleVerifier: ed25519BundleVerifier(keys.publicKey),