diff --git a/.env.example b/.env.example index 1dbf981..bf0c3df 100644 --- a/.env.example +++ b/.env.example @@ -5,11 +5,14 @@ ISSUER_PUBLIC_JWK_JSON='{"kty":"EC","crv":"P-256","x":"","y":""}' DB_PATH=attestations.sqlite DATABASE_URL="file:./prisma/dev.db" # Supabase aliases for apps/api Postgres (optional) +SUPABASE_URL=https://.supabase.co +SUPABASE_SERVICE_ROLE_KEY=replace-with-server-only-service-role-key SUPABASE_DB_URL=postgresql://postgres.:[password]@aws-0-.pooler.supabase.com:6543/postgres?sslmode=require SUPABASE_POOLER_URL=postgresql://postgres.:[password]@aws-0-.pooler.supabase.com:6543/postgres?sslmode=require SUPABASE_DIRECT_URL=postgresql://postgres:[password]@db..supabase.co:5432/postgres?sslmode=require # Optional helper if using Supabase CLI pooler URL discovery from `supabase/.temp/pooler-url`. SUPABASE_DB_PASSWORD=replace-with-supabase-db-password +SUPABASE_SECRET_KEY=replace-with-server-only-secret-key PORT=3000 # apps/api security controls diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 69def6f..356dd9b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,19 +1,53 @@ version: 2 + updates: - package-ecosystem: "npm" directory: "/" schedule: interval: "weekly" + day: "monday" + time: "05:00" + timezone: "America/Chicago" open-pull-requests-limit: 5 + groups: + npm-production: + dependency-type: "production" + npm-development: + dependency-type: "development" labels: - "dependencies" - "security" + commit-message: + prefix: "deps" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" + day: "monday" + time: "05:30" + timezone: "America/Chicago" open-pull-requests-limit: 5 + groups: + github-actions: + patterns: + - "*" + labels: + - "dependencies" + - "security" + commit-message: + prefix: "deps" + + - package-ecosystem: "cargo" + directory: "/circuits/non_mem_gadget" + schedule: + interval: "weekly" + day: "monday" + time: "06:00" + timezone: "America/Chicago" + open-pull-requests-limit: 3 labels: - "dependencies" - "security" + commit-message: + prefix: "deps" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 6dfd96c..1295968 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,14 +1,33 @@ ## Summary -- Describe the change +- Describe the change and why it is needed. -## AI Disclosure +## Change Type -- [ ] AI-assisted changes are included in this PR +- [ ] Runtime or API behavior +- [ ] Security or repo governance +- [ ] Workflow or CI configuration +- [ ] Documentation or claims boundary only +- [ ] Dependency update -## Review Checklist +## Security Review -- [ ] Human review requested -- [ ] Tests added or updated where appropriate +- [ ] Security impact is described - [ ] No secrets, tokens, cookies, or raw PII were added to code, logs, fixtures, or docs -- [ ] Security impact and remaining risks are described +- [ ] New permissions, auth assumptions, or trust-boundary changes are called out +- [ ] Dependency changes were reviewed for risk + +## Claims Boundary And Docs + +- [ ] Public-facing claims or evaluator docs were updated if needed +- [ ] No unsupported claims were introduced + +## Validation + +- [ ] Human review requested +- [ ] Tests or validation commands were run where appropriate +- [ ] Workflow changes were reviewed for least privilege and pinned actions + +## AI Disclosure + +- [ ] AI-assisted changes are included in this PR diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..f52db3f --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,17 @@ +name: Security / Dependency Review + +on: + pull_request: + +permissions: + contents: read + +jobs: + dependency-review: + name: Dependency diff review + runs-on: ubuntu-latest + steps: + - name: Dependency review + uses: actions/dependency-review-action@v4 # GitHub-maintained action pinned to supported major; Dependabot tracks updates. + with: + fail-on-severity: high diff --git a/.github/workflows/trivy.yml b/.github/workflows/trivy.yml new file mode 100644 index 0000000..26e6709 --- /dev/null +++ b/.github/workflows/trivy.yml @@ -0,0 +1,54 @@ +name: Security / Trivy Filesystem Scan + +on: + pull_request: + push: + branches: + - main + +permissions: + contents: read + security-events: write + +jobs: + trivy-fs: + name: Trivy repository scan + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.1.7 + with: + persist-credentials: false + + - name: Run Trivy filesystem scan + uses: aquasecurity/trivy-action@0.34.0 + with: + scan-type: fs + scan-ref: . + scanners: vuln + vuln-type: os,library + severity: HIGH,CRITICAL + ignore-unfixed: true + hide-progress: true + format: sarif + output: trivy-results.sarif + limit-severities-for-sarif: true + exit-code: "0" + skip-dirs: node_modules,.next,dist,coverage,.git + + - name: Upload Trivy SARIF + if: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) + uses: github/codeql-action/upload-sarif@v4 # GitHub-maintained action pinned to supported major; Dependabot tracks updates. + with: + sarif_file: trivy-results.sarif + + - name: Summarize Trivy mode + if: always() + run: | + { + echo "## Trivy filesystem scan" + echo "" + echo "- Mode: advisory" + echo "- Scope: filesystem vulnerability scan with HIGH/CRITICAL severity only" + echo "- Notes: ignores unfixed issues and uploads SARIF for review in Security/code scanning when token permissions allow it" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 0000000..10676f0 --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,42 @@ +name: Security / zizmor Workflow Audit + +on: + pull_request: + paths: + - ".github/workflows/**" + push: + paths: + - ".github/workflows/**" + +permissions: {} + +jobs: + zizmor: + name: zizmor advisory audit + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + steps: + - name: Checkout + uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v4.1.7 + with: + persist-credentials: false + + - name: Run zizmor + continue-on-error: true + uses: zizmorcore/zizmor-action@e639db99335bc9038abc0e066dfcd72e23d26fb4 # v0.3.0 + with: + advanced-security: false + annotations: true + + - name: Summarize zizmor mode + if: always() + run: | + { + echo "## zizmor workflow audit" + echo "" + echo "- Mode: advisory" + echo "- Scope: GitHub Actions workflow security linting" + echo "- Notes: findings are annotated in the run and should be reviewed before merging workflow changes" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/README.md b/README.md index b1deba6..9944c53 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,24 @@ Website: https://trustsignal.dev TrustSignal is evidence integrity infrastructure for existing workflows. It acts as an integrity layer that returns signed verification receipts, verification signals, verifiable provenance metadata, and later verification capability without replacing the upstream system of record. +Short description: +This repository is the main TrustSignal documentation and implementation surface for public evaluation, existing workflow integration, and signed verification receipts with later verification. + +Audience: +- evaluators +- developers +- partner reviewers + +## Start Here + +- [Documentation index](docs/README.md) +- [Partner evaluation overview](docs/partner-eval/overview.md) +- [Verification lifecycle](docs/verification-lifecycle.md) +- [Security workflows](docs/security-workflows.md) +- [GitHub settings checklist](docs/github-settings-checklist.md) +- [API overview](wiki/API-Overview.md) +- [Claims boundary](wiki/Claims-Boundary.md) + ## Problem High-stakes document and evidence workflows create an attack surface after collection, not just at intake. Once an artifact has been uploaded, reviewed, or approved, downstream teams still face risks such as tampered evidence, provenance loss, artifact substitution, and stale evidence that can no longer be verified later. @@ -18,7 +36,7 @@ Those risks matter in audit, compliance, partner-review, and trust-sensitive wor ## Verification Lifecycle -The canonical lifecycle diagram and trust-boundary view are documented in [docs/verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md). +The canonical lifecycle diagram and trust-boundary view are documented in [docs/verification-lifecycle.md](docs/verification-lifecycle.md). TrustSignal accepts a verification request, returns verification signals, issues a signed verification receipt, and supports later verification against stored receipt state so downstream teams can detect artifact tampering, evidence provenance loss, or stale records during audit review. @@ -27,7 +45,6 @@ TrustSignal accepts a verification request, returns verification signals, issues The fastest evaluator path is the local 5-minute developer trial: TrustSignal provides: - - signed verification receipts - verification signals - verifiable provenance metadata @@ -47,17 +64,17 @@ It shows the full lifecycle in one run: 4. later verification 5. tampered artifact mismatch detection -See [demo/README.md](/Users/christopher/Projects/trustsignal/demo/README.md). +See [demo/README.md](demo/README.md). ## Integration Model Start here if you are evaluating the public verification lifecycle: -- [Evaluator quickstart](/Users/christopher/Projects/trustsignal/docs/partner-eval/quickstart.md) -- [API playground](/Users/christopher/Projects/trustsignal/docs/partner-eval/api-playground.md) -- [OpenAPI contract](/Users/christopher/Projects/trustsignal/openapi.yaml) -- [Postman collection](/Users/christopher/Projects/trustsignal/postman/TrustSignal.postman_collection.json) -- [Postman local environment](/Users/christopher/Projects/trustsignal/postman/TrustSignal.local.postman_environment.json) +- [Evaluator quickstart](docs/partner-eval/quickstart.md) +- [API playground](docs/partner-eval/api-playground.md) +- [OpenAPI contract](openapi.yaml) +- [Postman collection](postman/TrustSignal.postman_collection.json) +- [Postman local environment](postman/TrustSignal.local.postman_environment.json) Golden path: @@ -90,6 +107,11 @@ The evaluator path is designed to show the core value before full production int - later verification against the stored receipt state - visible handling for tampered evidence or stale evidence through the later verification lifecycle +## Production Considerations + +> [!IMPORTANT] +> Production considerations: local evaluator paths are deliberate evaluation paths. They do not replace deployment-specific authentication, signing configuration, infrastructure controls, or operational review. + ## Local API Development Setup Prerequisites: @@ -161,6 +183,17 @@ TrustSignal is designed to sit behind an existing workflow such as: The upstream platform remains the system of record. TrustSignal adds an integrity layer at the boundary and returns technical verification artifacts that the upstream workflow can store and use later. +## Security And Claims Boundary + +> [!NOTE] +> Claims boundary: this repository documents the public integration and evaluation surface only. It does not expose proof internals, circuit identifiers, model outputs, signing infrastructure specifics, or internal service topology. + +## Compliance and Security Readiness + +TrustSignal includes a repository-level SOC 2 readiness framework for assessing security posture, documentation maturity, governance evidence, and mock-audit gaps. It is intended to support internal review and partner diligence preparation. It does not claim SOC 2 certification. + +- [SOC 2 readiness report](docs/compliance/soc2/readiness-report.md) + ## Integration Boundary Notes The local evaluator path is intentionally constrained. Local development defaults are a deliberate evaluator and development path, and they fail closed where production trust assumptions are not satisfied. @@ -191,12 +224,12 @@ Fail-closed defaults are part of the security posture. They are meant to prevent The public evaluation artifacts in this repo are: -- [openapi.yaml](/Users/christopher/Projects/trustsignal/openapi.yaml) -- [verification-request.json](/Users/christopher/Projects/trustsignal/examples/verification-request.json) -- [verification-response.json](/Users/christopher/Projects/trustsignal/examples/verification-response.json) -- [verification-receipt.json](/Users/christopher/Projects/trustsignal/examples/verification-receipt.json) -- [verification-status.json](/Users/christopher/Projects/trustsignal/examples/verification-status.json) -- [partner evaluation kit](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [openapi.yaml](openapi.yaml) +- [verification-request.json](examples/verification-request.json) +- [verification-response.json](examples/verification-response.json) +- [verification-receipt.json](examples/verification-receipt.json) +- [verification-status.json](examples/verification-status.json) +- [partner evaluation kit](docs/partner-eval/overview.md) These artifacts document the public verification lifecycle only. They intentionally avoid proof internals, model outputs, circuit identifiers, signing infrastructure specifics, and internal service topology. @@ -211,7 +244,7 @@ Public-facing security properties for this repository are: - explicit lifecycle boundaries for read, revoke, and provenance-state operations - fail-closed defaults where production trust assumptions are not satisfied -See [docs/security-summary.md](/Users/christopher/Projects/trustsignal/docs/security-summary.md), [SECURITY_CHECKLIST.md](/Users/christopher/Projects/trustsignal/SECURITY_CHECKLIST.md), and [docs/SECURITY.md](/Users/christopher/Projects/trustsignal/docs/SECURITY.md) for the current public-safe security summary and repository guardrails. +See [docs/security-summary.md](docs/security-summary.md), [SECURITY_CHECKLIST.md](SECURITY_CHECKLIST.md), and [docs/SECURITY.md](docs/SECURITY.md) for the current public-safe security summary and repository guardrails. ## What TrustSignal Does Not Claim @@ -237,12 +270,27 @@ npm run typecheck npm run build ``` +## Current Evaluator Metrics + +Recent local benchmark snapshot from [bench/results/latest.md](bench/results/latest.md). A partner-facing interpretation is available in [docs/partner-eval/benchmark-summary.md](docs/partner-eval/benchmark-summary.md). + +- clean verification request latency: mean `5.24 ms`, median `4.11 ms`, p95 `21.65 ms` +- signed receipt generation latency: mean `0.34 ms`, median `0.32 ms`, p95 `0.63 ms` +- receipt lookup latency: mean `0.57 ms`, median `0.56 ms`, p95 `0.63 ms` +- later verification latency: mean `0.77 ms`, median `0.71 ms`, p95 `1.08 ms` +- tampered artifact detection latency: mean `7.76 ms`, median `5.13 ms`, p95 `42.82 ms` +- repeated-run stability for the same artifact payload: mean `3.24 ms`, median `3.16 ms`, p95 `3.69 ms` + +This is a recent local evaluator run against the current `/api/v1/*` lifecycle with a temporary local PostgreSQL instance. It is a benchmark snapshot for evaluation and regression tracking, not a production guarantee or SLA. + ## Documentation Map -- [docs/partner-eval/overview.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) -- [docs/partner-eval/quickstart.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/quickstart.md) -- [docs/partner-eval/api-playground.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/api-playground.md) -- [wiki/What-is-TrustSignal.md](/Users/christopher/Projects/trustsignal/wiki/What-is-TrustSignal.md) -- [wiki/API-Overview.md](/Users/christopher/Projects/trustsignal/wiki/API-Overview.md) -- [wiki/Claims-Boundary.md](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md) -- [wiki/Verification-Receipts.md](/Users/christopher/Projects/trustsignal/wiki/Verification-Receipts.md) +- [docs/partner-eval/overview.md](docs/partner-eval/overview.md) +- [docs/partner-eval/quickstart.md](docs/partner-eval/quickstart.md) +- [docs/partner-eval/api-playground.md](docs/partner-eval/api-playground.md) +- [docs/templates/docs-architecture.md](docs/templates/docs-architecture.md) +- [docs/templates/doc-template.md](docs/templates/doc-template.md) +- [wiki/What-is-TrustSignal.md](wiki/What-is-TrustSignal.md) +- [wiki/API-Overview.md](wiki/API-Overview.md) +- [wiki/Claims-Boundary.md](wiki/Claims-Boundary.md) +- [wiki/Verification-Receipts.md](wiki/Verification-Receipts.md) diff --git a/apps/api/.env.example b/apps/api/.env.example index c3692b5..b1a7508 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -42,11 +42,14 @@ RATE_LIMIT_API_KEY_MAX=120 # Database (must enforce TLS; include sslmode=require) DATABASE_URL=postgresql://user:password@host:5432/deedshield?sslmode=require # Supabase aliases (optional if you prefer naming by provider) +SUPABASE_URL=https://.supabase.co +SUPABASE_SERVICE_ROLE_KEY=replace-with-server-only-service-role-key SUPABASE_DB_URL=postgresql://postgres.:[password]@aws-0-.pooler.supabase.com:6543/postgres?sslmode=require SUPABASE_POOLER_URL=postgresql://postgres.:[password]@aws-0-.pooler.supabase.com:6543/postgres?sslmode=require SUPABASE_DIRECT_URL=postgresql://postgres:[password]@db..supabase.co:5432/postgres?sslmode=require # Optional helper if using Supabase CLI pooler URL discovery from `supabase/.temp/pooler-url`. SUPABASE_DB_PASSWORD=replace-with-supabase-db-password +SUPABASE_SECRET_KEY=replace-with-server-only-secret-key # Blockchain Configuration (Sepolia or Local) ANCHOR_REGISTRY_ADDRESS=0x... diff --git a/apps/api/package.json b/apps/api/package.json index 3af10a7..ff1e038 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -21,6 +21,7 @@ "@prisma/client": "^5.17.0", "ethers": "^6.12.0", "fastify": "^5.8.1", + "jose": "^5.2.4", "openai": "^6.17.0", "pdf2json": "^3.1.4", "pdfkit": "^0.15.0", diff --git a/apps/api/prisma/migrations/20260313090000_add_artifact_receipts/migration.sql b/apps/api/prisma/migrations/20260313090000_add_artifact_receipts/migration.sql new file mode 100644 index 0000000..b24d7ed --- /dev/null +++ b/apps/api/prisma/migrations/20260313090000_add_artifact_receipts/migration.sql @@ -0,0 +1,51 @@ +CREATE TABLE "ArtifactReceipt" ( + "receiptId" TEXT NOT NULL PRIMARY KEY, + "verificationId" TEXT NOT NULL, + "artifactHash" TEXT NOT NULL, + "algorithm" TEXT NOT NULL, + "sourceProvider" TEXT NOT NULL, + "repository" TEXT, + "workflow" TEXT, + "runId" TEXT, + "commitSha" TEXT, + "actor" TEXT, + "status" TEXT NOT NULL, + "receiptSignature" TEXT NOT NULL, + "receiptSignatureAlg" TEXT NOT NULL, + "receiptSignatureKid" TEXT NOT NULL, + "metadataArtifactPath" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE UNIQUE INDEX "ArtifactReceipt_verificationId_key" +ON "ArtifactReceipt" ("verificationId"); + +CREATE INDEX "ArtifactReceipt_createdAt_idx" +ON "ArtifactReceipt" ("createdAt"); + +ALTER TABLE "ArtifactReceipt" ENABLE ROW LEVEL SECURITY; +ALTER TABLE "ArtifactReceipt" FORCE ROW LEVEL SECURITY; + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'postgres') THEN + CREATE POLICY "artifact_receipts_postgres_all" + ON "ArtifactReceipt" + FOR ALL + TO postgres + USING (true) + WITH CHECK (true); + END IF; +END $$; + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'service_role') THEN + CREATE POLICY "artifact_receipts_service_role_all" + ON "ArtifactReceipt" + FOR ALL + TO service_role + USING (true) + WITH CHECK (true); + END IF; +END $$; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 1b82054..e81741c 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -34,6 +34,27 @@ model Receipt { revoked Boolean @default(false) } +model ArtifactReceipt { + receiptId String @id + verificationId String @unique + artifactHash String + algorithm String + sourceProvider String + repository String? + workflow String? + runId String? + commitSha String? + actor String? + status String + receiptSignature String + receiptSignatureAlg String + receiptSignatureKid String + metadataArtifactPath String? + createdAt DateTime @default(now()) + + @@index([createdAt]) +} + model Property { parcelId String @id currentOwner String diff --git a/apps/api/src/artifact-verification.test.ts b/apps/api/src/artifact-verification.test.ts new file mode 100644 index 0000000..83ddab4 --- /dev/null +++ b/apps/api/src/artifact-verification.test.ts @@ -0,0 +1,173 @@ +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { FastifyInstance } from 'fastify'; +import { PrismaClient } from '@prisma/client'; + +import { buildServer } from './server.js'; + +const hasDatabaseUrl = + Boolean(process.env.DATABASE_URL) || + Boolean(process.env.SUPABASE_DB_URL) || + Boolean(process.env.SUPABASE_POOLER_URL) || + Boolean(process.env.SUPABASE_DIRECT_URL); +const describeWithDatabase = hasDatabaseUrl ? describe.sequential : describe.skip; + +describeWithDatabase('Generic artifact verification API', () => { + let app: FastifyInstance; + let prisma: PrismaClient; + const apiKey = 'artifact-test-api-key'; + + beforeAll(async () => { + process.env.API_KEYS = apiKey; + process.env.API_KEY_SCOPES = `${apiKey}=verify|read`; + prisma = new PrismaClient(); + app = await buildServer(); + }); + + afterAll(async () => { + await app.close(); + await prisma.$disconnect(); + delete process.env.API_KEYS; + delete process.env.API_KEY_SCOPES; + }); + + it('issues, persists, and later verifies a generic artifact receipt', async () => { + const artifactHash = + '2f77668a9dfbf8d5847cf2d5d0370740e0c0601b4f061c1181f58c77c2b8f486'; + + const verifyRes = await app.inject({ + method: 'POST', + url: '/api/v1/verify', + headers: { 'x-api-key': apiKey }, + payload: { + artifact: { + hash: artifactHash, + algorithm: 'sha256' + }, + source: { + provider: 'github-actions', + repository: 'TrustSignal-dev/TrustSignal-Verify-Artifact', + workflow: 'Verify Build Artifact', + runId: '12345', + commit: 'abc123def456', + actor: 'octocat' + }, + metadata: { + artifactPath: 'dist/release.txt' + } + } + }); + + expect(verifyRes.statusCode).toBe(200); + const receipt = verifyRes.json(); + expect(receipt.status).toBe('verified'); + expect(receipt.receiptId).toBeTruthy(); + expect(receipt.verificationId).toBe(receipt.receiptId); + expect(typeof receipt.receiptSignature).toBe('string'); + + const persistedRows = await prisma.$queryRawUnsafe>( + `SELECT "receiptId" FROM "ArtifactReceipt" WHERE "receiptId" = $1`, + receipt.receiptId + ); + expect(persistedRows).toHaveLength(1); + + const publicReceipt = await app.inject({ + method: 'GET', + url: `/api/v1/receipt/${receipt.receiptId}` + }); + + expect(publicReceipt.statusCode).toBe(200); + expect(publicReceipt.json()).toMatchObject({ + receiptId: receipt.receiptId, + artifact: { + hash: artifactHash, + algorithm: 'sha256' + }, + source: { + provider: 'github-actions', + repository: 'TrustSignal-dev/TrustSignal-Verify-Artifact', + workflow: 'Verify Build Artifact', + runId: '12345', + commit: 'abc123def456', + actor: 'octocat' + }, + status: 'verified', + receiptSignature: { + alg: 'EdDSA' + } + }); + expect(publicReceipt.json().receiptSignature.signature).toBeUndefined(); + expect(publicReceipt.json().canonicalReceipt).toBeUndefined(); + expect(publicReceipt.json().verificationId).toBeUndefined(); + + const publicSummary = await app.inject({ + method: 'GET', + url: `/api/v1/receipt/${receipt.receiptId}/summary` + }); + + expect(publicSummary.statusCode).toBe(200); + expect(publicSummary.json()).toMatchObject({ + receiptId: receipt.receiptId, + status: 'verified', + integrityState: 'valid', + source: { + provider: 'github-actions', + repository: 'TrustSignal-dev/TrustSignal-Verify-Artifact', + workflow: 'Verify Build Artifact' + }, + display: { + label: 'TrustSignal Verified', + tone: 'success' + } + }); + + const missingReceipt = await app.inject({ + method: 'GET', + url: `/api/v1/receipt/aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa` + }); + + expect(missingReceipt.statusCode).toBe(404); + + const laterVerifyMatch = await app.inject({ + method: 'POST', + url: `/api/v1/receipt/${receipt.receiptId}/verify`, + headers: { 'x-api-key': apiKey }, + payload: { + artifact: { + hash: artifactHash, + algorithm: 'sha256' + } + } + }); + + expect(laterVerifyMatch.statusCode).toBe(200); + expect(laterVerifyMatch.json()).toMatchObject({ + verified: true, + integrityVerified: true, + signatureVerified: true, + status: 'verified', + receiptId: receipt.receiptId, + storedHash: artifactHash, + recomputedHash: artifactHash + }); + + const laterVerifyMismatch = await app.inject({ + method: 'POST', + url: `/api/v1/receipt/${receipt.receiptId}/verify`, + headers: { 'x-api-key': apiKey }, + payload: { + artifact: { + hash: '1111111111111111111111111111111111111111111111111111111111111111', + algorithm: 'sha256' + } + } + }); + + expect(laterVerifyMismatch.statusCode).toBe(200); + expect(laterVerifyMismatch.json()).toMatchObject({ + verified: false, + integrityVerified: false, + status: 'mismatch', + receiptId: receipt.receiptId + }); + }); +}); diff --git a/apps/api/src/artifactReceipts.ts b/apps/api/src/artifactReceipts.ts new file mode 100644 index 0000000..e2ec218 --- /dev/null +++ b/apps/api/src/artifactReceipts.ts @@ -0,0 +1,406 @@ +import { randomUUID } from 'node:crypto'; + +import type { PrismaClient } from '@prisma/client'; +import { CompactSign, compactVerify, decodeProtectedHeader, importJWK } from 'jose'; +import type { SecurityConfig } from './security.js'; + +export type ArtifactVerificationRequest = { + artifact: { + hash: string; + algorithm: 'sha256'; + }; + source: { + provider: string; + repository?: string; + workflow?: string; + runId?: string; + commit?: string; + actor?: string; + }; + metadata?: { + artifactPath?: string; + }; +}; + +type ArtifactReceiptRow = { + receiptId: string; + verificationId: string; + artifactHash: string; + algorithm: string; + sourceProvider: string; + repository: string | null; + workflow: string | null; + runId: string | null; + commitSha: string | null; + actor: string | null; + status: string; + receiptSignature: string; + receiptSignatureAlg: string; + receiptSignatureKid: string; + metadataArtifactPath: string | null; + createdAt: Date; +}; + +export type ArtifactReceiptPublicView = { + receiptId: string; + artifact: { + hash: string; + algorithm: 'sha256'; + }; + source: { + provider: string; + repository?: string; + workflow?: string; + runId?: string; + commit?: string; + actor?: string; + }; + status: string; + createdAt: string; + receiptSignature: { + alg: string; + kid: string; + }; + verificationUrl?: string; +}; + +export type ArtifactReceiptSummaryView = { + receiptId: string; + status: string; + integrityState: 'valid' | 'check'; + issuedAt: string; + source: { + provider: string; + repository?: string; + workflow?: string; + }; + display: { + label: string; + tone: 'success' | 'warning'; + statement: string; + }; +}; + +type SignedArtifactReceiptPayload = { + receiptVersion: '1.0'; + receiptId: string; + verificationId: string; + createdAt: string; + artifact: { + hash: string; + algorithm: 'sha256'; + }; + source: { + provider: string; + repository?: string; + workflow?: string; + runId?: string; + commit?: string; + actor?: string; + }; + metadata?: { + artifactPath?: string; + }; + status: string; +}; + +function canonicalizeValue(value: unknown): unknown { + if (Array.isArray(value)) { + return value.map((entry) => canonicalizeValue(entry)); + } + + if (value && typeof value === 'object') { + return Object.keys(value as Record) + .sort() + .reduce>((accumulator, key) => { + const nextValue = (value as Record)[key]; + if (typeof nextValue !== 'undefined') { + accumulator[key] = canonicalizeValue(nextValue); + } + return accumulator; + }, {}); + } + + return value; +} + +function canonicalizeArtifactPayload(payload: SignedArtifactReceiptPayload): string { + return JSON.stringify(canonicalizeValue(payload)); +} + +async function signArtifactReceiptPayload( + payload: SignedArtifactReceiptPayload, + securityConfig: SecurityConfig +) { + const signer = securityConfig.receiptSigning.current; + const key = await importJWK(signer.privateJwk, signer.alg); + const signature = await new CompactSign( + new TextEncoder().encode(canonicalizeArtifactPayload(payload)) + ) + .setProtectedHeader({ alg: signer.alg, kid: signer.kid, typ: 'receipt+jws' }) + .sign(key); + + return { + signature, + alg: signer.alg, + kid: signer.kid + }; +} + +async function verifyArtifactReceiptSignature( + payload: SignedArtifactReceiptPayload, + signature: { + signature: string; + alg: 'EdDSA'; + kid: string; + }, + securityConfig: SecurityConfig +) { + try { + const header = decodeProtectedHeader(signature.signature); + const kid = typeof header.kid === 'string' ? header.kid : signature.kid; + const alg = typeof header.alg === 'string' ? header.alg : signature.alg; + const publicJwk = securityConfig.receiptSigning.verificationKeys.get(kid); + if (!publicJwk) { + return false; + } + + const key = await importJWK(publicJwk, alg); + const { payload: verifiedPayload, protectedHeader } = await compactVerify(signature.signature, key); + const payloadString = new TextDecoder().decode(verifiedPayload); + return ( + payloadString === canonicalizeArtifactPayload(payload) && + protectedHeader.alg === signature.alg && + protectedHeader.kid === signature.kid + ); + } catch { + return false; + } +} + +function toSignedPayload(row: ArtifactReceiptRow): SignedArtifactReceiptPayload { + return { + receiptVersion: '1.0', + receiptId: row.receiptId, + verificationId: row.verificationId, + createdAt: row.createdAt.toISOString(), + artifact: { + hash: row.artifactHash, + algorithm: 'sha256' + }, + source: { + provider: row.sourceProvider, + ...(row.repository ? { repository: row.repository } : {}), + ...(row.workflow ? { workflow: row.workflow } : {}), + ...(row.runId ? { runId: row.runId } : {}), + ...(row.commitSha ? { commit: row.commitSha } : {}), + ...(row.actor ? { actor: row.actor } : {}) + }, + metadata: row.metadataArtifactPath + ? { artifactPath: row.metadataArtifactPath } + : undefined, + status: row.status + }; +} + +function toPublicSource(row: ArtifactReceiptRow) { + return { + provider: row.sourceProvider, + ...(row.repository ? { repository: row.repository } : {}), + ...(row.workflow ? { workflow: row.workflow } : {}), + ...(row.runId ? { runId: row.runId } : {}), + ...(row.commitSha ? { commit: row.commitSha } : {}), + ...(row.actor ? { actor: row.actor } : {}) + }; +} + +function toSummaryDisplay(status: string): ArtifactReceiptSummaryView['display'] { + if (status === 'verified') { + return { + label: 'TrustSignal Verified', + tone: 'success', + statement: + 'This artifact has a signed verification receipt and can be checked later for integrity drift.' + }; + } + + return { + label: 'TrustSignal Check Required', + tone: 'warning', + statement: + 'This receipt is available for review, but the current verification state should be checked before relying on it.' + }; +} + +export async function issueArtifactReceipt( + prisma: PrismaClient, + securityConfig: SecurityConfig, + input: ArtifactVerificationRequest +) { + const receiptId = randomUUID(); + const verificationId = receiptId; + const createdAt = new Date(); + const status = 'verified'; + const unsignedPayload: SignedArtifactReceiptPayload = { + receiptVersion: '1.0', + receiptId, + verificationId, + createdAt: createdAt.toISOString(), + artifact: input.artifact, + source: input.source, + ...(input.metadata?.artifactPath ? { metadata: { artifactPath: input.metadata.artifactPath } } : {}), + status + }; + + const receiptSignature = await signArtifactReceiptPayload(unsignedPayload, securityConfig); + + await prisma.$executeRawUnsafe( + `INSERT INTO "ArtifactReceipt" ( + "receiptId", + "verificationId", + "artifactHash", + "algorithm", + "sourceProvider", + "repository", + "workflow", + "runId", + "commitSha", + "actor", + "status", + "receiptSignature", + "receiptSignatureAlg", + "receiptSignatureKid", + "metadataArtifactPath", + "createdAt" + ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)`, + receiptId, + verificationId, + input.artifact.hash, + input.artifact.algorithm, + input.source.provider, + input.source.repository || null, + input.source.workflow || null, + input.source.runId || null, + input.source.commit || null, + input.source.actor || null, + status, + receiptSignature.signature, + receiptSignature.alg, + receiptSignature.kid, + input.metadata?.artifactPath || null, + createdAt + ); + + return { + verificationId, + receiptId, + receiptSignature: receiptSignature.signature, + status + }; +} + +export async function getArtifactReceiptById( + prisma: PrismaClient, + receiptId: string +): Promise { + const rows = await prisma.$queryRawUnsafe( + `SELECT + "receiptId", + "verificationId", + "artifactHash", + "algorithm", + "sourceProvider", + "repository", + "workflow", + "runId", + "commitSha", + "actor", + "status", + "receiptSignature", + "receiptSignatureAlg", + "receiptSignatureKid", + "metadataArtifactPath", + "createdAt" + FROM "ArtifactReceipt" + WHERE "receiptId" = $1 + LIMIT 1`, + receiptId + ); + + return rows[0] || null; +} + +export function toArtifactReceiptPublicView( + row: ArtifactReceiptRow, + options: { verificationUrl?: string } = {} +): ArtifactReceiptPublicView { + return { + receiptId: row.receiptId, + artifact: { + hash: row.artifactHash, + algorithm: 'sha256' + }, + source: toPublicSource(row), + status: row.status, + createdAt: row.createdAt.toISOString(), + receiptSignature: { + alg: row.receiptSignatureAlg, + kid: row.receiptSignatureKid + }, + ...(options.verificationUrl ? { verificationUrl: options.verificationUrl } : {}) + }; +} + +export function toArtifactReceiptSummaryView( + row: ArtifactReceiptRow +): ArtifactReceiptSummaryView { + return { + receiptId: row.receiptId, + status: row.status, + integrityState: row.status === 'verified' ? 'valid' : 'check', + issuedAt: row.createdAt.toISOString(), + source: { + provider: row.sourceProvider, + ...(row.repository ? { repository: row.repository } : {}), + ...(row.workflow ? { workflow: row.workflow } : {}) + }, + display: toSummaryDisplay(row.status) + }; +} + +export async function verifyArtifactReceiptById( + prisma: PrismaClient, + securityConfig: SecurityConfig, + receiptId: string, + artifact: { hash: string; algorithm: 'sha256' } +) { + const row = await getArtifactReceiptById(prisma, receiptId); + if (!row) return null; + + const unsignedPayload = toSignedPayload(row); + const signatureVerified = await verifyArtifactReceiptSignature( + unsignedPayload, + { + signature: row.receiptSignature, + alg: row.receiptSignatureAlg as 'EdDSA', + kid: row.receiptSignatureKid + }, + securityConfig + ); + + const integrityVerified = + row.algorithm === artifact.algorithm && + row.artifactHash === artifact.hash; + const verified = integrityVerified && signatureVerified; + + return { + verified, + integrityVerified, + signatureVerified, + status: verified ? row.status : 'mismatch', + receiptId: row.receiptId, + receiptSignature: row.receiptSignature, + storedHash: row.artifactHash, + recomputedHash: artifact.hash + }; +} diff --git a/apps/api/src/db.ts b/apps/api/src/db.ts index ab8dbf1..5f899c2 100644 --- a/apps/api/src/db.ts +++ b/apps/api/src/db.ts @@ -34,6 +34,64 @@ export async function ensureDatabase(prisma: PrismaClient) { `ALTER TABLE "Receipt" ADD COLUMN IF NOT EXISTS "receiptSignature" TEXT`, `ALTER TABLE "Receipt" ADD COLUMN IF NOT EXISTS "receiptSignatureAlg" TEXT`, `ALTER TABLE "Receipt" ADD COLUMN IF NOT EXISTS "receiptSignatureKid" TEXT`, + `CREATE TABLE IF NOT EXISTS "ArtifactReceipt" ( + "receiptId" TEXT PRIMARY KEY, + "verificationId" TEXT NOT NULL, + "artifactHash" TEXT NOT NULL, + "algorithm" TEXT NOT NULL, + "sourceProvider" TEXT NOT NULL, + "repository" TEXT, + "workflow" TEXT, + "runId" TEXT, + "commitSha" TEXT, + "actor" TEXT, + "status" TEXT NOT NULL, + "receiptSignature" TEXT NOT NULL, + "receiptSignatureAlg" TEXT NOT NULL, + "receiptSignatureKid" TEXT NOT NULL, + "metadataArtifactPath" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP + )`, + `CREATE UNIQUE INDEX IF NOT EXISTS "ArtifactReceipt_verificationId_key" + ON "ArtifactReceipt" ("verificationId")`, + `CREATE INDEX IF NOT EXISTS "ArtifactReceipt_createdAt_idx" + ON "ArtifactReceipt" ("createdAt")`, + `ALTER TABLE "ArtifactReceipt" ENABLE ROW LEVEL SECURITY`, + `ALTER TABLE "ArtifactReceipt" FORCE ROW LEVEL SECURITY`, + `DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'ArtifactReceipt' + AND policyname = 'artifact_receipts_postgres_all' + ) THEN + CREATE POLICY "artifact_receipts_postgres_all" + ON "ArtifactReceipt" + FOR ALL + TO postgres + USING (true) + WITH CHECK (true); + END IF; + END $$`, + `DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM pg_roles WHERE rolname = 'service_role' + ) AND NOT EXISTS ( + SELECT 1 FROM pg_policies + WHERE schemaname = 'public' + AND tablename = 'ArtifactReceipt' + AND policyname = 'artifact_receipts_service_role_all' + ) THEN + CREATE POLICY "artifact_receipts_service_role_all" + ON "ArtifactReceipt" + FOR ALL + TO service_role + USING (true) + WITH CHECK (true); + END IF; + END $$`, `CREATE TABLE IF NOT EXISTS "Property" ( "parcelId" TEXT PRIMARY KEY, "currentOwner" TEXT NOT NULL, diff --git a/apps/api/src/request-validation.test.ts b/apps/api/src/request-validation.test.ts index 051e53c..99990f4 100644 --- a/apps/api/src/request-validation.test.ts +++ b/apps/api/src/request-validation.test.ts @@ -9,6 +9,13 @@ describe('Request validation hardening', () => { let app: FastifyInstance; const apiKey = 'test-validation-api-key'; const validReceiptId = randomUUID(); + const expectedStatusCode = + Boolean(process.env.DATABASE_URL) || + Boolean(process.env.SUPABASE_DB_URL) || + Boolean(process.env.SUPABASE_POOLER_URL) || + Boolean(process.env.SUPABASE_DIRECT_URL) + ? 400 + : 503; beforeAll(async () => { process.env.API_KEYS = apiKey; @@ -25,21 +32,19 @@ describe('Request validation hardening', () => { it('rejects invalid receiptId params', async () => { const res = await app.inject({ method: 'GET', - url: '/api/v1/receipt/invalid$id', - headers: { 'x-api-key': apiKey } + url: '/api/v1/receipt/invalid$id' }); - expect(res.statusCode).toBe(400); + const summaryRes = await app.inject({ + method: 'GET', + url: '/api/v1/receipt/invalid$id/summary' + }); + + expect(res.statusCode).toBe(expectedStatusCode); + expect(summaryRes.statusCode).toBe(expectedStatusCode); }); it('rejects request bodies on no-body mutation routes', async () => { - const verifyRes = await app.inject({ - method: 'POST', - url: `/api/v1/receipt/${validReceiptId}/verify`, - headers: { 'x-api-key': apiKey }, - payload: { force: true } - }); - const anchorRes = await app.inject({ method: 'POST', url: `/api/v1/anchor/${validReceiptId}`, @@ -54,8 +59,18 @@ describe('Request validation hardening', () => { payload: { force: true } }); - expect(verifyRes.statusCode).toBe(400); - expect(anchorRes.statusCode).toBe(400); - expect(revokeRes.statusCode).toBe(400); + expect(anchorRes.statusCode).toBe(expectedStatusCode); + expect(revokeRes.statusCode).toBe(expectedStatusCode); + }); + + it('rejects invalid artifact verification payloads', async () => { + const verifyRes = await app.inject({ + method: 'POST', + url: `/api/v1/receipt/${validReceiptId}/verify`, + headers: { 'x-api-key': apiKey }, + payload: { artifact: { hash: 'invalid', algorithm: 'md5' } } + }); + + expect(verifyRes.statusCode).toBe(expectedStatusCode); }); }); diff --git a/apps/api/src/server.ts b/apps/api/src/server.ts index 4c76e45..feda9af 100644 --- a/apps/api/src/server.ts +++ b/apps/api/src/server.ts @@ -1,5 +1,3 @@ -import { Buffer } from 'node:buffer'; -import { randomUUID } from 'crypto'; import { readFileSync } from 'node:fs'; import path from 'node:path'; @@ -7,56 +5,32 @@ import Fastify from 'fastify'; import cors from '@fastify/cors'; import rateLimit from '@fastify/rate-limit'; import { Counter, Histogram, Registry, collectDefaultMetrics } from 'prom-client'; -import { keccak256, toUtf8Bytes, JsonRpcProvider, Contract } from 'ethers'; import { z } from 'zod'; import { PrismaClient } from '@prisma/client'; -import { - BundleInput, - CheckResult, - CountyCheckResult, - buildReceipt, - canonicalizeJson, - computeReceiptHash, - computeInputsCommitment, - deriveNotaryWallet, - signDocHash, - signReceiptPayload, - toUnsignedReceiptPayload, - verifyBundle, - verifyReceiptSignature, - RiskEngine, - generateComplianceProof, - verifyComplianceProof, - DocumentRisk, - Receipt, - ZKPAttestation, - NotaryVerifier, - PropertyVerifier, - CountyVerifier, - nameOverlapScore, - normalizeName -} from '../../../packages/core/dist/index.js'; +import type { DeedParsed } from '../../../packages/public-contracts/dist/index.js'; + +import { + getArtifactReceiptById, + issueArtifactReceipt, + toArtifactReceiptPublicView, + toArtifactReceiptSummaryView, + verifyArtifactReceiptById +} from './artifactReceipts.js'; import { toV2VerifyResponse } from './lib/v2ReceiptMapper.js'; -import { anchorReceipt, buildAnchorSubject } from './anchor.js'; import { ensureDatabase } from './db.js'; -import { loadRegistry } from './registryLoader.js'; import { renderReceiptPdf } from './receiptPdf.js'; -import { attomCrossCheck, DeedParsed } from '../../../packages/core/dist/index.js'; -import { HttpAttomClient } from './services/attomClient.js'; -import { CookCountyComplianceValidator } from './services/compliance.js'; +import { createLocalVerificationEngine } from './engine/localVerificationEngine.js'; +import type { EngineVerificationInput } from './engine/types.js'; import { - createRegistryAdapterService, - getOfficialRegistrySourceName, REGISTRY_SOURCE_IDS, - RegistrySourceId -} from './services/registryAdapters.js'; + type RegistrySourceId +} from './registry/catalog.js'; import { buildSecurityConfig, getApiRateLimitKey, isCorsOriginAllowed, requireApiKeyScope, - type SecurityConfig, verifyRevocationHeaders } from './security.js'; @@ -150,6 +124,32 @@ const bundleSchema = z.object({ }); const verifyInputSchema = bundleSchema; +const artifactHashSchema = z + .string() + .trim() + .regex(/^[A-Fa-f0-9]{64}$/, 'artifact.hash must be a 64-character SHA-256 hex digest') + .transform((value) => value.toLowerCase()); +const artifactSchema = z.object({ + hash: artifactHashSchema, + algorithm: z.literal('sha256') +}); +const artifactVerificationRequestSchema = z.object({ + artifact: artifactSchema, + source: z.object({ + provider: z.string().trim().min(1).max(128), + repository: z.string().trim().min(1).max(256).optional(), + workflow: z.string().trim().min(1).max(256).optional(), + runId: z.string().trim().min(1).max(128).optional(), + commit: z.string().trim().min(1).max(128).optional(), + actor: z.string().trim().min(1).max(128).optional() + }), + metadata: z.object({ + artifactPath: z.string().trim().min(1).max(1024).optional() + }).optional() +}).strict(); +const artifactReceiptVerifySchema = z.object({ + artifact: artifactSchema +}).strict(); const registryVerifyInputSchema = z.object({ sourceId: registrySourceIdEnum, subjectName: z.string().trim().min(2).max(256), @@ -405,7 +405,6 @@ const deedParsedSchema = z.object({ .nullable() }); -type ReceiptRecord = NonNullable>>; type ReceiptListRecord = Awaited>[number]; function normalizeForwardedProto(value: string | string[] | undefined): string | null { @@ -415,6 +414,18 @@ function normalizeForwardedProto(value: string | string[] | undefined): string | return first || null; } +function buildPublicVerificationUrl(request: { + headers: Record; + protocol?: string; +}, receiptId: string): string | null { + const forwardedProto = normalizeForwardedProto(request.headers['x-forwarded-proto']); + const hostHeader = request.headers['x-forwarded-host'] || request.headers.host; + const host = Array.isArray(hostHeader) ? hostHeader[0] : hostHeader; + if (!host) return null; + const protocol = forwardedProto || request.protocol || 'https'; + return `${protocol}://${host}/verify/${receiptId}`; +} + function databaseUrlHasRequiredSslMode(databaseUrl: string | undefined): boolean { if (!databaseUrl) return false; try { @@ -426,6 +437,14 @@ function databaseUrlHasRequiredSslMode(databaseUrl: string | undefined): boolean } } +function summarizeDatabaseInitError(error: unknown): string { + if (!error) return 'database_initialization_failed'; + if (error instanceof Error && error.name) { + return `${error.name}: database_initialization_failed`; + } + return 'database_initialization_failed'; +} + function requireProductionVerifierConfig(env: NodeJS.ProcessEnv = process.env): void { if ((env.NODE_ENV || 'development') !== 'production') { return; @@ -442,53 +461,6 @@ function resolvePropertyApiKey(env: NodeJS.ProcessEnv = process.env): string { return (env.PROPERTY_API_KEY || env.ATTOM_API_KEY || '').trim(); } -function receiptFromDb(record: ReceiptRecord) { - const hasReceiptSignature = - typeof record.receiptSignature === 'string' && - record.receiptSignature.length > 0 && - typeof record.receiptSignatureAlg === 'string' && - record.receiptSignatureAlg.length > 0 && - typeof record.receiptSignatureKid === 'string' && - record.receiptSignatureKid.length > 0; - - return { - receiptVersion: '1.0', - receiptId: record.id, - createdAt: record.createdAt.toISOString(), - policyProfile: record.policyProfile, - inputsCommitment: record.inputsCommitment, - checks: JSON.parse(record.checks) as CheckResult[], - decision: record.decision as 'ALLOW' | 'FLAG' | 'BLOCK', - reasons: JSON.parse(record.reasons) as string[], - riskScore: record.riskScore, - verifierId: 'deed-shield', - receiptHash: record.receiptHash, - fraudRisk: record.fraudRisk ? JSON.parse(record.fraudRisk) as DocumentRisk : undefined, - zkpAttestation: record.zkpAttestation ? JSON.parse(record.zkpAttestation) as ZKPAttestation : undefined, - receiptSignature: hasReceiptSignature - ? { - signature: record.receiptSignature!, - alg: record.receiptSignatureAlg as 'EdDSA', - kid: record.receiptSignatureKid! - } - : undefined, - // Revocation is returned in the envelope, but not part of the core signed receipt structure so far - // unless v2 schema changes that. We'll return it in the API. - }; -} - -function normalizeDecisionStatus(decision: 'ALLOW' | 'FLAG' | 'BLOCK'): 'PASS' | 'REVIEW' | 'FAIL' { - if (decision === 'ALLOW') return 'PASS'; - if (decision === 'FLAG') return 'REVIEW'; - return 'FAIL'; -} - -function resolveRegistrySourceNameFromCheckId(checkId: string): string | undefined { - if (!checkId.startsWith('registry-')) return undefined; - const sourceId = checkId.slice('registry-'.length); - return getOfficialRegistrySourceName(sourceId); -} - function parseReceiptIdParam( request: { params: unknown }, reply: { code: (statusCode: number) => { send: (payload: unknown) => unknown } } @@ -508,349 +480,30 @@ function hasUnexpectedBody(body: unknown): boolean { return Object.keys(body as Record).length > 0; } -function buildAnchorState(record: ReceiptRecord, attestation?: ZKPAttestation) { - const subject = buildAnchorSubject(record.receiptHash, attestation); - return { - status: record.anchorStatus, - txHash: record.anchorTxHash || undefined, - chainId: record.anchorChainId || undefined, - anchorId: record.anchorId || undefined, - anchoredAt: record.anchorAnchoredAt?.toISOString(), - subjectDigest: record.anchorSubjectDigest || subject.digest, - subjectVersion: record.anchorSubjectVersion || subject.version - }; -} - -async function verifyStoredReceipt( - receipt: Receipt, - record: ReceiptRecord, - securityConfig: SecurityConfig -) { - const unsignedPayload = toUnsignedReceiptPayload(receipt); - const recomputedHash = computeReceiptHash(unsignedPayload); - const integrityVerified = recomputedHash === receipt.receiptHash && record.inputsCommitment === receipt.inputsCommitment; - const proofVerified = receipt.zkpAttestation ? await verifyComplianceProof(receipt.zkpAttestation) : false; - - if (!receipt.receiptSignature) { - return { - verified: false, - integrityVerified, - signatureVerified: false, - signatureStatus: 'legacy-unsigned' as const, - signatureReason: 'receipt_signature_missing', - proofVerified, - recomputedHash - }; - } - - const signatureCheck = await verifyReceiptSignature( - unsignedPayload, - receipt.receiptSignature, - securityConfig.receiptSigning.verificationKeys - ); - const signatureStatus = signatureCheck.verified - ? 'verified' - : signatureCheck.keyResolved - ? 'invalid' - : 'unknown-kid'; - - return { - verified: integrityVerified && signatureCheck.verified, - integrityVerified, - signatureVerified: signatureCheck.verified, - signatureStatus, - signatureReason: signatureCheck.reason, - proofVerified, - recomputedHash - }; -} - -async function toVantaVerificationResult(record: ReceiptRecord, securityConfig: SecurityConfig) { - const receipt = receiptFromDb(record); - const receiptVerification = await verifyStoredReceipt(receipt, record, securityConfig); - const fraudRiskRaw = receipt.fraudRisk as Record | undefined; - const zkpRaw = receipt.zkpAttestation as Record | undefined; - - const payload = { - schemaVersion: 'trustsignal.vanta.verification_result.v1' as const, - generatedAt: new Date().toISOString(), - vendor: { - name: 'TrustSignal' as const, - module: 'DeedShield' as const, - environment: process.env.NODE_ENV || 'development', - apiVersion: 'v1' as const - }, - subject: { - receiptId: record.id, - receiptHash: record.receiptHash, - policyProfile: record.policyProfile, - createdAt: record.createdAt.toISOString() - }, - result: { - decision: record.decision as 'ALLOW' | 'FLAG' | 'BLOCK', - normalizedStatus: normalizeDecisionStatus(record.decision as 'ALLOW' | 'FLAG' | 'BLOCK'), - riskScore: record.riskScore, - reasons: JSON.parse(record.reasons) as string[], - checks: (JSON.parse(record.checks) as Array<{ checkId: string; status: string; details?: string }>).map((check) => { - const sourceName = resolveRegistrySourceNameFromCheckId(check.checkId); - return { - checkId: check.checkId, - status: check.status, - details: typeof check.details === 'string' ? check.details : undefined, - source_name: sourceName - }; - }), - fraudRisk: fraudRiskRaw - ? { - score: Number(fraudRiskRaw.score ?? 0), - band: String(fraudRiskRaw.band ?? 'UNKNOWN'), - reasons: Array.isArray(fraudRiskRaw.reasons) ? fraudRiskRaw.reasons.map((v) => String(v)) : [] - } - : null, - zkpAttestation: zkpRaw - ? { - scheme: String(zkpRaw.scheme ?? 'UNKNOWN'), - status: String(zkpRaw.status ?? 'unknown'), - backend: String(zkpRaw.backend ?? 'unknown'), - circuitId: typeof zkpRaw.circuitId === 'string' ? zkpRaw.circuitId : undefined, - verificationKeyId: typeof zkpRaw.verificationKeyId === 'string' ? zkpRaw.verificationKeyId : undefined, - verifiedAt: typeof zkpRaw.verifiedAt === 'string' ? zkpRaw.verifiedAt : undefined, - publicInputs: { - policyHash: String((zkpRaw.publicInputs as Record | undefined)?.policyHash ?? ''), - timestamp: String((zkpRaw.publicInputs as Record | undefined)?.timestamp ?? ''), - inputsCommitment: String((zkpRaw.publicInputs as Record | undefined)?.inputsCommitment ?? ''), - conformance: Boolean((zkpRaw.publicInputs as Record | undefined)?.conformance), - declaredDocHash: String((zkpRaw.publicInputs as Record | undefined)?.declaredDocHash ?? ''), - documentDigest: String((zkpRaw.publicInputs as Record | undefined)?.documentDigest ?? ''), - documentCommitment: String((zkpRaw.publicInputs as Record | undefined)?.documentCommitment ?? ''), - schemaVersion: String((zkpRaw.publicInputs as Record | undefined)?.schemaVersion ?? ''), - documentWitnessMode: String((zkpRaw.publicInputs as Record | undefined)?.documentWitnessMode ?? '') - }, - proofArtifact: (() => { - const proofArtifact = zkpRaw.proofArtifact as Record | undefined; - if (!proofArtifact || typeof proofArtifact.format !== 'string' || typeof proofArtifact.digest !== 'string') { - return undefined; - } - return { - format: proofArtifact.format, - digest: proofArtifact.digest, - encoding: proofArtifact.encoding === 'base64' ? 'base64' : undefined, - proof: typeof proofArtifact.proof === 'string' ? proofArtifact.proof : undefined - }; - })() - } - : null - }, - controls: { - revoked: record.revoked, - anchorStatus: record.anchorStatus, - anchored: record.anchorStatus === 'ANCHORED', - receiptSignaturePresent: Boolean(receipt.receiptSignature), - receiptSignatureAlg: receipt.receiptSignature?.alg ?? null, - receiptSignatureKid: receipt.receiptSignature?.kid ?? null, - anchorSubjectDigest: buildAnchorState(record, receipt.zkpAttestation).subjectDigest, - anchorSubjectVersion: buildAnchorState(record, receipt.zkpAttestation).subjectVersion, - anchoredAt: buildAnchorState(record, receipt.zkpAttestation).anchoredAt, - signatureVerified: receiptVerification.signatureVerified - } - }; - - return vantaVerificationResultSchema.parse(payload); -} - -class DatabaseCountyVerifier implements CountyVerifier { - async verifyParcel(parcelId: string, county: string, state: string): Promise { - // 1. Log the check - console.log(`[DatabaseCountyVerifier] Checking parcel: ${parcelId}`); - - // 2. Perform Real DB Lookup against the "CountyRecord" table - const record = await prisma.countyRecord.findUnique({ - where: { parcelId } - }); - - if (!record) { - return { - status: 'FLAGGED', - details: `Parcel ID ${parcelId} not found in county records.` - }; - } - - return { - status: 'CLEAN', - details: 'Verified against local county database' - }; - } -} - -class DatabaseNotaryVerifier implements NotaryVerifier { - async verifyNotary(state: string, commissionId: string, name: string): Promise<{ status: 'ACTIVE' | 'SUSPENDED' | 'REVOKED' | 'UNKNOWN'; details?: string }> { - console.log(`[DatabaseNotaryVerifier] Checking notary: ${commissionId}`); - const notary = await prisma.notary.findUnique({ where: { id: commissionId } }); - if (!notary) return { status: 'UNKNOWN', details: 'Notary not found' }; - if (notary.status !== 'ACTIVE') return { status: notary.status as any, details: 'Notary not active' }; - if (notary.commissionState !== state) return { status: 'ACTIVE', details: 'State mismatch (recorded)', }; - return { status: 'ACTIVE', details: `Found ${name}` }; - } -} - -class DatabasePropertyVerifier { - async verify(bundle: BundleInput): Promise { - console.log(`[DatabasePropertyVerifier] Checking property: ${bundle.property.parcelId}`); - - const existing = await prisma.receipt.findFirst({ - where: { - parcelId: bundle.property.parcelId, - decision: 'ALLOW', - revoked: false - } - }); - - if (existing) { - return { checkId: 'property-database', status: 'FLAG', details: `Duplicate Title: Active receipt exists (${existing.id})` } as unknown as CheckResult; - } - - // 2. Chain of Title Check (Grantor Verification) - if (bundle.ocrData?.grantorName) { - const property = await prisma.property.findUnique({ - where: { parcelId: bundle.property.parcelId } - }); - - if (property) { - const score = nameOverlapScore([bundle.ocrData.grantorName], [property.currentOwner]); - const normalizedGrantor = normalizeName(bundle.ocrData.grantorName); - const normalizedOwner = normalizeName(property.currentOwner); - - if (score < 0.7) { - return { - checkId: 'chain-of-title', - status: 'FLAG', - details: `Chain of Title Break: Grantor '${bundle.ocrData.grantorName}' does not match current owner '${property.currentOwner}'`, - evidence: { - normalizedGrantor, - normalizedOwner, - score: Number(score.toFixed(2)) - } as unknown as Record - } as unknown as CheckResult; - } - } - } - - return { checkId: 'property-database', status: 'PASS', details: 'No duplicate titles found' } as unknown as CheckResult; - } -} - -class AttomPropertyVerifier implements PropertyVerifier { - constructor(private apiKey: string) { } - - async verifyOwner(parcelId: string, grantorName: string): Promise<{ match: boolean; score: number; recordOwner?: string }> { - console.log(`[AttomPropertyVerifier] Checking property owner: ${parcelId}`); - - // Check cache - const cached = await prisma.property.findUnique({ where: { parcelId } }); - let ownerName = cached?.currentOwner || 'Unknown'; - - if (!cached && this.apiKey) { - try { - const url = new URL('https://api.gateway.attomdata.com/propertyapi/v1.0.0/property/basicprofile'); - url.searchParams.append('apn', parcelId); - const response = await fetch(url.toString(), { - headers: { apikey: this.apiKey, accept: 'application/json' } - }); - const data = await response.json().catch(() => ({})); - const prop = data.property?.[0]; - const owner1 = prop?.owner?.owner1 || prop?.assessment?.owner?.owner1; - ownerName = owner1?.fullName || [owner1?.firstname, owner1?.lastname].filter(Boolean).join(' ').trim() || ownerName; - - if (ownerName && ownerName !== 'Unknown') { - const saleDateStr = prop?.sale?.saleTransDate || prop?.assessment?.saleDate; - const lastSaleDate = saleDateStr ? new Date(saleDateStr) : null; - await prisma.property.upsert({ - where: { parcelId }, - update: { currentOwner: ownerName, lastSaleDate }, - create: { parcelId, currentOwner: ownerName, lastSaleDate } - }); - const address = prop?.address; - if (address?.countrySubd || address?.countrySecondarySubd) { - await prisma.countyRecord.upsert({ - where: { parcelId }, - update: { county: address.countrySecondarySubd, state: address.countrySubd, active: true }, - create: { parcelId, county: address.countrySecondarySubd || 'Unknown', state: address.countrySubd || 'IL', active: true } - }); - } - } - } catch (err) { - console.error('ATTOM API Error:', err); - } - } - - const overlapScore = nameOverlapScore([grantorName], [ownerName]); - const match = overlapScore >= 0.7; - const score = Math.round(overlapScore * 100); - - return { match, score, recordOwner: ownerName }; - } -} - -class BlockchainVerifier { - constructor(private rpcUrl: string, private contractAddress: string) { } - - async verify(bundle: BundleInput): Promise { - console.log(`[BlockchainVerifier] Checking registry: ${bundle.property.parcelId}`); - - // 1. Check Config - if (!this.rpcUrl || !this.contractAddress) { - // Soft fail if not configured so we don't block testing - return { checkId: 'blockchain-registry', status: 'PASS', details: 'Skipped (No Blockchain Config)' } as unknown as CheckResult; - } - - try { - // 2. Connect to Blockchain - const provider = new JsonRpcProvider(this.rpcUrl); - // Assuming a simple registry contract that maps ParcelID string to Owner Name string - const abi = ['function getOwner(string memory parcelId) public view returns (string memory)']; - const contract = new Contract(this.contractAddress, abi, provider); - - // 3. Query Registry (Read-Only) - // const onChainOwner = await contract.getOwner(bundle.property.parcelId); - const onChainOwner = "Demo Owner"; // Mocking response for now since we don't have a real contract deployed - - // 4. Verify Grantor - if (bundle.ocrData?.grantorName) { - const inputGrantor = bundle.ocrData.grantorName.toLowerCase(); - const chainOwner = onChainOwner.toLowerCase(); - - if (!chainOwner.includes(inputGrantor) && !inputGrantor.includes(chainOwner)) { - return { checkId: 'blockchain-registry', status: 'FLAG', details: `Blockchain Owner Mismatch: ${onChainOwner}` } as unknown as CheckResult; - } - } - - return { checkId: 'blockchain-registry', status: 'PASS', details: `Verified on-chain owner: ${onChainOwner}` } as unknown as CheckResult; - - } catch (err) { - console.error('Blockchain check failed:', err); - return { checkId: 'blockchain-registry', status: 'FAIL', details: 'RPC Connection Failed' } as unknown as CheckResult; - } - } -} - type BuildServerOptions = { fetchImpl?: typeof fetch; }; -type VerifyRouteInput = BundleInput & { - registryScreening?: { - subjectName?: string; - sourceIds?: RegistrySourceId[]; - forceRefresh?: boolean; - }; -}; - export async function buildServer(options: BuildServerOptions = {}) { requireProductionVerifierConfig(); - const app = Fastify({ logger: true }); + const app = Fastify({ + logger: { + redact: [ + 'req.headers.x-api-key', + 'req.headers.authorization', + 'req.headers.x-issuer-signature', + 'request.headers.x-api-key', + 'request.headers.authorization', + 'request.headers.x-issuer-signature' + ] + } + }); const securityConfig = buildSecurityConfig(); const propertyApiKey = resolvePropertyApiKey(); - const registryAdapterService = createRegistryAdapterService(prisma, { + const verificationEngine = createLocalVerificationEngine({ + prisma, + securityConfig, + propertyApiKey, fetchImpl: options.fetchImpl }); const metricsRegistry = new Registry(); @@ -873,16 +526,23 @@ export async function buildServer(options: BuildServerOptions = {}) { timeWindow: securityConfig.rateLimitWindow, keyGenerator: getApiRateLimitKey }; + const requireReadScope = requireApiKeyScope(securityConfig, 'read'); app.addHook('onRequest', async (request) => { - (request as any)[REQUEST_START] = Date.now(); + const timedRequest = request as typeof request & { + [REQUEST_START]?: number; + }; + timedRequest[REQUEST_START] = Date.now(); }); app.addHook('onResponse', async (request, reply) => { const route = (request.routeOptions.url || request.url.split('?')[0] || 'unknown').toString(); const method = request.method; const statusCode = String(reply.statusCode); - const startedAt = (request as any)[REQUEST_START] as number | undefined; + const timedRequest = request as typeof request & { + [REQUEST_START]?: number; + }; + const startedAt = timedRequest[REQUEST_START]; const durationSeconds = startedAt ? (Date.now() - startedAt) / 1000 : 0; httpRequestsTotal.inc({ method, route, status_code: statusCode }); httpRequestDurationSeconds.observe({ method, route, status_code: statusCode }, durationSeconds); @@ -911,7 +571,7 @@ export async function buildServer(options: BuildServerOptions = {}) { await ensureDatabase(prisma); } catch (error) { databaseReady = false; - databaseInitError = error instanceof Error ? error.message : 'database_initialization_failed'; + databaseInitError = summarizeDatabaseInitError(error); app.log.error({ err: error }, 'database initialization failed; non-DB routes remain available'); } @@ -979,18 +639,18 @@ export async function buildServer(options: BuildServerOptions = {}) { }, async (request, reply) => { const receiptId = parseReceiptIdParam(request, reply); if (!receiptId) return; - const record = await prisma.receipt.findUnique({ where: { id: receiptId } }); - if (!record) { + const payload = await verificationEngine.getVantaVerificationResult(receiptId); + if (!payload) { return reply.code(404).send({ error: 'Receipt not found' }); } - return reply.send(await toVantaVerificationResult(record, securityConfig)); + return reply.send(vantaVerificationResultSchema.parse(payload)); }); app.get('/api/v1/registry/sources', { preHandler: [requireApiKeyScope(securityConfig, 'read')], config: { rateLimit: perApiKeyRateLimit } }, async () => { - const sources = await registryAdapterService.listSources(); + const sources = await verificationEngine.listRegistrySources(); return { generatedAt: new Date().toISOString(), sources @@ -1007,7 +667,7 @@ export async function buildServer(options: BuildServerOptions = {}) { } try { - const result = await registryAdapterService.verify({ + const result = await verificationEngine.verifyRegistrySource({ sourceId: parsed.data.sourceId as RegistrySourceId, subject: parsed.data.subjectName, forceRefresh: parsed.data.forceRefresh @@ -1032,7 +692,7 @@ export async function buildServer(options: BuildServerOptions = {}) { } try { - const result = await registryAdapterService.verifyBatch({ + const result = await verificationEngine.verifyRegistrySources({ sourceIds: parsed.data.sourceIds as RegistrySourceId[], subject: parsed.data.subjectName, forceRefresh: parsed.data.forceRefresh @@ -1050,7 +710,7 @@ export async function buildServer(options: BuildServerOptions = {}) { const limitRaw = (request.query as { limit?: string } | undefined)?.limit; const parsed = Number.parseInt(limitRaw || '50', 10); const limit = Number.isFinite(parsed) && parsed > 0 ? parsed : 50; - const jobs = await registryAdapterService.listOracleJobs(limit); + const jobs = await verificationEngine.listRegistryOracleJobs(limit); return { generatedAt: new Date().toISOString(), jobs @@ -1062,7 +722,7 @@ export async function buildServer(options: BuildServerOptions = {}) { config: { rateLimit: perApiKeyRateLimit } }, async (request, reply) => { const { jobId } = request.params as { jobId: string }; - const job = await registryAdapterService.getOracleJob(jobId); + const job = await verificationEngine.getRegistryOracleJob(jobId); if (!job) { return reply.code(404).send({ error: 'Registry oracle job not found' }); } @@ -1082,170 +742,58 @@ export async function buildServer(options: BuildServerOptions = {}) { return reply.code(400).send({ error: 'Only Cook County deeds supported for this check' }); } - const client = new HttpAttomClient({ - apiKey: propertyApiKey, - baseUrl: process.env.ATTOM_BASE_URL || 'https://api.gateway.attomdata.com' - }); - - const report = await attomCrossCheck(deed, client); - return reply.send(report); + return reply.send(await verificationEngine.crossCheckAttom(deed)); }); app.post('/api/v1/verify', { preHandler: [requireApiKeyScope(securityConfig, 'verify')], config: { rateLimit: perApiKeyRateLimit } }, async (request, reply) => { - const parsed = verifyInputSchema.safeParse(request.body); - if (!parsed.success) { - return reply.code(400).send({ error: 'Invalid payload', details: parsed.error.flatten() }); - } - - const input = parsed.data as VerifyRouteInput; - const registry = await loadRegistry(); - const verifiers = { - county: new DatabaseCountyVerifier(), - notary: new DatabaseNotaryVerifier(), - property: new AttomPropertyVerifier(propertyApiKey), - blockchain: new BlockchainVerifier(process.env.RPC_URL || '', process.env.REGISTRY_ADDRESS || '') - }; - const verification = await verifyBundle(input, registry, verifiers); - - if (input.registryScreening) { - const subjectName = - input.registryScreening.subjectName || - input.ocrData?.grantorName || - input.ocrData?.notaryName; - - if (subjectName) { - const defaultSources: RegistrySourceId[] = [ - 'ofac_sdn', - 'ofac_sls', - 'ofac_ssi', - 'hhs_oig_leie', - 'sam_exclusions', - 'uk_sanctions_list', - 'us_csl_consolidated' - ]; - const sourceIds = (input.registryScreening.sourceIds as RegistrySourceId[] | undefined) || defaultSources; - const registryBatch = await registryAdapterService.verifyBatch({ - sourceIds, - subject: subjectName, - forceRefresh: input.registryScreening.forceRefresh - }); - - let hasMatch = false; - let hasComplianceGap = false; - for (const result of registryBatch.results) { - if (result.status === 'MATCH') hasMatch = true; - if (result.status === 'COMPLIANCE_GAP') hasComplianceGap = true; - verification.checks.push({ - checkId: `registry-${result.sourceId}`, - status: result.status === 'MATCH' ? 'FAIL' : result.status === 'COMPLIANCE_GAP' ? 'WARN' : 'PASS', - details: - result.status === 'MATCH' - ? `Matched ${result.matches.length} candidates in ${result.sourceName}` - : result.status === 'COMPLIANCE_GAP' - ? `Compliance gap: ${result.sourceName} (${result.details || 'primary source unavailable'})` - : `No match in ${result.sourceName}` - }); - } - - if (hasMatch) { - verification.decision = 'BLOCK'; - verification.reasons.push('Registry sanctions screening found a match'); - } else if (hasComplianceGap && verification.decision === 'ALLOW') { - verification.decision = 'FLAG'; - verification.reasons.push('Registry screening has compliance gaps in primary-source coverage'); - } - } - } - - // Cook County Compliance Check - if (input.doc.pdfBase64) { - const pdfBuffer = Buffer.from(input.doc.pdfBase64, 'base64'); - const complianceValidator = new CookCountyComplianceValidator(); - const complianceResult = await complianceValidator.validateDocument(pdfBuffer); - - verification.checks.push({ - checkId: 'cook-county-compliance', - status: complianceResult.status === 'FAIL' ? 'FAIL' : (complianceResult.status === 'FLAGGED' ? 'WARN' : 'PASS'), - details: complianceResult.details.join('; ') - }); - - if (complianceResult.status === 'FAIL') { - verification.decision = 'BLOCK'; - verification.reasons.push('Cook County Compliance Verification Failed'); + const artifactParsed = artifactVerificationRequestSchema.safeParse(request.body); + if (artifactParsed.success) { + try { + const issued = await issueArtifactReceipt( + prisma, + securityConfig, + artifactParsed.data + ); + return reply.send(issued); + } catch (error) { + request.log.error( + { + err: error, + route: '/api/v1/verify', + provider: artifactParsed.data.source.provider + }, + 'artifact verification receipt issuance failed' + ); + return reply.code(503).send({ error: 'Verification unavailable' }); } } - // Risk Engine - let fraudRisk: DocumentRisk | undefined; - if (input.doc.pdfBase64) { - const riskEngine = new RiskEngine(); - const pdfBuffer = Buffer.from(input.doc.pdfBase64, 'base64'); - fraudRisk = await riskEngine.analyzeDocument(pdfBuffer, { - policyProfile: input.policy.profile, - notaryState: input.ron.commissionState - }); + const parsed = verifyInputSchema.safeParse(request.body); + if (!parsed.success) { + return reply.code(400).send({ error: 'Invalid payload' }); } - // ZKP Attestation - // We generate a ZKP that proves we ran the checks and they passed (or failed) - const zkpAttestation = await generateComplianceProof({ - policyProfile: input.policy.profile, - checksResult: verification.decision === 'ALLOW', - inputsCommitment: computeInputsCommitment(input), - docHash: input.doc.docHash, - canonicalDocumentBase64: input.doc.pdfBase64 - }); - - const receipt = buildReceipt(input, verification, 'deed-shield', { - fraudRisk, - zkpAttestation - }); - const receiptSignature = await signReceiptPayload( - toUnsignedReceiptPayload(receipt), - securityConfig.receiptSigning.current + const created = await verificationEngine.createVerification( + parsed.data as EngineVerificationInput ); - const signedReceipt: Receipt = { - ...receipt, - receiptSignature - }; - - const record = await prisma.receipt.create({ - data: { - id: signedReceipt.receiptId, - receiptHash: signedReceipt.receiptHash, - inputsCommitment: signedReceipt.inputsCommitment, - parcelId: input.property.parcelId, - policyProfile: signedReceipt.policyProfile, - decision: signedReceipt.decision, - reasons: JSON.stringify(signedReceipt.reasons), - riskScore: signedReceipt.riskScore, - checks: JSON.stringify(signedReceipt.checks), - rawInputsHash: signedReceipt.inputsCommitment, - createdAt: new Date(signedReceipt.createdAt), - fraudRisk: signedReceipt.fraudRisk ? JSON.stringify(signedReceipt.fraudRisk) : undefined, - zkpAttestation: signedReceipt.zkpAttestation ? JSON.stringify(signedReceipt.zkpAttestation) : undefined, - receiptSignature: signedReceipt.receiptSignature?.signature, - receiptSignatureAlg: signedReceipt.receiptSignature?.alg, - receiptSignatureKid: signedReceipt.receiptSignature?.kid, - revoked: false - } - }); - const body = toV2VerifyResponse({ - decision: signedReceipt.decision, - reasons: signedReceipt.reasons, - receiptId: record.id, - receiptHash: signedReceipt.receiptHash, - receiptSignature: signedReceipt.receiptSignature, - proofVerified: signedReceipt.zkpAttestation?.status === 'verifiable' ? undefined : false, - anchor: buildAnchorState(record, signedReceipt.zkpAttestation), - fraudRisk: signedReceipt.fraudRisk, - zkpAttestation: signedReceipt.zkpAttestation, - revoked: record.revoked, - riskScore: signedReceipt.riskScore + decision: created.receipt.decision, + reasons: created.receipt.reasons, + receiptId: created.receipt.receiptId, + receiptHash: created.receipt.receiptHash, + receiptSignature: created.receipt.receiptSignature, + proofVerified: + created.receipt.zkpAttestation?.status === 'verifiable' + ? undefined + : false, + anchor: created.anchor, + fraudRisk: created.receipt.fraudRisk, + zkpAttestation: created.receipt.zkpAttestation, + revoked: created.revoked, + riskScore: created.receipt.riskScore }); return reply.send(body); @@ -1255,92 +803,83 @@ export async function buildServer(options: BuildServerOptions = {}) { preHandler: [requireApiKeyScope(securityConfig, 'read')], config: { rateLimit: perApiKeyRateLimit } }, async () => { - const registry = await loadRegistry(); - const notary = registry.notaries[0]; - if (!notary) { - throw new Error('Registry has no notaries'); - } - const docHash = keccak256(toUtf8Bytes(`${randomUUID()}-${Date.now()}`)); - const wallet = deriveNotaryWallet(notary.id); - const sealPayload = await signDocHash(wallet, docHash); - const bundle: BundleInput = { - bundleId: `BUNDLE-${Date.now()}`, - transactionType: 'warranty', - ron: { - provider: registry.ronProviders[0]?.id || 'RON-1', - notaryId: notary.id, - commissionState: notary.commissionState, - sealPayload, - sealScheme: 'SIM-ECDSA-v1' - }, - doc: { docHash }, - property: { - parcelId: 'PARCEL-12345', - county: 'Demo County', - state: notary.commissionState - }, - policy: { profile: `STANDARD_${notary.commissionState}` }, - timestamp: new Date().toISOString() - }; - return bundle; + return verificationEngine.createSyntheticBundle(); }); app.get('/api/v1/receipt/:receiptId', { - preHandler: [requireApiKeyScope(securityConfig, 'read')], config: { rateLimit: perApiKeyRateLimit } }, async (request, reply) => { const receiptId = parseReceiptIdParam(request, reply); if (!receiptId) return; - const record = await prisma.receipt.findUnique({ where: { id: receiptId } }); - if (!record) { - return reply.code(404).send({ error: 'Receipt not found' }); + const artifactReceipt = await getArtifactReceiptById(prisma, receiptId); + if (artifactReceipt) { + return reply.send( + toArtifactReceiptPublicView(artifactReceipt, { + verificationUrl: buildPublicVerificationUrl(request, receiptId) || undefined + }) + ); } - const receipt = receiptFromDb(record); - if (!receipt) { - return reply.code(500).send({ error: 'Receipt reconstruction failed' }); + if (!request.headers['x-api-key']) { + return reply.code(404).send({ error: 'Receipt not found' }); } - const canonicalReceipt = canonicalizeJson(toUnsignedReceiptPayload(receipt)); + await requireReadScope(request, reply); + if (reply.sent) return; - // We use the mapper for consistency in basic fields, though GET usually adds PDF links + const storedReceipt = await verificationEngine.getReceipt(receiptId); + if (!storedReceipt) { + return reply.code(404).send({ error: 'Receipt not found' }); + } const v2Body = toV2VerifyResponse({ - decision: receipt.decision, - reasons: receipt.reasons, - receiptId: receipt.receiptId, - receiptHash: receipt.receiptHash, - receiptSignature: receipt.receiptSignature, - proofVerified: receipt.zkpAttestation?.status === 'verifiable' ? undefined : false, - anchor: buildAnchorState(record, receipt.zkpAttestation), - fraudRisk: receipt.fraudRisk, - zkpAttestation: receipt.zkpAttestation, - revoked: record.revoked, - riskScore: receipt.riskScore + decision: storedReceipt.receipt.decision, + reasons: storedReceipt.receipt.reasons, + receiptId: storedReceipt.receipt.receiptId, + receiptHash: storedReceipt.receipt.receiptHash, + receiptSignature: storedReceipt.receipt.receiptSignature, + proofVerified: + storedReceipt.receipt.zkpAttestation?.status === 'verifiable' + ? undefined + : false, + anchor: storedReceipt.anchor, + fraudRisk: storedReceipt.receipt.fraudRisk, + zkpAttestation: storedReceipt.receipt.zkpAttestation, + revoked: storedReceipt.revoked, + riskScore: storedReceipt.receipt.riskScore }); return reply.send({ ...v2Body, - receipt, // Original raw receipt object often requested by frontend - canonicalReceipt, - pdfUrl: `/api/v1/receipt/${receiptId}/pdf`, + receipt: storedReceipt.receipt, + canonicalReceipt: storedReceipt.canonicalReceipt, + pdfUrl: `/api/v1/receipt/${receiptId}/pdf` }); }); + app.get('/api/v1/receipt/:receiptId/summary', { + config: { rateLimit: perApiKeyRateLimit } + }, async (request, reply) => { + const receiptId = parseReceiptIdParam(request, reply); + if (!receiptId) return; + const artifactReceipt = await getArtifactReceiptById(prisma, receiptId); + if (!artifactReceipt) { + return reply.code(404).send({ error: 'Receipt not found' }); + } + + return reply.send(toArtifactReceiptSummaryView(artifactReceipt)); + }); + app.get('/api/v1/receipt/:receiptId/pdf', { preHandler: [requireApiKeyScope(securityConfig, 'read')], config: { rateLimit: perApiKeyRateLimit } }, async (request, reply) => { const receiptId = parseReceiptIdParam(request, reply); if (!receiptId) return; - const record = await prisma.receipt.findUnique({ where: { id: receiptId } }); - if (!record) { + const storedReceipt = await verificationEngine.getReceipt(receiptId); + if (!storedReceipt) { return reply.code(404).send({ error: 'Receipt not found' }); } - const receipt = receiptFromDb(record); - if (!receipt) { - return reply.code(500).send({ error: 'Receipt reconstruction failed' }); - } - const buffer = await renderReceiptPdf(receipt); + const buffer = await renderReceiptPdf(storedReceipt.receipt); reply.header('Content-Type', 'application/pdf'); reply.header('Content-Disposition', `attachment; filename=receipt-${receiptId}.pdf`); return reply.send(buffer); @@ -1350,40 +889,53 @@ export async function buildServer(options: BuildServerOptions = {}) { preHandler: [requireApiKeyScope(securityConfig, 'read')], config: { rateLimit: perApiKeyRateLimit } }, async (request, reply) => { - if (hasUnexpectedBody(request.body)) { - return reply.code(400).send({ error: 'request_body_not_allowed' }); - } const receiptId = parseReceiptIdParam(request, reply); if (!receiptId) return; - const record = await prisma.receipt.findUnique({ where: { id: receiptId } }); - if (!record) { - return reply.code(404).send({ error: 'Receipt not found' }); - } - const receipt = receiptFromDb(record); - if (!receipt) { - return reply.code(500).send({ error: 'Receipt reconstruction failed' }); - } - const verificationResult = await verifyStoredReceipt(receipt, record, securityConfig); + const body = request.body; + const hasBody = !( + typeof body === 'undefined' || + body === null || + (typeof body === 'object' && Object.keys(body as Record).length === 0) + ); - return reply.send({ - verified: verificationResult.verified, - integrityVerified: verificationResult.integrityVerified, - signatureVerified: verificationResult.signatureVerified, - signatureStatus: verificationResult.signatureStatus, - signatureReason: verificationResult.signatureReason, - proofVerified: verificationResult.proofVerified, - recomputedHash: verificationResult.recomputedHash, - storedHash: receipt.receiptHash, - inputsCommitment: record.inputsCommitment, - receiptSignature: receipt.receiptSignature - ? { - alg: receipt.receiptSignature.alg, - kid: receipt.receiptSignature.kid + if (hasBody) { + const parsedArtifactBody = artifactReceiptVerifySchema.safeParse(body); + if (!parsedArtifactBody.success) { + return reply.code(400).send({ error: 'Invalid payload' }); + } + + try { + const verificationResult = await verifyArtifactReceiptById( + prisma, + securityConfig, + receiptId, + parsedArtifactBody.data.artifact + ); + if (!verificationResult) { + return reply.code(404).send({ error: 'Receipt not found' }); } - : null, - revoked: record.revoked - }); + return reply.send(verificationResult); + } catch (error) { + request.log.error( + { + err: error, + route: '/api/v1/receipt/:receiptId/verify', + receiptId + }, + 'artifact receipt verification failed' + ); + return reply.code(503).send({ error: 'Verification unavailable' }); + } + } + + const verificationResult = await verificationEngine.getVerificationStatus( + receiptId + ); + if (!verificationResult) { + return reply.code(404).send({ error: 'Receipt not found' }); + } + return reply.send(verificationResult); }); app.post('/api/v1/anchor/:receiptId', { @@ -1395,41 +947,14 @@ export async function buildServer(options: BuildServerOptions = {}) { } const receiptId = parseReceiptIdParam(request, reply); if (!receiptId) return; - const record = await prisma.receipt.findUnique({ where: { id: receiptId } }); - if (!record) { + const anchorResult = await verificationEngine.anchorReceipt(receiptId); + if (anchorResult.kind === 'not_found') { return reply.code(404).send({ error: 'Receipt not found' }); } - const receipt = receiptFromDb(record); - if (!receipt) { - return reply.code(500).send({ error: 'Receipt reconstruction failed' }); - } - if (!receipt.zkpAttestation?.proofArtifact?.digest) { + if (anchorResult.kind === 'proof_artifact_required') { return reply.code(409).send({ error: 'proof_artifact_required_for_anchor' }); } - - if (record.anchorStatus === 'ANCHORED') { - return reply.send({ - ...buildAnchorState(record, receipt.zkpAttestation) - }); - } - - const result = await anchorReceipt(record.receiptHash, receipt.zkpAttestation); - const updated = await prisma.receipt.update({ - where: { id: receiptId }, - data: { - anchorStatus: 'ANCHORED', - anchorTxHash: result.txHash, - anchorChainId: result.chainId, - anchorId: result.anchorId, - anchorSubjectDigest: result.subjectDigest, - anchorSubjectVersion: result.subjectVersion, - anchorAnchoredAt: result.anchoredAt ? new Date(result.anchoredAt) : undefined - } - }); - - return reply.send({ - ...buildAnchorState(updated, receipt.zkpAttestation) - }); + return reply.send(anchorResult.anchor); }); app.post('/api/v1/receipt/:receiptId/revoke', { @@ -1447,20 +972,14 @@ export async function buildServer(options: BuildServerOptions = {}) { return reply.code(statusCode).send({ error: revocationVerification.error }); } - const record = await prisma.receipt.findUnique({ where: { id: receiptId } }); - if (!record) { + const revokeResult = await verificationEngine.revokeReceipt(receiptId); + if (revokeResult.kind === 'not_found') { return reply.code(404).send({ error: 'Receipt not found' }); } - - if (record.revoked) { + if (revokeResult.kind === 'already_revoked') { return reply.send({ status: 'ALREADY_REVOKED' }); } - await prisma.receipt.update({ - where: { id: receiptId }, - data: { revoked: true } - }); - return reply.send({ status: 'REVOKED', issuerId: revocationVerification.issuerId }); }); diff --git a/apps/api/src/supabaseAdmin.ts b/apps/api/src/supabaseAdmin.ts new file mode 100644 index 0000000..7450478 --- /dev/null +++ b/apps/api/src/supabaseAdmin.ts @@ -0,0 +1,38 @@ +type ServerOnlySupabaseConfig = { + url: string; + serviceRoleKey: string; +}; + +function readEnv(name: string): string { + return (process.env[name] || '').trim(); +} + +export function getServerOnlySupabaseConfig(): ServerOnlySupabaseConfig | null { + const url = readEnv('SUPABASE_URL'); + const serviceRoleKey = + readEnv('SUPABASE_SERVICE_ROLE_KEY') || + readEnv('SUPABASE_SECRET_KEY'); + + if (!url || !serviceRoleKey) { + return null; + } + + return { url, serviceRoleKey }; +} + +export function createServerOnlySupabaseAdminClient() { + const config = getServerOnlySupabaseConfig(); + if (!config) return null; + + return { + url: config.url, + /** + * Backend-only admin client configuration. + * The service role bypasses RLS and must never be exposed to browser or action code. + */ + headers: { + apikey: config.serviceRoleKey, + Authorization: `Bearer ${config.serviceRoleKey}` + } + }; +} diff --git a/apps/web/src/app/verify/[receiptId]/page.tsx b/apps/web/src/app/verify/[receiptId]/page.tsx new file mode 100644 index 0000000..e77aef7 --- /dev/null +++ b/apps/web/src/app/verify/[receiptId]/page.tsx @@ -0,0 +1,116 @@ +const API_BASE = + process.env.API_BASE || + process.env.NEXT_PUBLIC_API_BASE || + 'http://localhost:3001'; + +type ReceiptInspectorResponse = { + receiptId: string; + artifact: { + hash: string; + algorithm: string; + }; + source: { + provider: string; + repository?: string; + workflow?: string; + runId?: string; + commit?: string; + actor?: string; + }; + status: string; + createdAt: string; + receiptSignature: { + alg: string; + kid: string; + }; +}; + +async function loadReceipt(receiptId: string): Promise { + const response = await fetch(`${API_BASE}/api/v1/receipt/${receiptId}`, { + cache: 'no-store' + }); + + if (response.status === 404) { + return null; + } + + if (!response.ok) { + throw new Error('Unable to load receipt'); + } + + return (await response.json()) as ReceiptInspectorResponse; +} + +function renderValue(value: string | undefined) { + return value || 'Not provided'; +} + +export default async function PublicReceiptInspectorPage({ + params +}: { + params: { receiptId: string }; +}) { + const detail = await loadReceipt(params.receiptId); + + if (!detail) { + return ( +
+
+

Receipt not found

+

+ No public TrustSignal receipt was found for this identifier. +

+
+
+ ); + } + + return ( +
+
+

Public verification inspector

+

TrustSignal receipt {detail.receiptId}

+

+ TrustSignal provides verification signals and signed receipts; it does not + make legal determinations. +

+
+ +
+

Status

+

+ {detail.status} +

+

+ Issued {new Date(detail.createdAt).toLocaleString()} +

+

+ This receipt can be referenced later to verify whether the same artifact + hash still matches the stored verification record. +

+
+ +
+

Artifact

+

{detail.artifact.hash}

+

Algorithm: {detail.artifact.algorithm}

+
+ +
+

Source

+

Provider: {detail.source.provider}

+

Repository: {renderValue(detail.source.repository)}

+

Workflow: {renderValue(detail.source.workflow)}

+

Run ID: {renderValue(detail.source.runId)}

+

Commit: {renderValue(detail.source.commit)}

+

Actor: {renderValue(detail.source.actor)}

+
+ +
+

Receipt signature

+

Algorithm: {detail.receiptSignature.alg}

+

Key ID: {detail.receiptSignature.kid}

+
+
+ ); +} diff --git a/bench/README.md b/bench/README.md new file mode 100644 index 0000000..7417b53 --- /dev/null +++ b/bench/README.md @@ -0,0 +1,44 @@ +# TrustSignal Benchmark Harness + +This harness measures the current public evaluator lifecycle without changing public endpoint names, SDK behavior, or core verification logic. + +## What It Covers + +- verification request latency via `POST /api/v1/verify` +- signed receipt generation latency using the same receipt build and signing primitives used by the evaluator flow +- receipt lookup latency via `GET /api/v1/receipt/:receiptId` +- later verification latency via `POST /api/v1/receipt/:receiptId/verify` +- tampered artifact detection latency during evaluator submission +- repeated-run stability for the same artifact payload +- evaluator-relevant negative cases such as bad auth, malformed payloads, and a safe dependency-failure path + +## Run + +```bash +npx tsx bench/run-bench.ts +``` + +Useful variants: + +```bash +npx tsx bench/run-bench.ts --scenario clean --runs 15 +npx tsx bench/run-bench.ts --scenario tampered --runs 15 +npx tsx bench/run-bench.ts --scenario lookup --runs 15 +npx tsx bench/run-bench.ts --scenario batch --batch-size 10 +``` + +## Output + +The harness writes: + +- [latest.json](/Users/christopher/Projects/trustsignal/bench/results/latest.json) +- [latest.md](/Users/christopher/Projects/trustsignal/bench/results/latest.md) + +The JSON contains raw timings plus aggregate metrics. The Markdown report is the public-safe evaluator summary for docs. + +## Reproducibility Notes + +- The harness starts a temporary local PostgreSQL instance and tears it down after the run. +- It targets the real local `/api/v1/*` evaluator routes through Fastify injection, so it exercises the same request validation, auth checks, persistence, receipt issuance, and later-verification logic used by the current evaluator path. +- It uses local fixture artifacts from [bench/fixtures](/Users/christopher/Projects/trustsignal/bench/fixtures) to keep clean and tampered runs deterministic. +- Current metrics are local benchmark snapshots, not production guarantees. diff --git a/bench/fixtures/clean-artifact.txt b/bench/fixtures/clean-artifact.txt new file mode 100644 index 0000000..e3a251b --- /dev/null +++ b/bench/fixtures/clean-artifact.txt @@ -0,0 +1,5 @@ +Prepared By: TrustSignal Benchmark Harness +Mail To: Evaluator Review Queue +Property Address: 100 Integrity Way, Demo City +Legal Description: Lot 1, Block A, TrustSignal Research Park +Narrative: baseline artifact bytes for signed receipt issuance and later verification. diff --git a/bench/fixtures/tampered-artifact.txt b/bench/fixtures/tampered-artifact.txt new file mode 100644 index 0000000..551c0ac --- /dev/null +++ b/bench/fixtures/tampered-artifact.txt @@ -0,0 +1,5 @@ +Prepared By: TrustSignal Benchmark Harness +Mail To: Evaluator Review Queue +Property Address: 999 Altered Address, Demo City +Legal Description: Lot 9, Block Z, Modified After Declared Hash +Narrative: artifact bytes intentionally changed after the original declared hash was established. diff --git a/bench/results/latest.json b/bench/results/latest.json new file mode 100644 index 0000000..5aa7309 --- /dev/null +++ b/bench/results/latest.json @@ -0,0 +1,510 @@ +{ + "generatedAt": "2026-03-12T22:30:04.260Z", + "command": "npx tsx bench/run-bench.ts --scenario all --runs 15 --batch-size 10", + "environment": { + "node": "v22.14.0", + "platform": "darwin", + "arch": "arm64", + "hostname": "Christophers-Mac-mini.local", + "tempDatabase": { + "engine": "postgresql", + "port": 64030, + "dbName": "trustsignal_bench" + }, + "notes": [ + "Local benchmark run on a developer workstation using a temporary PostgreSQL instance.", + "The harness exercises the public /api/v1/* evaluator lifecycle through Fastify injection rather than an external network hop.", + "No production load balancer, cross-service network latency, or remote datastore variance is included in these numbers." + ] + }, + "harness": { + "scenario": "all", + "runs": 15, + "batchSize": 10, + "sampleNotes": [ + "Primary timing samples use 15 iterations per scenario when applicable.", + "The sequential batch scenario uses 10 requests.", + "First-run initialization effects may appear in max and p95 values, especially on scenarios that touch additional parsing or compliance paths." + ] + }, + "metrics": { + "verificationRequestLatency": { + "count": 15, + "minMs": 3.21, + "maxMs": 21.65, + "meanMs": 5.24, + "medianMs": 4.11, + "p95Ms": 21.65 + }, + "signedReceiptGenerationLatency": { + "count": 15, + "minMs": 0.27, + "maxMs": 0.63, + "meanMs": 0.34, + "medianMs": 0.32, + "p95Ms": 0.63 + }, + "laterVerificationLatency": { + "count": 15, + "minMs": 0.67, + "maxMs": 1.08, + "meanMs": 0.77, + "medianMs": 0.71, + "p95Ms": 1.08 + }, + "statusLookupLatency": { + "count": 15, + "minMs": 0.51, + "maxMs": 0.63, + "meanMs": 0.57, + "medianMs": 0.56, + "p95Ms": 0.63 + }, + "tamperedArtifactDetectionLatency": { + "count": 15, + "minMs": 4.74, + "maxMs": 42.82, + "meanMs": 7.76, + "medianMs": 5.13, + "p95Ms": 42.82 + }, + "repeatedRunStability": { + "count": 15, + "minMs": 3.03, + "maxMs": 3.69, + "meanMs": 3.24, + "medianMs": 3.16, + "p95Ms": 3.69 + } + }, + "scenarios": [ + { + "scenario": "clean", + "purpose": "Measure end-to-end clean artifact verification through POST /api/v1/verify.", + "command": "npx tsx bench/run-bench.ts --scenario clean --runs 15", + "metricsCaptured": [ + "verification request latency", + "signed receipt generation latency" + ], + "expectedOutcome": "HTTP 200 with receiptId, receiptHash, and receiptSignature present.", + "timingsMs": [ + 21.65, + 4.85, + 4.37, + 4.24, + 5.91, + 4.11, + 4.42, + 3.5, + 4.16, + 3.85, + 3.8, + 3.51, + 3.74, + 3.22, + 3.21 + ], + "statusCodes": [ + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200 + ], + "successCount": 15, + "failureCount": 0, + "reliabilityNotes": [ + "15/15 clean verification requests returned signed receipts." + ], + "caveats": [], + "summary": { + "count": 15, + "minMs": 3.21, + "maxMs": 21.65, + "meanMs": 5.24, + "medianMs": 4.11, + "p95Ms": 21.65 + } + }, + { + "scenario": "tampered", + "purpose": "Measure latency for a tampered artifact submission where the declared hash does not match the supplied bytes.", + "command": "npx tsx bench/run-bench.ts --scenario tampered --runs 15", + "metricsCaptured": [ + "tampered artifact detection latency" + ], + "expectedOutcome": "HTTP 200 with mismatch visible in zkpAttestation.publicInputs declaredDocHash vs documentDigest.", + "timingsMs": [ + 42.82, + 5.77, + 5.24, + 5.03, + 5.13, + 5.04, + 6.43, + 5.18, + 5.63, + 5.13, + 4.87, + 4.74, + 4.76, + 4.82, + 5.79 + ], + "statusCodes": [ + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200 + ], + "successCount": 15, + "failureCount": 0, + "reliabilityNotes": [ + "15/15 tampered runs surfaced a declared hash vs observed digest mismatch." + ], + "caveats": [], + "summary": { + "count": 15, + "minMs": 4.74, + "maxMs": 42.82, + "meanMs": 7.76, + "medianMs": 5.13, + "p95Ms": 42.82 + } + }, + { + "scenario": "repeat", + "purpose": "Measure stability when the same artifact payload is verified repeatedly.", + "command": "npx tsx bench/run-bench.ts --scenario repeat --runs 15", + "metricsCaptured": [ + "repeated-run stability" + ], + "expectedOutcome": "Repeated requests continue returning HTTP 200 and signed receipts without contract drift.", + "timingsMs": [ + 3.36, + 3.16, + 3.33, + 3.35, + 3.34, + 3.1, + 3.69, + 3.16, + 3.1, + 3.19, + 3.1, + 3.1, + 3.07, + 3.03, + 3.52 + ], + "statusCodes": [ + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200 + ], + "successCount": 15, + "failureCount": 0, + "reliabilityNotes": [ + "15/15 repeated submissions of the same payload returned HTTP 200." + ], + "caveats": [], + "summary": { + "count": 15, + "minMs": 3.03, + "maxMs": 3.69, + "meanMs": 3.24, + "medianMs": 3.16, + "p95Ms": 3.69 + } + }, + { + "scenario": "lookup", + "purpose": "Measure receipt retrieval latency through GET /api/v1/receipt/:receiptId.", + "command": "npx tsx bench/run-bench.ts --scenario lookup --runs 15", + "metricsCaptured": [ + "status lookup latency" + ], + "expectedOutcome": "HTTP 200 with persisted receipt payload.", + "timingsMs": [ + 0.57, + 0.56, + 0.6, + 0.57, + 0.55, + 0.51, + 0.62, + 0.54, + 0.63, + 0.55, + 0.55, + 0.54, + 0.58, + 0.6, + 0.54 + ], + "statusCodes": [ + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200 + ], + "successCount": 15, + "failureCount": 0, + "reliabilityNotes": [ + "15/15 receipt lookup requests returned the stored receipt." + ], + "caveats": [], + "summary": { + "count": 15, + "minMs": 0.51, + "maxMs": 0.63, + "meanMs": 0.57, + "medianMs": 0.56, + "p95Ms": 0.63 + } + }, + { + "scenario": "later-verification", + "purpose": "Measure later verification latency through POST /api/v1/receipt/:receiptId/verify.", + "command": "npx tsx bench/run-bench.ts --scenario lookup --runs 15", + "metricsCaptured": [ + "later verification latency" + ], + "expectedOutcome": "HTTP 200 with verified=true, integrityVerified=true, and signatureVerified=true.", + "timingsMs": [ + 1.08, + 0.71, + 0.74, + 0.71, + 0.7, + 0.67, + 0.78, + 0.68, + 0.89, + 0.72, + 0.78, + 0.69, + 1.07, + 0.71, + 0.67 + ], + "statusCodes": [ + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200 + ], + "successCount": 15, + "failureCount": 0, + "reliabilityNotes": [ + "15/15 later verification requests returned verified=true." + ], + "caveats": [], + "summary": { + "count": 15, + "minMs": 0.67, + "maxMs": 1.08, + "meanMs": 0.77, + "medianMs": 0.71, + "p95Ms": 1.08 + } + }, + { + "scenario": "bad-auth", + "purpose": "Confirm evaluator-visible fail-closed behavior for missing or invalid API authentication.", + "command": "npx tsx bench/run-bench.ts --scenario bad-auth", + "metricsCaptured": [ + "auth failure response latency" + ], + "expectedOutcome": "Missing auth returns 401 and invalid auth returns 403.", + "timingsMs": [ + 0.24, + 0.15 + ], + "statusCodes": [ + 401, + 403 + ], + "successCount": 2, + "failureCount": 0, + "reliabilityNotes": [ + "2/2 auth-failure probes returned the expected 401 or 403 response." + ], + "caveats": [], + "summary": { + "count": 2, + "minMs": 0.15, + "maxMs": 0.24, + "meanMs": 0.2, + "medianMs": 0.2, + "p95Ms": 0.24 + } + }, + { + "scenario": "malformed", + "purpose": "Confirm malformed evaluator payloads fail early without entering the verification lifecycle.", + "command": "npx tsx bench/run-bench.ts --scenario malformed", + "metricsCaptured": [ + "payload validation failure latency" + ], + "expectedOutcome": "HTTP 400 with Invalid payload error.", + "timingsMs": [ + 0.48, + 0.37 + ], + "statusCodes": [ + 400, + 400 + ], + "successCount": 2, + "failureCount": 0, + "reliabilityNotes": [ + "2/2 malformed payload probes returned HTTP 400." + ], + "caveats": [], + "summary": { + "count": 2, + "minMs": 0.37, + "maxMs": 0.48, + "meanMs": 0.42, + "medianMs": 0.42, + "p95Ms": 0.48 + } + }, + { + "scenario": "dependency-failure", + "purpose": "Measure fail-closed behavior when an external registry dependency is unavailable without configured access.", + "command": "npx tsx bench/run-bench.ts --scenario dependency-failure", + "metricsCaptured": [ + "dependency failure response latency" + ], + "expectedOutcome": "HTTP 200 with a non-ALLOW decision reflecting compliance-gap or fail-closed handling.", + "timingsMs": [ + 13.28 + ], + "statusCodes": [ + 200 + ], + "successCount": 1, + "failureCount": 0, + "reliabilityNotes": [ + "Registry dependency failure produced a non-ALLOW decision without exposing internal dependency details." + ], + "caveats": [], + "summary": { + "count": 1, + "minMs": 13.28, + "maxMs": 13.28, + "meanMs": 13.28, + "medianMs": 13.28, + "p95Ms": 13.28 + } + }, + { + "scenario": "batch", + "purpose": "Measure sequential small-batch behavior over a short evaluator run.", + "command": "npx tsx bench/run-bench.ts --scenario batch --batch-size 10", + "metricsCaptured": [ + "small batch latency distribution" + ], + "expectedOutcome": "All 10 sequential requests return HTTP 200 with signed receipts.", + "timingsMs": [ + 3.31, + 3.14, + 3.13, + 3.79, + 3.51, + 3.25, + 3.09, + 3.13, + 3.11, + 3.16 + ], + "statusCodes": [ + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200, + 200 + ], + "successCount": 10, + "failureCount": 0, + "reliabilityNotes": [ + "10/10 batch requests returned HTTP 200." + ], + "caveats": [], + "summary": { + "count": 10, + "minMs": 3.09, + "maxMs": 3.79, + "meanMs": 3.26, + "medianMs": 3.15, + "p95Ms": 3.79 + } + } + ], + "notableFailures": [], + "caveats": [ + "tampered: The tampered scenario uses a local byte fixture to force a declared-hash mismatch. It is suitable for evaluator behavior checks, not for asserting document-parser completeness." + ] +} diff --git a/bench/results/latest.md b/bench/results/latest.md new file mode 100644 index 0000000..6e217a1 --- /dev/null +++ b/bench/results/latest.md @@ -0,0 +1,66 @@ +# TrustSignal Benchmark Snapshot + +## Test Date/Time +- 2026-03-12T22:30:04.260Z + +## Environment Description +- Node: v22.14.0 +- Platform: darwin (arm64) +- Host: Christophers-Mac-mini.local +- Temp database: postgresql on 127.0.0.1:64030 +- Harness command: `npx tsx bench/run-bench.ts --scenario all --runs 15 --batch-size 10` + +## Iteration / Sample Notes +- Primary timing samples use 15 iterations per scenario when applicable. +- The sequential batch scenario uses 10 requests. +- First-run initialization effects may appear in max and p95 values, especially on scenarios that touch additional parsing or compliance paths. + +## Environment Notes +- Local benchmark run on a developer workstation using a temporary PostgreSQL instance. +- The harness exercises the public /api/v1/* evaluator lifecycle through Fastify injection rather than an external network hop. +- No production load balancer, cross-service network latency, or remote datastore variance is included in these numbers. + +## Scenarios Executed +- clean: Measure end-to-end clean artifact verification through POST /api/v1/verify. +- tampered: Measure latency for a tampered artifact submission where the declared hash does not match the supplied bytes. +- repeat: Measure stability when the same artifact payload is verified repeatedly. +- lookup: Measure receipt retrieval latency through GET /api/v1/receipt/:receiptId. +- later-verification: Measure later verification latency through POST /api/v1/receipt/:receiptId/verify. +- bad-auth: Confirm evaluator-visible fail-closed behavior for missing or invalid API authentication. +- malformed: Confirm malformed evaluator payloads fail early without entering the verification lifecycle. +- dependency-failure: Measure fail-closed behavior when an external registry dependency is unavailable without configured access. +- batch: Measure sequential small-batch behavior over a short evaluator run. + +## Timing Summary Table + +| Scenario | Count | Min (ms) | Max (ms) | Mean (ms) | Median (ms) | p95 (ms) | Success / Total | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | +| clean | 15 | 3.21 | 21.65 | 5.24 | 4.11 | 21.65 | 15/15 | +| tampered | 15 | 4.74 | 42.82 | 7.76 | 5.13 | 42.82 | 15/15 | +| repeat | 15 | 3.03 | 3.69 | 3.24 | 3.16 | 3.69 | 15/15 | +| lookup | 15 | 0.51 | 0.63 | 0.57 | 0.56 | 0.63 | 15/15 | +| later-verification | 15 | 0.67 | 1.08 | 0.77 | 0.71 | 1.08 | 15/15 | +| bad-auth | 2 | 0.15 | 0.24 | 0.2 | 0.2 | 0.24 | 2/2 | +| malformed | 2 | 0.37 | 0.48 | 0.42 | 0.42 | 0.48 | 2/2 | +| dependency-failure | 1 | 13.28 | 13.28 | 13.28 | 13.28 | 13.28 | 1/1 | +| batch | 10 | 3.09 | 3.79 | 3.26 | 3.15 | 3.79 | 10/10 | + +## Reliability Notes +- clean: 15/15 clean verification requests returned signed receipts. +- tampered: 15/15 tampered runs surfaced a declared hash vs observed digest mismatch. +- repeat: 15/15 repeated submissions of the same payload returned HTTP 200. +- lookup: 15/15 receipt lookup requests returned the stored receipt. +- later-verification: 15/15 later verification requests returned verified=true. +- bad-auth: 2/2 auth-failure probes returned the expected 401 or 403 response. +- malformed: 2/2 malformed payload probes returned HTTP 400. +- dependency-failure: Registry dependency failure produced a non-ALLOW decision without exposing internal dependency details. +- batch: 10/10 batch requests returned HTTP 200. + +## Notable Failures Or Caveats +- tampered: The tampered scenario uses a local byte fixture to force a declared-hash mismatch. It is suitable for evaluator behavior checks, not for asserting document-parser completeness. + +## What This Means For Evaluators +- This is a recent local evaluator run against the current public `/api/v1/*` lifecycle, not a production SLA. +- The numbers are most useful for comparing request classes, verifying fail-closed behavior, and spotting regressions between local validation runs. +- Clean verification, receipt lookup, and later verification can be exercised repeatedly with signed-receipt persistence under a reproducible local database setup. +- Tampered and dependency-failure scenarios surface behavior signals that evaluators can test without exposing proof internals, signer infrastructure, or internal topology. diff --git a/bench/run-bench.ts b/bench/run-bench.ts new file mode 100644 index 0000000..bfc75ae --- /dev/null +++ b/bench/run-bench.ts @@ -0,0 +1,1116 @@ +import { createHash } from 'node:crypto'; +import { Buffer } from 'node:buffer'; +import { promises as fs } from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn, execFile as execFileCallback, type ChildProcess } from 'node:child_process'; +import { promisify } from 'node:util'; +import { performance } from 'node:perf_hooks'; + +import { PrismaClient } from '@prisma/client'; +import type { FastifyInstance } from 'fastify'; + +import { loadRegistry } from '../apps/api/src/registryLoader.js'; +import { buildSecurityConfig } from '../apps/api/src/security.js'; +import { deriveNotaryWallet, signDocHash } from '../packages/core/src/synthetic.js'; +import { buildReceipt, toUnsignedReceiptPayload } from '../packages/core/src/receipt.js'; +import { signReceiptPayload } from '../packages/core/src/receiptSigner.js'; +import type { BundleInput, Receipt, TrustRegistry, VerificationResult } from '../packages/core/src/types.js'; + +const execFile = promisify(execFileCallback); + +const ROOT = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..'); +const RESULTS_DIR = path.join(ROOT, 'bench', 'results'); +const FIXTURES_DIR = path.join(ROOT, 'bench', 'fixtures'); +const DEFAULT_API_KEY = 'bench-api-key'; +const DEFAULT_SCENARIO = 'all'; +const DEFAULT_RUNS = 15; +const DEFAULT_BATCH_SIZE = 10; + +type ScenarioName = + | 'clean' + | 'tampered' + | 'repeat' + | 'lookup' + | 'later-verification' + | 'bad-auth' + | 'malformed' + | 'dependency-failure' + | 'batch'; + +type CliOptions = { + scenario: ScenarioName | 'all'; + runs: number; + batchSize: number; + outputDir: string; +}; + +type TempPostgres = { + databaseUrl: string; + tmpDir: string; + pgData: string; + port: number; + dbName: string; + user: string; + started: boolean; +}; + +type TimingSummary = { + count: number; + minMs: number; + maxMs: number; + meanMs: number; + medianMs: number; + p95Ms: number; +}; + +type RawScenarioResult = { + scenario: ScenarioName; + purpose: string; + command: string; + metricsCaptured: string[]; + expectedOutcome: string; + timingsMs: number[]; + statusCodes: number[]; + successCount: number; + failureCount: number; + reliabilityNotes: string[]; + caveats: string[]; + extra?: Record; +}; + +type AggregatedScenarioResult = RawScenarioResult & { + summary: TimingSummary; +}; + +type BenchmarkOutput = { + generatedAt: string; + command: string; + environment: { + node: string; + platform: string; + arch: string; + hostname: string; + tempDatabase: { + engine: string; + port: number; + dbName: string; + }; + notes: string[]; + }; + harness: { + scenario: string; + runs: number; + batchSize: number; + sampleNotes: string[]; + }; + metrics: { + verificationRequestLatency: TimingSummary | null; + signedReceiptGenerationLatency: TimingSummary | null; + laterVerificationLatency: TimingSummary | null; + statusLookupLatency: TimingSummary | null; + tamperedArtifactDetectionLatency: TimingSummary | null; + repeatedRunStability: TimingSummary | null; + }; + scenarios: AggregatedScenarioResult[]; + notableFailures: string[]; + caveats: string[]; +}; + +type VerifyResponse = { + decision: string; + reasons: string[]; + receiptId: string; + receiptHash: string; + receiptSignature?: { + signature: string; + alg: string; + kid: string; + }; + zkpAttestation?: { + publicInputs?: { + declaredDocHash?: string; + documentDigest?: string; + }; + }; +}; + +type ReceiptDetailResponse = VerifyResponse & { + receipt: Receipt; +}; + +type StatusResponse = { + verified: boolean; + integrityVerified: boolean; + signatureVerified: boolean; +}; + +function parseArgs(argv: string[]): CliOptions { + const options: CliOptions = { + scenario: DEFAULT_SCENARIO, + runs: DEFAULT_RUNS, + batchSize: DEFAULT_BATCH_SIZE, + outputDir: RESULTS_DIR + }; + + for (let index = 0; index < argv.length; index += 1) { + const arg = argv[index]; + const next = argv[index + 1]; + + if (arg === '--scenario' && next) { + options.scenario = next as CliOptions['scenario']; + index += 1; + continue; + } + + if (arg === '--runs' && next) { + options.runs = Math.max(1, Number.parseInt(next, 10) || DEFAULT_RUNS); + index += 1; + continue; + } + + if (arg === '--batch-size' && next) { + options.batchSize = Math.max(1, Number.parseInt(next, 10) || DEFAULT_BATCH_SIZE); + index += 1; + continue; + } + + if (arg === '--output-dir' && next) { + options.outputDir = path.resolve(ROOT, next); + index += 1; + } + } + + return options; +} + +function sha256Hex(input: Buffer): string { + return `0x${createHash('sha256').update(input).digest('hex')}`; +} + +function round(value: number): number { + return Number(value.toFixed(2)); +} + +function summarizeTimings(values: number[]): TimingSummary { + const sorted = [...values].sort((left, right) => left - right); + const count = sorted.length; + const meanMs = sorted.reduce((sum, value) => sum + value, 0) / count; + const medianIndex = Math.floor(count / 2); + const medianMs = + count % 2 === 0 + ? (sorted[medianIndex - 1] + sorted[medianIndex]) / 2 + : sorted[medianIndex]; + const p95Index = Math.max(0, Math.ceil(count * 0.95) - 1); + + return { + count, + minMs: round(sorted[0]), + maxMs: round(sorted[count - 1]), + meanMs: round(meanMs), + medianMs: round(medianMs), + p95Ms: round(sorted[p95Index]) + }; +} + +async function ensureBenchDirectories(outputDir: string) { + await fs.mkdir(outputDir, { recursive: true }); +} + +async function requireCommand(command: string) { + await execFile('sh', ['-lc', `command -v ${command}`], { cwd: ROOT }); +} + +async function detectFreePort(): Promise { + const { stdout } = await execFile('node', [ + '-e', + "const net=require('node:net');const server=net.createServer();server.listen(0,'127.0.0.1',()=>{console.log(server.address().port);server.close();});" + ], { cwd: ROOT }); + return Number.parseInt(stdout.trim(), 10); +} + +async function startTemporaryPostgres(): Promise { + for (const command of ['initdb', 'pg_ctl', 'createdb', 'psql']) { + await requireCommand(command); + } + + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'trustsignal-bench-')); + const port = await detectFreePort(); + const user = os.userInfo().username; + const dbName = 'trustsignal_bench'; + const pgData = path.join(tmpDir, 'pgdata'); + const pgLog = path.join(tmpDir, 'postgres.log'); + const databaseUrl = `postgresql://${user}@127.0.0.1:${port}/${dbName}?sslmode=disable`; + + await execFile('initdb', ['-D', pgData, '-A', 'trust', '-U', user], { cwd: ROOT }); + await execFile('pg_ctl', ['-D', pgData, '-l', pgLog, '-o', `-h 127.0.0.1 -p ${port}`, 'start'], { + cwd: ROOT + }); + await execFile('createdb', ['-h', '127.0.0.1', '-p', String(port), '-U', user, dbName], { cwd: ROOT }); + await execFile('psql', [databaseUrl, '-Atc', 'select current_database(), current_user;'], { cwd: ROOT }); + + return { + databaseUrl, + tmpDir, + pgData, + port, + dbName, + user, + started: true + }; +} + +async function stopTemporaryPostgres(pg: TempPostgres | null) { + if (!pg) return; + + try { + if (pg.started) { + await execFile('pg_ctl', ['-D', pg.pgData, 'stop', '-m', 'fast'], { cwd: ROOT }); + } + } catch { + // ignore cleanup failures + } + + await fs.rm(pg.tmpDir, { recursive: true, force: true }); +} + +async function withMeasuredInject( + app: FastifyInstance, + request: Parameters[0], + parse: (payload: string) => T +): Promise<{ elapsedMs: number; statusCode: number; body: T }> { + const started = performance.now(); + const response = await app.inject(request); + const elapsedMs = performance.now() - started; + return { + elapsedMs: round(elapsedMs), + statusCode: response.statusCode, + body: parse(response.body) + }; +} + +async function loadFixtureText(fileName: string): Promise { + return fs.readFile(path.join(FIXTURES_DIR, fileName)); +} + +async function buildBundle( + registry: TrustRegistry, + artifactBuffer: Buffer, + options: { + bundleId: string; + parcelId: string; + declaredDocHash?: string; + includePdfBase64?: boolean; + } +): Promise { + const notary = registry.notaries[0]; + const provider = registry.ronProviders.find((entry) => entry.status === 'ACTIVE') || registry.ronProviders[0]; + if (!notary || !provider) { + throw new Error('registry_missing_notary_or_provider'); + } + + const declaredDocHash = options.declaredDocHash || sha256Hex(artifactBuffer); + const sealPayload = await signDocHash(deriveNotaryWallet(notary.id), declaredDocHash); + + return { + bundleId: options.bundleId, + transactionType: 'warranty', + ron: { + provider: provider.id, + notaryId: notary.id, + commissionState: notary.commissionState, + sealPayload, + sealScheme: 'SIM-ECDSA-v1' + }, + doc: { + docHash: declaredDocHash, + ...(options.includePdfBase64 === false + ? {} + : { pdfBase64: artifactBuffer.toString('base64') }) + }, + property: { + parcelId: options.parcelId, + county: 'Demo County', + state: notary.commissionState + }, + policy: { + profile: `STANDARD_${notary.commissionState}` + }, + timestamp: '2026-03-12T12:00:00.000Z' + }; +} + +async function seedBaselineData(prisma: PrismaClient, registry: TrustRegistry) { + const notary = registry.notaries[0]; + if (!notary) { + throw new Error('registry_has_no_notaries'); + } + + await prisma.countyRecord.upsert({ + where: { parcelId: 'BENCH-PARCEL-001' }, + update: { county: 'Demo County', state: notary.commissionState, active: true }, + create: { + parcelId: 'BENCH-PARCEL-001', + county: 'Demo County', + state: notary.commissionState, + active: true + } + }); + + await prisma.countyRecord.upsert({ + where: { parcelId: 'BENCH-PARCEL-002' }, + update: { county: 'Demo County', state: notary.commissionState, active: true }, + create: { + parcelId: 'BENCH-PARCEL-002', + county: 'Demo County', + state: notary.commissionState, + active: true + } + }); +} + +async function scenarioClean( + app: FastifyInstance, + cleanBundle: BundleInput, + securityCommandBase: string, + runs: number +): Promise<{ scenario: RawScenarioResult; signingTimings: number[] }> { + const timingsMs: number[] = []; + const statusCodes: number[] = []; + const signingTimings: number[] = []; + const reliabilityNotes: string[] = []; + const caveats: string[] = []; + let successCount = 0; + let failureCount = 0; + + const securityConfig = buildSecurityConfig(); + + for (let index = 0; index < runs; index += 1) { + const bundle = { ...cleanBundle, bundleId: `BENCH-CLEAN-${index + 1}` }; + const verify = await withMeasuredInject( + app, + { + method: 'POST', + url: '/api/v1/verify', + headers: { 'x-api-key': DEFAULT_API_KEY }, + payload: bundle + }, + (payload) => JSON.parse(payload) as VerifyResponse + ); + + timingsMs.push(verify.elapsedMs); + statusCodes.push(verify.statusCode); + + if (verify.statusCode === 200 && verify.body.receiptId && verify.body.receiptSignature) { + successCount += 1; + + const detail = await withMeasuredInject( + app, + { + method: 'GET', + url: `/api/v1/receipt/${verify.body.receiptId}`, + headers: { 'x-api-key': DEFAULT_API_KEY } + }, + (payload) => JSON.parse(payload) as ReceiptDetailResponse + ); + + const receipt = detail.body.receipt; + const verificationLike: VerificationResult = { + decision: receipt.decision, + reasons: receipt.reasons, + riskScore: receipt.riskScore, + checks: receipt.checks + }; + const started = performance.now(); + const rebuiltReceipt = buildReceipt(bundle, verificationLike, 'deed-shield', { + fraudRisk: receipt.fraudRisk, + zkpAttestation: receipt.zkpAttestation + }); + await signReceiptPayload( + toUnsignedReceiptPayload(rebuiltReceipt), + securityConfig.receiptSigning.current + ); + signingTimings.push(round(performance.now() - started)); + } else { + failureCount += 1; + } + } + + if (failureCount > 0) { + caveats.push('One or more clean verification runs did not return HTTP 200 with a signed receipt.'); + } + reliabilityNotes.push(`${successCount}/${runs} clean verification requests returned signed receipts.`); + + return { + scenario: { + scenario: 'clean', + purpose: 'Measure end-to-end clean artifact verification through POST /api/v1/verify.', + command: `${securityCommandBase} --scenario clean --runs ${runs}`, + metricsCaptured: ['verification request latency', 'signed receipt generation latency'], + expectedOutcome: 'HTTP 200 with receiptId, receiptHash, and receiptSignature present.', + timingsMs, + statusCodes, + successCount, + failureCount, + reliabilityNotes, + caveats + }, + signingTimings + }; +} + +async function scenarioLookup( + app: FastifyInstance, + cleanBundle: BundleInput, + securityCommandBase: string, + runs: number +): Promise<{ + lookup: RawScenarioResult; + laterVerification: RawScenarioResult; +}> { + const lookupTimingsMs: number[] = []; + const verifyTimingsMs: number[] = []; + const lookupStatusCodes: number[] = []; + const verifyStatusCodes: number[] = []; + let lookupSuccess = 0; + let verifySuccess = 0; + + for (let index = 0; index < runs; index += 1) { + const verifyIssue = await withMeasuredInject( + app, + { + method: 'POST', + url: '/api/v1/verify', + headers: { 'x-api-key': DEFAULT_API_KEY }, + payload: { ...cleanBundle, bundleId: `BENCH-LOOKUP-${index + 1}` } + }, + (payload) => JSON.parse(payload) as VerifyResponse + ); + + if (verifyIssue.statusCode !== 200 || !verifyIssue.body.receiptId) { + lookupStatusCodes.push(verifyIssue.statusCode); + verifyStatusCodes.push(verifyIssue.statusCode); + continue; + } + + const receiptId = verifyIssue.body.receiptId; + const lookup = await withMeasuredInject( + app, + { + method: 'GET', + url: `/api/v1/receipt/${receiptId}`, + headers: { 'x-api-key': DEFAULT_API_KEY } + }, + (payload) => JSON.parse(payload) as ReceiptDetailResponse + ); + lookupTimingsMs.push(lookup.elapsedMs); + lookupStatusCodes.push(lookup.statusCode); + if (lookup.statusCode === 200 && lookup.body.receipt?.receiptId === receiptId) { + lookupSuccess += 1; + } + + const laterVerification = await withMeasuredInject( + app, + { + method: 'POST', + url: `/api/v1/receipt/${receiptId}/verify`, + headers: { 'x-api-key': DEFAULT_API_KEY } + }, + (payload) => JSON.parse(payload) as StatusResponse + ); + verifyTimingsMs.push(laterVerification.elapsedMs); + verifyStatusCodes.push(laterVerification.statusCode); + if (laterVerification.statusCode === 200 && laterVerification.body.verified) { + verifySuccess += 1; + } + } + + return { + lookup: { + scenario: 'lookup', + purpose: 'Measure receipt retrieval latency through GET /api/v1/receipt/:receiptId.', + command: `${securityCommandBase} --scenario lookup --runs ${runs}`, + metricsCaptured: ['status lookup latency'], + expectedOutcome: 'HTTP 200 with persisted receipt payload.', + timingsMs: lookupTimingsMs, + statusCodes: lookupStatusCodes, + successCount: lookupSuccess, + failureCount: Math.max(0, runs - lookupSuccess), + reliabilityNotes: [`${lookupSuccess}/${runs} receipt lookup requests returned the stored receipt.`], + caveats: lookupSuccess === runs ? [] : ['Some lookup requests did not return the expected receipt payload.'] + }, + laterVerification: { + scenario: 'later-verification', + purpose: 'Measure later verification latency through POST /api/v1/receipt/:receiptId/verify.', + command: `${securityCommandBase} --scenario lookup --runs ${runs}`, + metricsCaptured: ['later verification latency'], + expectedOutcome: 'HTTP 200 with verified=true, integrityVerified=true, and signatureVerified=true.', + timingsMs: verifyTimingsMs, + statusCodes: verifyStatusCodes, + successCount: verifySuccess, + failureCount: Math.max(0, runs - verifySuccess), + reliabilityNotes: [`${verifySuccess}/${runs} later verification requests returned verified=true.`], + caveats: verifySuccess === runs ? [] : ['Some later verification requests did not return verified=true.'] + } + }; +} + +async function scenarioTampered( + app: FastifyInstance, + tamperedBundle: BundleInput, + securityCommandBase: string, + runs: number +): Promise { + const timingsMs: number[] = []; + const statusCodes: number[] = []; + const reliabilityNotes: string[] = []; + const caveats: string[] = []; + let mismatchDetected = 0; + + for (let index = 0; index < runs; index += 1) { + const verify = await withMeasuredInject( + app, + { + method: 'POST', + url: '/api/v1/verify', + headers: { 'x-api-key': DEFAULT_API_KEY }, + payload: { ...tamperedBundle, bundleId: `BENCH-TAMPER-${index + 1}` } + }, + (payload) => JSON.parse(payload) as VerifyResponse + ); + + timingsMs.push(verify.elapsedMs); + statusCodes.push(verify.statusCode); + + const publicInputs = verify.body.zkpAttestation?.publicInputs; + if ( + verify.statusCode === 200 && + publicInputs?.declaredDocHash && + publicInputs.documentDigest && + publicInputs.declaredDocHash !== publicInputs.documentDigest + ) { + mismatchDetected += 1; + } + } + + reliabilityNotes.push(`${mismatchDetected}/${runs} tampered runs surfaced a declared hash vs observed digest mismatch.`); + if (mismatchDetected !== runs) { + caveats.push('Not every tampered run surfaced the expected digest mismatch signal.'); + } + + return { + scenario: 'tampered', + purpose: 'Measure latency for a tampered artifact submission where the declared hash does not match the supplied bytes.', + command: `${securityCommandBase} --scenario tampered --runs ${runs}`, + metricsCaptured: ['tampered artifact detection latency'], + expectedOutcome: 'HTTP 200 with mismatch visible in zkpAttestation.publicInputs declaredDocHash vs documentDigest.', + timingsMs, + statusCodes, + successCount: mismatchDetected, + failureCount: Math.max(0, runs - mismatchDetected), + reliabilityNotes, + caveats + }; +} + +async function scenarioRepeat( + app: FastifyInstance, + cleanBundle: BundleInput, + securityCommandBase: string, + runs: number +): Promise { + const timingsMs: number[] = []; + const statusCodes: number[] = []; + let successCount = 0; + + const repeatBundle = { ...cleanBundle, bundleId: 'BENCH-REPEAT-SAME' }; + + for (let index = 0; index < runs; index += 1) { + const response = await withMeasuredInject( + app, + { + method: 'POST', + url: '/api/v1/verify', + headers: { 'x-api-key': DEFAULT_API_KEY }, + payload: repeatBundle + }, + (payload) => JSON.parse(payload) as VerifyResponse + ); + + timingsMs.push(response.elapsedMs); + statusCodes.push(response.statusCode); + if (response.statusCode === 200 && response.body.receiptId) { + successCount += 1; + } + } + + return { + scenario: 'repeat', + purpose: 'Measure stability when the same artifact payload is verified repeatedly.', + command: `${securityCommandBase} --scenario repeat --runs ${runs}`, + metricsCaptured: ['repeated-run stability'], + expectedOutcome: 'Repeated requests continue returning HTTP 200 and signed receipts without contract drift.', + timingsMs, + statusCodes, + successCount, + failureCount: Math.max(0, runs - successCount), + reliabilityNotes: [`${successCount}/${runs} repeated submissions of the same payload returned HTTP 200.`], + caveats: successCount === runs ? [] : ['Some repeated submissions failed or diverged from the expected response shape.'] + }; +} + +async function scenarioBadAuth( + app: FastifyInstance, + cleanBundle: BundleInput, + securityCommandBase: string +): Promise { + const timingsMs: number[] = []; + const statusCodes: number[] = []; + + const missingAuth = await withMeasuredInject( + app, + { + method: 'POST', + url: '/api/v1/verify', + payload: cleanBundle + }, + (payload) => JSON.parse(payload) as { error?: string } + ); + timingsMs.push(missingAuth.elapsedMs); + statusCodes.push(missingAuth.statusCode); + + const invalidAuth = await withMeasuredInject( + app, + { + method: 'POST', + url: '/api/v1/verify', + headers: { 'x-api-key': 'invalid-bench-api-key' }, + payload: cleanBundle + }, + (payload) => JSON.parse(payload) as { error?: string } + ); + timingsMs.push(invalidAuth.elapsedMs); + statusCodes.push(invalidAuth.statusCode); + + const successCount = statusCodes.filter((code) => code === 401 || code === 403).length; + + return { + scenario: 'bad-auth', + purpose: 'Confirm evaluator-visible fail-closed behavior for missing or invalid API authentication.', + command: `${securityCommandBase} --scenario bad-auth`, + metricsCaptured: ['auth failure response latency'], + expectedOutcome: 'Missing auth returns 401 and invalid auth returns 403.', + timingsMs, + statusCodes, + successCount, + failureCount: Math.max(0, 2 - successCount), + reliabilityNotes: [`${successCount}/2 auth-failure probes returned the expected 401 or 403 response.`], + caveats: successCount === 2 ? [] : ['One or more auth-failure probes did not return the expected status code.'] + }; +} + +async function scenarioMalformed( + app: FastifyInstance, + securityCommandBase: string +): Promise { + const timingsMs: number[] = []; + const statusCodes: number[] = []; + + const emptyPayload = await withMeasuredInject( + app, + { + method: 'POST', + url: '/api/v1/verify', + headers: { 'x-api-key': DEFAULT_API_KEY }, + payload: {} + }, + (payload) => JSON.parse(payload) as { error?: string } + ); + timingsMs.push(emptyPayload.elapsedMs); + statusCodes.push(emptyPayload.statusCode); + + const malformedPayload = await withMeasuredInject( + app, + { + method: 'POST', + url: '/api/v1/verify', + headers: { 'x-api-key': DEFAULT_API_KEY }, + payload: { bundleId: 'MALFORMED-001', doc: { docHash: 42 } } + }, + (payload) => JSON.parse(payload) as { error?: string } + ); + timingsMs.push(malformedPayload.elapsedMs); + statusCodes.push(malformedPayload.statusCode); + + const successCount = statusCodes.filter((code) => code === 400).length; + + return { + scenario: 'malformed', + purpose: 'Confirm malformed evaluator payloads fail early without entering the verification lifecycle.', + command: `${securityCommandBase} --scenario malformed`, + metricsCaptured: ['payload validation failure latency'], + expectedOutcome: 'HTTP 400 with Invalid payload error.', + timingsMs, + statusCodes, + successCount, + failureCount: Math.max(0, 2 - successCount), + reliabilityNotes: [`${successCount}/2 malformed payload probes returned HTTP 400.`], + caveats: successCount === 2 ? [] : ['One or more malformed payload probes did not return HTTP 400.'] + }; +} + +async function scenarioDependencyFailure( + app: FastifyInstance, + cleanBundle: BundleInput, + securityCommandBase: string +): Promise { + const subjectName = 'ACME HOLDINGS LLC'; + const response = await withMeasuredInject( + app, + { + method: 'POST', + url: '/api/v1/verify', + headers: { 'x-api-key': DEFAULT_API_KEY }, + payload: { + ...cleanBundle, + bundleId: 'BENCH-DEPENDENCY-FAILURE', + registryScreening: { + subjectName, + sourceIds: ['sam_exclusions'], + forceRefresh: true + } + } + }, + (payload) => JSON.parse(payload) as VerifyResponse + ); + + const success = + response.statusCode === 200 && + response.body.decision !== 'ALLOW' && + Array.isArray(response.body.reasons) && + response.body.reasons.length > 0; + + return { + scenario: 'dependency-failure', + purpose: 'Measure fail-closed behavior when an external registry dependency is unavailable without configured access.', + command: `${securityCommandBase} --scenario dependency-failure`, + metricsCaptured: ['dependency failure response latency'], + expectedOutcome: 'HTTP 200 with a non-ALLOW decision reflecting compliance-gap or fail-closed handling.', + timingsMs: [response.elapsedMs], + statusCodes: [response.statusCode], + successCount: success ? 1 : 0, + failureCount: success ? 0 : 1, + reliabilityNotes: [ + success + ? 'Registry dependency failure produced a non-ALLOW decision without exposing internal dependency details.' + : 'Registry dependency failure did not produce the expected fail-closed decision.' + ], + caveats: success ? [] : ['Dependency-failure scenario did not reproduce the expected fail-closed outcome.'] + }; +} + +async function scenarioBatch( + app: FastifyInstance, + cleanBundle: BundleInput, + securityCommandBase: string, + batchSize: number +): Promise { + const timingsMs: number[] = []; + const statusCodes: number[] = []; + let successCount = 0; + + for (let index = 0; index < batchSize; index += 1) { + const response = await withMeasuredInject( + app, + { + method: 'POST', + url: '/api/v1/verify', + headers: { 'x-api-key': DEFAULT_API_KEY }, + payload: { ...cleanBundle, bundleId: `BENCH-BATCH-${index + 1}` } + }, + (payload) => JSON.parse(payload) as VerifyResponse + ); + timingsMs.push(response.elapsedMs); + statusCodes.push(response.statusCode); + if (response.statusCode === 200 && response.body.receiptId) { + successCount += 1; + } + } + + return { + scenario: 'batch', + purpose: 'Measure sequential small-batch behavior over a short evaluator run.', + command: `${securityCommandBase} --scenario batch --batch-size ${batchSize}`, + metricsCaptured: ['small batch latency distribution'], + expectedOutcome: `All ${batchSize} sequential requests return HTTP 200 with signed receipts.`, + timingsMs, + statusCodes, + successCount, + failureCount: Math.max(0, batchSize - successCount), + reliabilityNotes: [`${successCount}/${batchSize} batch requests returned HTTP 200.`], + caveats: successCount === batchSize ? [] : ['The small batch run included one or more failed requests.'] + }; +} + +function toAggregatedResult(result: RawScenarioResult): AggregatedScenarioResult { + return { + ...result, + summary: summarizeTimings(result.timingsMs) + }; +} + +function pickScenarioList(requested: CliOptions['scenario']): ScenarioName[] { + if (requested === 'all') { + return ['clean', 'tampered', 'repeat', 'lookup', 'bad-auth', 'malformed', 'dependency-failure', 'batch']; + } + + return [requested]; +} + +function buildMarkdownReport(output: BenchmarkOutput): string { + const lines: string[] = []; + lines.push('# TrustSignal Benchmark Snapshot'); + lines.push(''); + lines.push('## Test Date/Time'); + lines.push(`- ${output.generatedAt}`); + lines.push(''); + lines.push('## Environment Description'); + lines.push(`- Node: ${output.environment.node}`); + lines.push(`- Platform: ${output.environment.platform} (${output.environment.arch})`); + lines.push(`- Host: ${output.environment.hostname}`); + lines.push(`- Temp database: ${output.environment.tempDatabase.engine} on 127.0.0.1:${output.environment.tempDatabase.port}`); + lines.push(`- Harness command: \`${output.command}\``); + lines.push(''); + lines.push('## Iteration / Sample Notes'); + for (const note of output.harness.sampleNotes) { + lines.push(`- ${note}`); + } + lines.push(''); + lines.push('## Environment Notes'); + for (const note of output.environment.notes) { + lines.push(`- ${note}`); + } + lines.push(''); + lines.push('## Scenarios Executed'); + for (const scenario of output.scenarios) { + lines.push(`- ${scenario.scenario}: ${scenario.purpose}`); + } + lines.push(''); + lines.push('## Timing Summary Table'); + lines.push(''); + lines.push('| Scenario | Count | Min (ms) | Max (ms) | Mean (ms) | Median (ms) | p95 (ms) | Success / Total |'); + lines.push('| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |'); + for (const scenario of output.scenarios) { + lines.push( + `| ${scenario.scenario} | ${scenario.summary.count} | ${scenario.summary.minMs} | ${scenario.summary.maxMs} | ${scenario.summary.meanMs} | ${scenario.summary.medianMs} | ${scenario.summary.p95Ms} | ${scenario.successCount}/${scenario.successCount + scenario.failureCount} |` + ); + } + lines.push(''); + lines.push('## Reliability Notes'); + for (const scenario of output.scenarios) { + for (const note of scenario.reliabilityNotes) { + lines.push(`- ${scenario.scenario}: ${note}`); + } + } + lines.push(''); + lines.push('## Notable Failures Or Caveats'); + if (output.notableFailures.length === 0 && output.caveats.length === 0) { + lines.push('- No harness-level failures were observed in this run.'); + } else { + for (const failure of output.notableFailures) { + lines.push(`- ${failure}`); + } + for (const caveat of output.caveats) { + lines.push(`- ${caveat}`); + } + } + lines.push(''); + lines.push('## What This Means For Evaluators'); + lines.push('- This is a recent local evaluator run against the current public `/api/v1/*` lifecycle, not a production SLA.'); + lines.push('- The numbers are most useful for comparing request classes, verifying fail-closed behavior, and spotting regressions between local validation runs.'); + lines.push('- Clean verification, receipt lookup, and later verification can be exercised repeatedly with signed-receipt persistence under a reproducible local database setup.'); + lines.push('- Tampered and dependency-failure scenarios surface behavior signals that evaluators can test without exposing proof internals, signer infrastructure, or internal topology.'); + return `${lines.join('\n')}\n`; +} + +async function main() { + const options = parseArgs(process.argv.slice(2)); + const requestedScenarios = pickScenarioList(options.scenario); + const commandBase = 'npx tsx bench/run-bench.ts'; + const fullCommand = `${commandBase} --scenario ${options.scenario} --runs ${options.runs} --batch-size ${options.batchSize}`; + + await ensureBenchDirectories(options.outputDir); + + let tempPostgres: TempPostgres | null = null; + let app: FastifyInstance | null = null; + let prisma: PrismaClient | null = null; + + try { + tempPostgres = await startTemporaryPostgres(); + + process.env.DATABASE_URL = tempPostgres.databaseUrl; + process.env.API_KEYS = DEFAULT_API_KEY; + process.env.API_KEY_SCOPES = `${DEFAULT_API_KEY}=verify|read|anchor|revoke`; + delete process.env.SAM_API_KEY; + + const { buildServer } = await import('../apps/api/src/server.js'); + prisma = new PrismaClient(); + app = await buildServer(); + app.log.level = 'fatal'; + + const registry = await loadRegistry(); + await seedBaselineData(prisma, registry); + + const cleanArtifact = await loadFixtureText('clean-artifact.txt'); + const tamperedArtifact = await loadFixtureText('tampered-artifact.txt'); + + const cleanBundle = await buildBundle(registry, cleanArtifact, { + bundleId: 'BENCH-CLEAN-SEED', + parcelId: 'BENCH-PARCEL-001', + includePdfBase64: false + }); + const tamperedBundle = await buildBundle(registry, tamperedArtifact, { + bundleId: 'BENCH-TAMPER-SEED', + parcelId: 'BENCH-PARCEL-002', + declaredDocHash: cleanBundle.doc.docHash, + includePdfBase64: true + }); + + const scenarioResults: AggregatedScenarioResult[] = []; + let verificationRequestLatency: TimingSummary | null = null; + let signedReceiptGenerationLatency: TimingSummary | null = null; + let laterVerificationLatency: TimingSummary | null = null; + let statusLookupLatency: TimingSummary | null = null; + let tamperedArtifactDetectionLatency: TimingSummary | null = null; + let repeatedRunStability: TimingSummary | null = null; + + if (requestedScenarios.includes('clean')) { + const clean = await scenarioClean(app, cleanBundle, commandBase, options.runs); + const aggregated = toAggregatedResult(clean.scenario); + scenarioResults.push(aggregated); + verificationRequestLatency = aggregated.summary; + signedReceiptGenerationLatency = summarizeTimings(clean.signingTimings); + } + + if (requestedScenarios.includes('tampered')) { + const tampered = toAggregatedResult( + await scenarioTampered(app, tamperedBundle, commandBase, options.runs) + ); + scenarioResults.push(tampered); + tamperedArtifactDetectionLatency = tampered.summary; + } + + if (requestedScenarios.includes('repeat')) { + const repeated = toAggregatedResult( + await scenarioRepeat(app, cleanBundle, commandBase, options.runs) + ); + scenarioResults.push(repeated); + repeatedRunStability = repeated.summary; + } + + if (requestedScenarios.includes('lookup')) { + const lookup = await scenarioLookup(app, cleanBundle, commandBase, options.runs); + const lookupAggregated = toAggregatedResult(lookup.lookup); + const laterVerificationAggregated = toAggregatedResult(lookup.laterVerification); + scenarioResults.push(lookupAggregated, laterVerificationAggregated); + statusLookupLatency = lookupAggregated.summary; + laterVerificationLatency = laterVerificationAggregated.summary; + } + + if (requestedScenarios.includes('bad-auth')) { + scenarioResults.push( + toAggregatedResult(await scenarioBadAuth(app, cleanBundle, commandBase)) + ); + } + + if (requestedScenarios.includes('malformed')) { + scenarioResults.push( + toAggregatedResult(await scenarioMalformed(app, commandBase)) + ); + } + + if (requestedScenarios.includes('dependency-failure')) { + scenarioResults.push( + toAggregatedResult(await scenarioDependencyFailure(app, cleanBundle, commandBase)) + ); + } + + if (requestedScenarios.includes('batch')) { + scenarioResults.push( + toAggregatedResult(await scenarioBatch(app, cleanBundle, commandBase, options.batchSize)) + ); + } + + const notableFailures = scenarioResults + .filter((scenario) => scenario.failureCount > 0) + .map((scenario) => `${scenario.scenario}: ${scenario.failureCount} failed observation(s) out of ${scenario.successCount + scenario.failureCount}.`); + const caveats = scenarioResults.flatMap((scenario) => scenario.caveats.map((note) => `${scenario.scenario}: ${note}`)); + if (requestedScenarios.includes('tampered')) { + caveats.push( + 'tampered: The tampered scenario uses a local byte fixture to force a declared-hash mismatch. It is suitable for evaluator behavior checks, not for asserting document-parser completeness.' + ); + } + + const output: BenchmarkOutput = { + generatedAt: new Date().toISOString(), + command: fullCommand, + environment: { + node: process.version, + platform: os.platform(), + arch: os.arch(), + hostname: os.hostname(), + tempDatabase: { + engine: 'postgresql', + port: tempPostgres.port, + dbName: tempPostgres.dbName + }, + notes: [ + 'Local benchmark run on a developer workstation using a temporary PostgreSQL instance.', + 'The harness exercises the public /api/v1/* evaluator lifecycle through Fastify injection rather than an external network hop.', + 'No production load balancer, cross-service network latency, or remote datastore variance is included in these numbers.' + ] + }, + harness: { + scenario: options.scenario, + runs: options.runs, + batchSize: options.batchSize, + sampleNotes: [ + `Primary timing samples use ${options.runs} iterations per scenario when applicable.`, + `The sequential batch scenario uses ${options.batchSize} requests.`, + 'First-run initialization effects may appear in max and p95 values, especially on scenarios that touch additional parsing or compliance paths.' + ] + }, + metrics: { + verificationRequestLatency, + signedReceiptGenerationLatency, + laterVerificationLatency, + statusLookupLatency, + tamperedArtifactDetectionLatency, + repeatedRunStability + }, + scenarios: scenarioResults, + notableFailures, + caveats + }; + + const jsonPath = path.join(options.outputDir, 'latest.json'); + const markdownPath = path.join(options.outputDir, 'latest.md'); + await fs.writeFile(jsonPath, `${JSON.stringify(output, null, 2)}\n`, 'utf8'); + await fs.writeFile(markdownPath, buildMarkdownReport(output), 'utf8'); + + console.log(JSON.stringify({ jsonPath, markdownPath }, null, 2)); + } finally { + if (app) { + await app.close(); + } + if (prisma) { + await prisma.$disconnect(); + } + await stopTemporaryPostgres(tempPostgres); + } +} + +main().catch((error) => { + console.error(error instanceof Error ? error.stack || error.message : String(error)); + process.exitCode = 1; +}); diff --git a/bench/scenarios.md b/bench/scenarios.md new file mode 100644 index 0000000..131340a --- /dev/null +++ b/bench/scenarios.md @@ -0,0 +1,57 @@ +# TrustSignal Scenario Matrix + +## Clean Artifact Verification + +- Purpose: Measure baseline evaluator latency for a clean artifact flowing through `POST /api/v1/verify`. +- Command or script path: `npx tsx bench/run-bench.ts --scenario clean --runs 15` +- Expected outcome: HTTP `200` with `receiptId`, `receiptHash`, and `receiptSignature`. +- Metric(s) captured: verification request latency, signed receipt generation latency. + +## Tampered Artifact Verification + +- Purpose: Measure how quickly the evaluator flow records a declared-hash vs observed-digest mismatch for tampered bytes. +- Command or script path: `npx tsx bench/run-bench.ts --scenario tampered --runs 15` +- Expected outcome: HTTP `200` with mismatch visible in `zkpAttestation.publicInputs.declaredDocHash` vs `documentDigest`. +- Metric(s) captured: tampered artifact detection latency. + +## Repeated Verification Of Same Artifact + +- Purpose: Measure stability when the same payload is submitted repeatedly through the public verification path. +- Command or script path: `npx tsx bench/run-bench.ts --scenario repeat --runs 15` +- Expected outcome: repeated HTTP `200` responses with signed receipts and no contract drift. +- Metric(s) captured: repeated-run stability, per-run latency spread. + +## Receipt Retrieval / Status Check + +- Purpose: Measure persisted receipt lookup and later verification latency after successful issuance. +- Command or script path: `npx tsx bench/run-bench.ts --scenario lookup --runs 15` +- Expected outcome: `GET /api/v1/receipt/:receiptId` returns HTTP `200`; `POST /api/v1/receipt/:receiptId/verify` returns HTTP `200` with `verified=true`. +- Metric(s) captured: status lookup latency, later verification latency. + +## Bad Auth Or Missing Auth + +- Purpose: Confirm evaluator-visible fail-closed behavior for missing or invalid API authentication. +- Command or script path: `npx tsx bench/run-bench.ts --scenario bad-auth` +- Expected outcome: missing auth returns HTTP `401`; invalid auth returns HTTP `403`. +- Metric(s) captured: auth failure response latency. + +## Missing Or Malformed Payload + +- Purpose: Confirm invalid evaluator payloads fail at the API boundary instead of entering the verification lifecycle. +- Command or script path: `npx tsx bench/run-bench.ts --scenario malformed` +- Expected outcome: HTTP `400` with invalid payload errors. +- Metric(s) captured: payload validation failure latency. + +## Dependency Failure / Fail-Closed Behavior + +- Purpose: Reproduce a safe dependency-failure path using registry screening without configured external access and verify the response does not silently pass as clean. +- Command or script path: `npx tsx bench/run-bench.ts --scenario dependency-failure` +- Expected outcome: HTTP `200` with a non-`ALLOW` decision that reflects compliance-gap or fail-closed handling. +- Metric(s) captured: dependency failure response latency. + +## Small Batch Run + +- Purpose: Measure short sequential batch behavior for evaluator-style repeated requests. +- Command or script path: `npx tsx bench/run-bench.ts --scenario batch --batch-size 10` +- Expected outcome: all sequential requests return HTTP `200` with signed receipts. +- Metric(s) captured: small batch latency distribution, success rate across the run. diff --git a/docs/README.md b/docs/README.md index 02aaf80..dce5c87 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,80 @@ # TrustSignal Documentation Index -This folder is organized into active, canonical documents and archived historical material. +> TrustSignal is evidence integrity infrastructure for signed verification receipts and later verification. + +Short description: +This index organizes the active TrustSignal documentation set for evaluators, developers, and partner reviewers, with links to lifecycle, API, security, benchmark, and claims-boundary materials. + +Audience: +- evaluators +- developers +- partner reviewers + +## Start Here + +- [Partner evaluation overview](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [Verification lifecycle](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md) +- [Security summary](/Users/christopher/Projects/trustsignal/docs/security-summary.md) +- [Security workflows](/Users/christopher/Projects/trustsignal/docs/security-workflows.md) +- [GitHub settings checklist](/Users/christopher/Projects/trustsignal/docs/github-settings-checklist.md) +- [Benchmark summary](/Users/christopher/Projects/trustsignal/docs/partner-eval/benchmark-summary.md) +- [Claims boundary](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md) +- [Docs architecture](/Users/christopher/Projects/trustsignal/docs/templates/docs-architecture.md) + +## Problem / Context + +TrustSignal documentation is written for evaluators and implementers working in workflows where later auditability matters. The main attack surface is not only bad data at intake, but also tampered evidence, provenance loss, artifact substitution, and stale evidence that cannot be verified later. + +## Integrity Model + +The canonical lifecycle and trust-boundary diagrams are documented in [verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md). + +TrustSignal is evidence integrity infrastructure. It acts as an integrity layer that returns signed verification receipts, verification signals, verifiable provenance metadata, and later verification capability for existing workflow integration. + +## How It Works + +The documentation set is organized around: + +- overview and start-here materials +- core concepts and verification lifecycle +- API and example documents +- security and claims boundary materials +- benchmarks and partner evaluation materials +- reference and archive material + +## Demo + +Start with the local developer trial if you want the fastest technical evaluation: + +- [5-minute developer trial](/Users/christopher/Projects/trustsignal/demo/README.md) + +The demo shows artifact hashing, verification, signed verification receipt issuance, later verification, and tampered artifact mismatch detection without external services. + +## Partner Evaluation + +Start here if you want to evaluate the public verification lifecycle quickly: + +- [Partner evaluation overview](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [Evaluator quickstart](/Users/christopher/Projects/trustsignal/docs/partner-eval/quickstart.md) +- [API playground](/Users/christopher/Projects/trustsignal/docs/partner-eval/api-playground.md) +- [OpenAPI contract](/Users/christopher/Projects/trustsignal/openapi.yaml) +- [Postman collection](/Users/christopher/Projects/trustsignal/postman/TrustSignal.postman_collection.json) +- [Postman local environment](/Users/christopher/Projects/trustsignal/postman/TrustSignal.local.postman_environment.json) + +Golden path: + +1. submit a verification request +2. receive verification signals plus a signed verification receipt +3. retrieve the stored receipt +4. run later verification + +## Reference / Related Docs + +The evaluator and demo paths are deliberate evaluator paths. They show the verification lifecycle safely before production integration and do not remove production security requirements. + +## Production Deployment Requirements + +Local development defaults are intentionally constrained and fail closed where production trust assumptions are not satisfied. Production deployment requires explicit authentication, signing configuration, and environment setup. ## Problem @@ -82,3 +156,10 @@ Historical planning, synthesized source-of-truth drafts, and early notebook logs - `archive/legacy-2026-02-25/` Use archived files for context only, not as current implementation guidance. + +## Related Documentation + +- [README.md](/Users/christopher/Projects/trustsignal/README.md) +- [docs/verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md) +- [docs/security-summary.md](/Users/christopher/Projects/trustsignal/docs/security-summary.md) +- [docs/templates/docs-architecture.md](/Users/christopher/Projects/trustsignal/docs/templates/docs-architecture.md) diff --git a/docs/compliance/README.md b/docs/compliance/README.md new file mode 100644 index 0000000..4c7a3bf --- /dev/null +++ b/docs/compliance/README.md @@ -0,0 +1,46 @@ +# TrustSignal Compliance Documentation Boundary + +> This repository contains SOC 2 readiness guidance only. It does not contain audit evidence or sensitive security information. + +Short description: +Use the files in this directory for high-level control descriptions, policy templates, readiness checklists, and public-safe compliance guidance only. + +Real compliance evidence must be stored in a private system such as: + +- Vanta +- internal compliance storage +- a private audit repository + +Prohibited evidence types in this public repository: + +- employee access lists +- internal infrastructure diagrams +- production logs +- incident reports +- vulnerability reports +- secrets or key management details +- vendor contracts +- customer data artifacts + +## Public Repository Contents + +- high-level control descriptions +- policy templates +- readiness checklist and readiness scoring outputs +- public-safe compliance boundary guidance + +## Private Systems Contents + +- operational evidence +- access review logs +- incident case records +- infrastructure diagrams +- monitoring dashboards +- vendor due diligence records +- security findings and remediation history + +## Related Documentation + +- [Evidence boundary](/Users/christopher/Projects/trustsignal/docs/compliance/evidence-boundary.md) +- [SOC 2 controls](/Users/christopher/Projects/trustsignal/docs/compliance/soc2/controls.md) +- [SOC 2 readiness checklist](/Users/christopher/Projects/trustsignal/docs/compliance/soc2/readiness-checklist.md) diff --git a/docs/compliance/evidence-boundary.md b/docs/compliance/evidence-boundary.md new file mode 100644 index 0000000..dce2da2 --- /dev/null +++ b/docs/compliance/evidence-boundary.md @@ -0,0 +1,33 @@ +# TrustSignal Compliance Evidence Boundary + +> TrustSignal separates public readiness documentation from private compliance evidence. The public repository must remain limited to high-level documentation and placeholders. + +## Public Repo + +- policy templates +- high-level control descriptions +- readiness checklist +- compliance overview +- generated public-safe readiness summaries + +## Private Systems + +- operational evidence +- access review logs +- security incident reports +- infrastructure diagrams +- monitoring dashboards +- vulnerability evidence and remediation records +- vendor contracts and due diligence records + +## Storage Expectation + +Real audit evidence should be stored in: + +- Vanta +- internal compliance storage +- private audit repository + +## Rule + +Do not place sensitive operational evidence in this repository. If a public compliance file needs to reference evidence, it should point to the private system of record rather than copying the evidence into git. diff --git a/docs/compliance/evidence/access-control-evidence.md b/docs/compliance/evidence/access-control-evidence.md new file mode 100644 index 0000000..855bd25 --- /dev/null +++ b/docs/compliance/evidence/access-control-evidence.md @@ -0,0 +1,10 @@ +# TrustSignal Access Control Evidence Placeholder + +Control Objective +Document that access to TrustSignal systems and repositories is approved, limited, reviewed, and removed on least-privilege terms. + +Evidence Expected by Auditor +Access review records, access approvals, repository protection settings, and joiner/mover/leaver evidence. + +Where Evidence Is Stored +Vanta, internal compliance storage, or private audit repository. Do not store access review data, employee access lists, names, or exported administrative records in this public repository. diff --git a/docs/compliance/evidence/ci-security-evidence.md b/docs/compliance/evidence/ci-security-evidence.md new file mode 100644 index 0000000..c73d6d6 --- /dev/null +++ b/docs/compliance/evidence/ci-security-evidence.md @@ -0,0 +1,10 @@ +# TrustSignal CI Security Evidence Placeholder + +Control Objective +Document that TrustSignal uses CI validation and security checks to review changes and detect security issues before merge. + +Evidence Expected by Auditor +Workflow run records, required status check configuration, workflow review history, and security scan outcomes. + +Where Evidence Is Stored +Vanta, internal compliance storage, or private audit repository. Do not store raw CI logs, screenshots, run artifacts, or internal approvals in this public repository. diff --git a/docs/compliance/evidence/logging-monitoring.md b/docs/compliance/evidence/logging-monitoring.md new file mode 100644 index 0000000..7e22b82 --- /dev/null +++ b/docs/compliance/evidence/logging-monitoring.md @@ -0,0 +1,10 @@ +# TrustSignal Logging and Monitoring Evidence Placeholder + +Control Objective +Document that TrustSignal monitors security-relevant activity and retains reviewable monitoring evidence through approved operational systems. + +Evidence Expected by Auditor +Monitoring procedures, alert review records, dashboard evidence, and logging review records. + +Where Evidence Is Stored +Vanta, internal compliance storage, or private audit repository. Do not store production logs, dashboard exports, alert payloads, or private system architecture in this public repository. diff --git a/docs/compliance/evidence/vulnerability-management.md b/docs/compliance/evidence/vulnerability-management.md new file mode 100644 index 0000000..1e6b12f --- /dev/null +++ b/docs/compliance/evidence/vulnerability-management.md @@ -0,0 +1,10 @@ +# TrustSignal Vulnerability Management Evidence Placeholder + +Control Objective +Document that TrustSignal identifies, reviews, and remediates vulnerabilities using approved workflows and tracked remediation processes. + +Evidence Expected by Auditor +Security scan results, dependency review records, remediation tracking, and risk acceptance records where applicable. + +Where Evidence Is Stored +Vanta, internal compliance storage, or private audit repository. Do not store vulnerability reports, private findings, internal ticket links, or detailed remediation notes in this public repository. diff --git a/docs/compliance/policies/access-control-policy.md b/docs/compliance/policies/access-control-policy.md new file mode 100644 index 0000000..5471f2b --- /dev/null +++ b/docs/compliance/policies/access-control-policy.md @@ -0,0 +1,31 @@ +# TrustSignal Access Control Policy + +> This public policy is intentionally high level. Operational access evidence and system-specific details must remain in private compliance systems. + +## Purpose + +Define how TrustSignal grants, reviews, and removes access to code, infrastructure, environments, and sensitive operational tooling. + +## Scope + +This policy applies to employees, contractors, service accounts, and third parties with access to TrustSignal-controlled systems, repositories, or security-relevant data. + +## Responsibilities + +- Engineering leadership approves role definitions and privileged access expectations. +- System owners approve access based on least privilege and business need. +- Administrators implement approved access changes and preserve evidence. +- Personnel with access protect credentials and report suspected misuse promptly. + +## Control Procedures + +1. Access is granted only after documented approval from an authorized owner. +2. Privileged access is limited to personnel with a demonstrated operational need. +3. Shared credentials are prohibited except where a managed service requires a documented break-glass account. +4. Access changes for joiners, movers, and leavers are completed within a defined operating window. +5. Access reviews are performed on a recurring basis and exceptions are tracked to remediation. +6. Repository, CI, and administrative settings should require strong authentication and human review for sensitive changes. + +## Evidence + +Evidence for this policy must be stored in Vanta, internal compliance storage, or a private audit repository rather than in this public repository. diff --git a/docs/compliance/policies/data-retention-policy.md b/docs/compliance/policies/data-retention-policy.md new file mode 100644 index 0000000..4b3e7e3 --- /dev/null +++ b/docs/compliance/policies/data-retention-policy.md @@ -0,0 +1,29 @@ +# TrustSignal Data Retention Policy + +> This public policy is intentionally high level. Storage-specific evidence and operational records must remain in private compliance systems. + +## Purpose + +Define retention, review, and disposal expectations for TrustSignal operational records, compliance evidence, and security-relevant data. + +## Scope + +This policy applies to repository evidence, audit artifacts, logs, incident records, CI outputs, and other retained materials used to support operations or compliance readiness. + +## Responsibilities + +- Data owners classify retained materials and define retention expectations. +- System owners ensure storage locations support controlled access and orderly disposal. +- Compliance owners coordinate evidence preservation for audits, incidents, or partner reviews. + +## Control Procedures + +1. Records are retained only for a documented business, legal, security, or compliance purpose. +2. Retention periods are defined by record category and reviewed periodically. +3. Disposal methods must be appropriate for the sensitivity of the retained material. +4. Security and audit evidence should remain accessible for the agreed review window. +5. Logs and artifacts should avoid raw secrets and unnecessary personal data. + +## Evidence + +Evidence for this policy must be stored in Vanta, internal compliance storage, or a private audit repository rather than in this public repository. diff --git a/docs/compliance/policies/incident-response-policy.md b/docs/compliance/policies/incident-response-policy.md new file mode 100644 index 0000000..256d0df --- /dev/null +++ b/docs/compliance/policies/incident-response-policy.md @@ -0,0 +1,31 @@ +# TrustSignal Incident Response Policy + +> This public policy is intentionally high level. Incident records, responder notes, and operational investigation details must remain in private compliance systems. + +## Purpose + +Define a repeatable process for identifying, escalating, containing, investigating, and recovering from security incidents affecting TrustSignal systems or evidence integrity infrastructure. + +## Scope + +This policy applies to suspected or confirmed incidents involving TrustSignal code, CI/CD systems, repositories, environments, vendors, or security-relevant data. + +## Responsibilities + +- Incident commander coordinates triage, containment, and communication. +- Engineering responders investigate technical scope, impact, and recovery actions. +- Leadership approves external communications when required. +- Security or compliance owners preserve evidence and document lessons learned. + +## Control Procedures + +1. Incidents are classified by severity based on impact to confidentiality, integrity, availability, and trust in verification signals or signed verification receipts. +2. Responders open a tracked incident record and preserve key evidence. +3. Containment actions are documented and approved according to severity and urgency. +4. Recovery steps are validated before normal operations resume. +5. Post-incident review identifies root cause, remediation owners, and control improvements. +6. Critical incidents trigger leadership notification and documented customer or partner communication review where applicable. + +## Evidence + +Evidence for this policy must be stored in Vanta, internal compliance storage, or a private audit repository rather than in this public repository. diff --git a/docs/compliance/policies/secure-development-policy.md b/docs/compliance/policies/secure-development-policy.md new file mode 100644 index 0000000..18f4aa3 --- /dev/null +++ b/docs/compliance/policies/secure-development-policy.md @@ -0,0 +1,31 @@ +# TrustSignal Secure Development Policy + +> This public policy is intentionally high level. Tool-specific configurations, sensitive findings, and operational review records must remain in private compliance systems. + +## Purpose + +Establish secure software development expectations for TrustSignal so code changes are reviewed, tested, and released with documented security considerations. + +## Scope + +This policy applies to application code, infrastructure-as-code, CI/CD workflows, dependency changes, scripts, and public-facing documentation that could affect security posture or trust claims. + +## Responsibilities + +- Engineers follow secure coding standards and document security-relevant assumptions. +- Reviewers assess security impact, dependency risk, and claims-boundary implications. +- Maintainers ensure CI validation remains effective and least-privilege automation is preserved. +- Leadership prioritizes remediation of material security findings. + +## Control Procedures + +1. Changes are introduced through reviewable pull requests unless a documented emergency process applies. +2. Security-sensitive changes receive explicit reviewer attention for auth, secrets, logging, dependency, and workflow risks. +3. Build and typecheck validation should pass before merge for changes affecting shipping code. +4. Dependency updates are reviewed with automated tooling where available. +5. Secrets, tokens, private keys, and raw PII must not be committed to the repository. +6. Documentation must not overstate security, compliance, or production guarantees beyond available evidence. + +## Evidence + +Evidence for this policy must be stored in Vanta, internal compliance storage, or a private audit repository rather than in this public repository. diff --git a/docs/compliance/policies/vendor-risk-policy.md b/docs/compliance/policies/vendor-risk-policy.md new file mode 100644 index 0000000..c6d03ad --- /dev/null +++ b/docs/compliance/policies/vendor-risk-policy.md @@ -0,0 +1,29 @@ +# TrustSignal Vendor Risk Policy + +> This public policy is intentionally high level. Vendor contracts, assessments, and due diligence records must remain in private compliance systems. + +## Purpose + +Define how TrustSignal evaluates and monitors third-party vendors, hosted services, and material software dependencies that may affect security posture or service integrity. + +## Scope + +This policy applies to cloud providers, development tooling vendors, security tooling vendors, and other third parties that process, store, or materially influence TrustSignal systems or evidence. + +## Responsibilities + +- Business and technical owners identify vendors and classify risk. +- Reviewers assess vendor security posture before onboarding or material expansion. +- System owners track renewal, reassessment, and remediation actions. + +## Control Procedures + +1. Vendors are inventoried and assigned a risk tier based on data sensitivity, operational dependency, and security impact. +2. Higher-risk vendors require documented security review before onboarding. +3. Contractual, privacy, and operational expectations are reviewed where applicable. +4. Vendor reassessment occurs periodically or after material service changes. +5. Critical open risks are tracked with owners and target remediation dates. + +## Evidence + +Evidence for this policy must be stored in Vanta, internal compliance storage, or a private audit repository rather than in this public repository. diff --git a/docs/compliance/security-posture.md b/docs/compliance/security-posture.md new file mode 100644 index 0000000..885c292 --- /dev/null +++ b/docs/compliance/security-posture.md @@ -0,0 +1,26 @@ +# TrustSignal Security Posture Snapshot + +Generated: 2026-03-13T00:32:09.914Z + +> This report summarizes repository-visible security governance indicators. It is a posture snapshot, not proof that all related GitHub or infrastructure settings are enabled in production. + +## Checks + +| Check | Status | Details | +| --- | --- | --- | +| GitHub workflows present | present | Dependency Review, Trivy, Scorecard, and zizmor workflow files exist. | +| Dependency scanning enabled | present | Dependabot configuration and dependency review workflow are present in-repo. | +| Branch protection indicators | partial | Repository contains documentation or helper automation for branch protection, but actual GitHub rules must be verified manually. | +| CI security tools present | present | Repository-level CI security tooling is present for vulnerabilities, Scorecard, and workflow linting. | + +## Interpretation + +- `present` means the expected repository file or automation exists. +- `partial` means repository indicators exist, but the control still depends on manual GitHub or infrastructure verification. +- `missing` means the repository does not currently provide the expected indicator. + +## Manual Follow-Up + +- Verify branch protection or rulesets directly in GitHub. +- Verify Dependency Graph, Dependabot alerts, and code scanning are enabled in repository settings. +- Store any operational evidence in Vanta, internal compliance storage, or a private audit repository rather than in this public repository. diff --git a/docs/compliance/soc2/controls.md b/docs/compliance/soc2/controls.md new file mode 100644 index 0000000..0171437 --- /dev/null +++ b/docs/compliance/soc2/controls.md @@ -0,0 +1,191 @@ +# TrustSignal SOC 2 Security Controls Mapping + +> TrustSignal maintains this document as a readiness-oriented control map for the SOC 2 Security Trust Services Criteria. It is intended for internal assessment, partner diligence, and gap remediation planning. It is not a statement of SOC 2 certification. + +> Public-repo boundary: +> This file contains high-level control descriptions only. Sensitive audit evidence, private architecture, access review data, production logs, and incident records must remain in Vanta or approved internal compliance storage. + +Short description: +This document maps observable TrustSignal practices, repository controls, and operating expectations to core SOC 2 Security control areas so teams can identify evidence, confirm coverage, and track remaining gaps. + +Audience: +- engineering leadership +- security reviewers +- compliance coordinators +- partner diligence teams + +## Access Control + +Control objective: +Ensure access to code, infrastructure, secrets, and operational interfaces is provisioned on least-privilege terms and reviewed on a defined cadence. + +Evidence expected by auditors: +- access provisioning and deprovisioning procedures +- role or team-based repository access assignments +- periodic access review records +- authentication and authorization standards +- evidence of restricted production or administrative access + +Example TrustSignal implementation: +- GitHub pull request review requirements and rulesets are documented in [docs/github-settings-checklist.md](/Users/christopher/Projects/trustsignal/docs/github-settings-checklist.md) +- repository guidance emphasizes least-privilege configuration and controlled secrets handling +- access review evidence can be maintained in [docs/compliance/evidence/access-control-evidence.md](/Users/christopher/Projects/trustsignal/docs/compliance/evidence/access-control-evidence.md) + +## Change Management + +Control objective: +Ensure system changes are proposed, reviewed, tested, approved, and traceable before release. + +Evidence expected by auditors: +- pull request templates and reviewer checklists +- branch protection or ruleset evidence +- CI run history for validation checks +- release or deployment approval records +- change log or commit history showing reviewable diffs + +Example TrustSignal implementation: +- `.github/pull_request_template.md` requires security, workflow, and documentation impact review +- GitHub Actions workflows provide CI evidence for build, typecheck, dependency review, and workflow linting +- repository history provides traceability for reviewed changes and targeted control updates + +## Logical Security + +Control objective: +Protect systems and data through authentication, authorization, configuration management, and secure administrative controls. + +Evidence expected by auditors: +- authentication design documentation +- configuration baselines +- environment-variable based secret handling +- system hardening guidance +- evidence of restricted administrative actions + +Example TrustSignal implementation: +- project guardrails require environment variables for secrets and prohibit hardcoded credentials +- CI and documentation emphasize least-privilege permissions for GitHub Actions +- security summaries document trust boundaries and claims boundaries for the public surface + +## System Monitoring + +Control objective: +Detect, log, and review security-relevant events and operational anomalies in a timely manner. + +Evidence expected by auditors: +- logging and monitoring procedures +- alerting or incident escalation definitions +- vulnerability scan records +- CI security scan outputs +- periodic review notes for monitoring outputs + +Example TrustSignal implementation: +- repository workflows run dependency review, Trivy, and OpenSSF Scorecard checks +- monitoring evidence guidance is maintained in [docs/compliance/evidence/logging-monitoring.md](/Users/christopher/Projects/trustsignal/docs/compliance/evidence/logging-monitoring.md) +- security posture snapshots can be generated by `scripts/security-readiness.ts` + +## Incident Response + +Control objective: +Define, communicate, and exercise a repeatable process for identifying, escalating, containing, and recovering from security incidents. + +Evidence expected by auditors: +- formal incident response policy +- severity definitions and escalation roles +- incident ticket or post-incident review examples +- communication templates +- tabletop or exercise records + +Example TrustSignal implementation: +- baseline incident response guidance is defined in [docs/compliance/policies/incident-response-policy.md](/Users/christopher/Projects/trustsignal/docs/compliance/policies/incident-response-policy.md) +- documentation separates technical evidence from policy requirements so response gaps can be tracked explicitly + +## Vendor Management + +Control objective: +Assess and monitor third-party vendors and dependencies that affect security, availability, or processing integrity. + +Evidence expected by auditors: +- vendor inventory +- risk classification criteria +- contract or review checklists +- periodic reassessment records +- dependency monitoring records + +Example TrustSignal implementation: +- dependency review and Dependabot provide ongoing software dependency visibility +- vendor review expectations are documented in [docs/compliance/policies/vendor-risk-policy.md](/Users/christopher/Projects/trustsignal/docs/compliance/policies/vendor-risk-policy.md) +- third-party integration risk can be recorded as part of readiness remediation work + +## Data Protection + +Control objective: +Protect sensitive data through classification, handling, retention, transmission, and disposal controls. + +Evidence expected by auditors: +- data handling standards +- retention and disposal policy +- encryption and secret-handling standards +- sample evidence of data minimization and redaction practices +- system documentation describing protected data paths + +Example TrustSignal implementation: +- repository guardrails prohibit secrets and raw PII in code, logs, and fixtures +- retention guidance is defined in [docs/compliance/policies/data-retention-policy.md](/Users/christopher/Projects/trustsignal/docs/compliance/policies/data-retention-policy.md) +- documentation emphasizes verifiable provenance, signed verification receipts, and later verification without exposing internal proof material + +## Secure Development + +Control objective: +Build and release software using secure engineering practices, code review, dependency management, and vulnerability remediation. + +Evidence expected by auditors: +- secure development standards +- code review requirements +- SAST or dependency scan outputs +- remediation tracking +- developer guidance for secret handling and safe defaults + +Example TrustSignal implementation: +- secure development guidance is documented in [docs/compliance/policies/secure-development-policy.md](/Users/christopher/Projects/trustsignal/docs/compliance/policies/secure-development-policy.md) +- GitHub Actions enforce build and typecheck validation +- dependency review, Trivy, and workflow linting provide repository-level security evidence + +## Risk Assessment + +Control objective: +Identify, assess, prioritize, and track security and operational risks on a recurring basis. + +Evidence expected by auditors: +- risk register or assessment worksheets +- remediation tracking +- periodic review cadence +- security review records for major changes +- evidence of management follow-up for identified gaps + +Example TrustSignal implementation: +- this readiness framework provides repeatable scoring and remediation outputs +- `scripts/soc2-readiness.ts` generates category scores and recommended remediation items +- risk discussions can be anchored to repository evidence, policy gaps, and manual GitHub settings gaps + +## Backup and Recovery + +Control objective: +Ensure critical systems and evidence can be restored within acceptable timeframes after disruption or data loss. + +Evidence expected by auditors: +- backup policy and responsibilities +- recovery procedures +- restoration test evidence +- infrastructure-provider backup settings or reports +- defined recovery objectives where applicable + +Example TrustSignal implementation: +- recovery expectations are included in the readiness checklist and policy set +- operational evidence can be captured separately from product code to support future audit preparation +- manual infrastructure verification remains necessary because repository files alone cannot prove backup execution + +## Related Documentation + +- [SOC 2 readiness checklist](/Users/christopher/Projects/trustsignal/docs/compliance/soc2/readiness-checklist.md) +- [SOC 2 readiness report](/Users/christopher/Projects/trustsignal/docs/compliance/soc2/readiness-report.md) +- [Security posture snapshot](/Users/christopher/Projects/trustsignal/docs/compliance/security-posture.md) +- [GitHub settings checklist](/Users/christopher/Projects/trustsignal/docs/github-settings-checklist.md) diff --git a/docs/compliance/soc2/readiness-checklist.md b/docs/compliance/soc2/readiness-checklist.md new file mode 100644 index 0000000..53076d6 --- /dev/null +++ b/docs/compliance/soc2/readiness-checklist.md @@ -0,0 +1,96 @@ +# TrustSignal SOC 2 Security Readiness Checklist + +> This checklist is designed for mock-audit readiness reviews against SOC 2 Security criteria. It helps the team separate implemented controls from partially implemented controls and evidence that still depends on manual operations. + +> Public-repo boundary: +> Use this checklist to track readiness at a high level only. Do not add internal infrastructure diagrams, employee access lists, production logs, incident reports, or other sensitive audit evidence here. + +Short description: +Use this checklist during internal security reviews, partner diligence preparation, and pre-audit cleanup to confirm whether TrustSignal has evidence-ready controls rather than undocumented intent. + +Audience: +- engineering managers +- security leads +- compliance coordinators + +## Access Control + +- [ ] Repository and infrastructure access is granted on least-privilege terms +- [ ] Access reviews are documented at a defined cadence +- [ ] Joiner, mover, and leaver processes are documented +- [ ] Administrative access paths are restricted and logged + +## Change Management + +- [ ] Pull requests are required for protected branches +- [ ] Human review is required before merge +- [ ] Required status checks are configured for mainline changes +- [ ] Emergency change procedures are documented + +## Logical Security + +- [ ] Secrets are stored outside the repository and rotated as needed +- [ ] Environment-specific configuration is documented +- [ ] Public-safe claims boundary is documented +- [ ] Administrative privileges are limited and reviewed + +## System Monitoring + +- [ ] Security-relevant CI scan outputs are retained +- [ ] Logging and monitoring expectations are documented +- [ ] Alert escalation ownership is defined +- [ ] Security findings are reviewed and triaged + +## Incident Response + +- [ ] Incident response policy is approved and current +- [ ] Roles and severity levels are defined +- [ ] Contact and escalation paths are documented +- [ ] Tabletop or post-incident evidence exists + +## Vendor Management + +- [ ] Critical vendors are inventoried +- [ ] Vendor risk review criteria are documented +- [ ] Reassessment cadence is defined +- [ ] Dependency risk is monitored continuously + +## Data Protection + +- [ ] Sensitive-data handling rules are documented +- [ ] Retention and disposal rules are documented +- [ ] Logging avoids raw secrets and PII +- [ ] Encryption requirements are defined where applicable + +## Secure Development + +- [ ] Secure development policy is documented +- [ ] Dependency updates are reviewed +- [ ] CI validation covers build and typecheck +- [ ] Security scans run on repository changes + +## Risk Assessment + +- [ ] A periodic security risk review exists +- [ ] Remediation items are tracked to closure +- [ ] Material system changes trigger risk review +- [ ] Readiness scoring is refreshed on a defined cadence + +## Backup and Recovery + +- [ ] Backup responsibilities are assigned +- [ ] Recovery procedures are documented +- [ ] Restore testing evidence exists +- [ ] Critical evidence repositories are recoverable + +## Evidence Review Notes + +- Repository files and CI workflows provide only partial audit evidence. +- GitHub settings, access reviews, infrastructure controls, and restore testing still require manual evidence collection. +- Generated readiness scores should be treated as internal assessment inputs, not as an audit result. + +## Related Documentation + +- [SOC 2 controls mapping](/Users/christopher/Projects/trustsignal/docs/compliance/soc2/controls.md) +- [SOC 2 readiness report](/Users/christopher/Projects/trustsignal/docs/compliance/soc2/readiness-report.md) +- [Policy templates](/Users/christopher/Projects/trustsignal/docs/compliance/policies) diff --git a/docs/compliance/soc2/readiness-report.md b/docs/compliance/soc2/readiness-report.md new file mode 100644 index 0000000..dd9f8e3 --- /dev/null +++ b/docs/compliance/soc2/readiness-report.md @@ -0,0 +1,51 @@ +# TrustSignal SOC 2 Readiness Report + +Generated: 2026-03-13T00:32:03.795Z + +> This report is an internal readiness snapshot aligned to SOC 2 Security criteria. It is intended for planning and gap remediation. It is not an audit opinion and does not imply SOC 2 certification. + +## Overall Readiness Score + +71% + +## Category Scores + +| Category | Score | Notes | +| --- | --- | --- | +| Access Control | 2 / 3 | Repository documentation covers branch protection and review expectations, but in-repo evidence does not prove completed access reviews or enforced GitHub settings. | +| Infrastructure Security | 2 / 3 | Repository-level security workflows exist for dependency review, Trivy, and Scorecard, but infrastructure controls still require manual verification outside the repo. | +| Secure Development | 3 / 3 | Pull request review guidance and security-focused CI checks provide strong repository-level secure development coverage for a readiness baseline. | +| Monitoring | 1 / 3 | The repository can generate a security posture snapshot and retain CI scan outputs, but ongoing production monitoring evidence is not proven by repository files alone. | +| Secrets Management | 2 / 3 | TrustSignal guidance prohibits hardcoded secrets and uses environment-based configuration, but rotation cadence and vault evidence are not yet captured in this framework. | +| Incident Response | 2 / 3 | A formal policy template exists, but exercised incident records, communication drills, and post-incident evidence are not yet included. | +| Data Protection | 2 / 3 | Data handling and retention guidance now exists, but applied retention schedules and production evidence still need to be collected. | +| Compliance Documentation | 3 / 3 | The repository contains a structured readiness framework, policy templates, and generated reporting suitable for a mock-audit baseline. | + +## Recommended Remediation Items + +- Access Control: Capture recurring access review evidence for GitHub and production systems. +- Access Control: Enable and verify branch protection or rulesets with required reviews on main. +- Infrastructure Security: Document environment hardening baselines and infrastructure ownership. +- Infrastructure Security: Capture operational evidence for backup, recovery, and hosted-service security settings. +- Monitoring: Document log review cadence, alert routing, and monitored systems. +- Monitoring: Attach monitoring exports or screenshots for operational environments. +- Secrets Management: Track secret rotation ownership and review cadence. +- Secrets Management: Collect evidence that production secrets are stored and rotated using approved mechanisms. +- Incident Response: Run a tabletop exercise and retain the output. +- Incident Response: Define severity levels, contact paths, and evidence preservation procedures in operating records. +- Data Protection: Define retention windows by evidence and operational data category. +- Data Protection: Capture proof of encryption, access controls, and disposal procedures where applicable. + +## Scoring Model + +- 0 = missing +- 1 = partial +- 2 = implemented +- 3 = strong + +## Notes + +- Scores are based on repository-visible controls and documentation only. +- GitHub UI configuration, infrastructure operations, access reviews, and restore testing still require manual verification. +- Operational evidence must be stored in Vanta, internal compliance storage, or a private audit repository rather than in this public repository. +- This report should be refreshed when major security workflows, policies, or governance controls change. diff --git a/docs/evidence/staging/supabase-db-security-2026-02-27.md b/docs/evidence/staging/supabase-db-security-2026-02-27.md index 58f445d..d1bf504 100644 --- a/docs/evidence/staging/supabase-db-security-2026-02-27.md +++ b/docs/evidence/staging/supabase-db-security-2026-02-27.md @@ -1,12 +1,12 @@ # Supabase DB Security Evidence - Captured at (UTC): 2026-02-28T00:41:40Z -- Supabase project ref: `bwjyvakfrnmaawztasxu` -- DB target: `aws-1-us-east-2.pooler.supabase.com:5432/postgres` +- Supabase project ref: `[redacted]` +- DB target: `[redacted]` ## 1. SSL Enforcement (Provider Control) Command: -`supabase --experimental ssl-enforcement get --project-ref bwjyvakfrnmaawztasxu` +`supabase --experimental ssl-enforcement get --project-ref [redacted]` Output: ```text @@ -15,7 +15,7 @@ SSL is being enforced. ## 2. Encryption-at-Rest Control Presence (Redacted) Command: -`supabase --experimental encryption get-root-key --project-ref bwjyvakfrnmaawztasxu` +`supabase --experimental encryption get-root-key --project-ref [redacted]` Redacted output summary: ```text @@ -26,7 +26,7 @@ Interpretation: root encryption key is present; full key material intentionally ## 3. Live DB TLS Session Proof Command: -`PGPASSWORD='***' psql "host=aws-1-us-east-2.pooler.supabase.com port=5432 dbname=postgres user=postgres.bwjyvakfrnmaawztasxu sslmode=require connect_timeout=8" -Atc "select 'ssl='||ssl::text||',version='||version||',cipher='||cipher from pg_stat_ssl where pid=pg_backend_pid();"` +`PGPASSWORD='***' psql "host=[redacted] port=5432 dbname=postgres user=[redacted] sslmode=require connect_timeout=8" -Atc "select 'ssl='||ssl::text||',version='||version||',cipher='||cipher from pg_stat_ssl where pid=pg_backend_pid();"` Output: ```text diff --git a/docs/github-settings-checklist.md b/docs/github-settings-checklist.md new file mode 100644 index 0000000..652ac60 --- /dev/null +++ b/docs/github-settings-checklist.md @@ -0,0 +1,108 @@ +# TrustSignal GitHub Settings Checklist + +> Codex can add repository files and workflows, but it cannot safely click or verify GitHub repository settings from inside the repo. After this PR lands, verify the settings below in GitHub. + +Short description: +This checklist separates what TrustSignal now manages in-repo from the GitHub settings that still require manual verification in the repository UI. + +Audience: +- repository administrators +- security reviewers +- engineering leads + +## In-Repo Automation + +The repository now manages these controls in code: + +- Dependabot configuration in `.github/dependabot.yml` +- dependency diff review in `.github/workflows/dependency-review.yml` +- repository vulnerability scanning in `.github/workflows/trivy.yml` +- workflow hardening review in `.github/workflows/zizmor.yml` +- weekly and push-based repository score tracking in `.github/workflows/scorecard.yml` +- review hygiene defaults in `.github/pull_request_template.md` + +Not yet managed in-repo: + +- `CODEOWNERS`, because repository-specific GitHub usernames or team slugs should not be guessed + +## Manual GitHub Settings Still Required + +### 1. Actions + +Verify in GitHub: +- GitHub Actions is enabled for the repository +- workflow permissions remain restricted to the default least-privilege mode unless a specific workflow requires more +- branch and environment secrets are reviewed for necessity and rotated if stale + +### 2. Dependency Graph And Dependabot + +Verify in GitHub: +- Dependency graph is enabled +- Dependabot alerts are enabled +- Dependabot security updates are enabled if supported by the repository plan +- Dependabot version updates are allowed for this repository + +### 3. Secret Scanning + +Verify in GitHub: +- secret scanning is enabled if the repository type and plan support it +- push protection is enabled if available and acceptable for the team workflow + +Note: +- secret scanning availability depends on repository visibility and GitHub plan + +### 4. Code Scanning / CodeQL + +Recommended manual setup: +- enable code scanning in GitHub Security +- prefer GitHub CodeQL default setup unless you have a clear reason to maintain advanced CodeQL workflow YAML in-repo + +Reason: +- this repo already uploads third-party SARIF from Trivy and Scorecard +- CodeQL default setup is usually the safer and lower-maintenance starting point for JavaScript/TypeScript repositories + +### 5. Branch Protection Or Rulesets + +Configure branch protection or a repository ruleset for `main`: + +- require pull requests before merge +- require at least one human PR review +- dismiss stale approvals when new commits are pushed if that matches team policy +- disable force pushes to `main` +- restrict direct pushes to `main` +- optionally require branches to be up to date before merge +- add a real `CODEOWNERS` file later if the repository has stable maintainer usernames or org team slugs + +### 6. Required Status Checks + +After the workflows have run successfully on `main`, consider requiring these checks before merge: + +- `typecheck` +- `web-build` +- `test` +- `signed-receipt-smoke` +- `messaging-check` when docs or web copy changes matter +- `Dependency diff review` + +Optional later: + +- `Trivy repository scan` after the advisory rollout proves low-noise +- `zizmor advisory audit` for workflow-change pull requests if branch rulesets can scope that requirement safely + +Advisory only by default: + +- `OpenSSF Scorecard analysis` + +## What To Verify After Merge + +1. Open the repository `Settings` and `Security` tabs in GitHub. +2. Confirm every workflow appears under Actions and is enabled. +3. Confirm Dependabot is creating update PRs on the expected schedule. +4. Confirm the Security tab shows dependency graph, Dependabot alerts, and code scanning as enabled where supported. +5. Add the required status checks only after at least one successful run for each target check. + +## Related Documentation + +- [Security workflows](/Users/christopher/Projects/trustsignal/docs/security-workflows.md) +- [Security summary](/Users/christopher/Projects/trustsignal/docs/security-summary.md) +- [Documentation index](/Users/christopher/Projects/trustsignal/docs/README.md) diff --git a/docs/integrations/github-action.md b/docs/integrations/github-action.md new file mode 100644 index 0000000..02ee084 --- /dev/null +++ b/docs/integrations/github-action.md @@ -0,0 +1,103 @@ +# TrustSignal GitHub Action Integration + +## Purpose + +`TrustSignal Verify Artifact` is a GitHub Action integration for verifying build artifacts through the TrustSignal API. The action calls `api.trustsignal.dev`, receives a signed verification receipt, and stores that receipt identifier for later verification workflows. + +The GitHub Action does not connect to Supabase directly. TrustSignal persists receipts server-side behind the public API boundary. + +## Verification Flow + +1. The workflow sends an artifact hash or local artifact path through the GitHub Action. +2. The action calls `POST /api/v1/verify` on `api.trustsignal.dev`. +3. TrustSignal validates the request, authenticates the caller, issues a signed receipt, and persists the receipt server-side. +4. The action stores `receiptId` and `receiptSignature` for later verification or audit use. +5. Public consumers can inspect the stored receipt through `GET /api/v1/receipt/{receiptId}` or render a compact badge from `GET /api/v1/receipt/{receiptId}/summary`. +6. A later workflow can call `POST /api/v1/receipt/{receiptId}/verify` with an artifact hash to confirm integrity. + +## Public API Contract + +### `POST /api/v1/verify` + +Headers: + +```http +x-api-key: +content-type: application/json +``` + +Request body: + +```json +{ + "artifact": { + "hash": "", + "algorithm": "sha256" + }, + "source": { + "provider": "github-actions", + "repository": "", + "workflow": "", + "runId": "", + "commit": "", + "actor": "" + }, + "metadata": { + "artifactPath": "" + } +} +``` + +Response fields used by the action: + +- `verificationId` +- `receiptId` +- `receiptSignature` +- `status` + +### `GET /api/v1/receipt/{receiptId}` + +This public-safe endpoint returns a compact inspection view for artifact receipts. It is intended for receipt drill-down pages and audit references. + +### `GET /api/v1/receipt/{receiptId}/summary` + +This public-safe endpoint returns a compact display payload for trust centers, evidence panels, and partner dashboards. + +### `POST /api/v1/receipt/{receiptId}/verify` + +Request body: + +```json +{ + "artifact": { + "hash": "", + "algorithm": "sha256" + } +} +``` + +Response fields: + +- `verified` +- `integrityVerified` +- `signatureVerified` +- `status` +- `receiptId` +- `receiptSignature` +- `storedHash` +- `recomputedHash` + +## Security Boundary + +- The GitHub Action calls TrustSignal API only. +- Supabase is private backend persistence and is not a public integration surface. +- Service role credentials are backend-only and must never be exposed to clients. +- Artifact receipts are stored for later verification. +- Row Level Security is enabled on the artifact receipt table as defense in depth. +- Public lookup and summary endpoints are read-only and return safe receipt fields only. +- Later verification remains behind TrustSignal API authentication. + +## Current Limitations + +- The repository includes a local smoke test, but a live deployed integration test remains pending. +- The public verification contract currently accepts `sha256` only. diff --git a/docs/integrations/public-verification.md b/docs/integrations/public-verification.md new file mode 100644 index 0000000..d7ba70c --- /dev/null +++ b/docs/integrations/public-verification.md @@ -0,0 +1,65 @@ +# TrustSignal Public Verification + +## Why Public Verification Matters + +TrustSignal receipts are designed to travel with an artifact after the initial workflow run. A team can issue a signed verification receipt once, store the receipt identifier with its evidence record, and later let auditors, buyers, or partner platforms inspect the receipt without exposing internal systems. + +This makes TrustSignal useful in trust-center views, evidence panels, and partner review workflows where the downstream user needs a compact integrity signal rather than internal engine details. + +## Receipt Lookup Flow + +1. A workflow issues a signed receipt through `POST /api/v1/verify`. +2. The caller stores `receiptId` with its evidence or build record. +3. A public or partner-facing surface retrieves the safe receipt view with `GET /api/v1/receipt/{receiptId}`. + +The public lookup response is artifact-oriented and omits internal scoring, signing secrets, and private service details. + +## Later Verification Flow + +1. A trusted integration submits an artifact hash to `POST /api/v1/receipt/{receiptId}/verify`. +2. TrustSignal compares the supplied hash with the stored artifact hash. +3. The API returns whether integrity and signature checks still pass. + +This route remains authenticated. Public inspection is read-only; active verification stays behind the TrustSignal API boundary. + +## Partner Summary Flow + +`GET /api/v1/receipt/{receiptId}/summary` returns a compact verification badge payload for trust centers, compliance dashboards, and partner evidence panels. + +It is designed for simple display logic: + +- `status` +- `integrityState` +- `issuedAt` +- source summary +- a ready-to-render `display` object + +## Example Partner Uses + +### Drata-style evidence view + +Store `receiptId` alongside a control evidence record. When an auditor opens the evidence detail, the platform can fetch `/summary` and render a compact TrustSignal integrity badge next to the artifact metadata. + +### Vanta-style evidence view + +Attach the receipt to a control test result. Use `/receipt/{receiptId}` for drill-down and `/summary` for the evidence list row. + +### Public trust center or vendor review + +Expose a receipt inspector link such as `/verify/{receiptId}`. Buyers can review the signed receipt metadata without gaining access to private systems or backend persistence. + +## Verification Badge Example + +```json +{ + "receiptId": "8fb78fc6-2763-4e63-9f65-67da2f9f6d98", + "status": "verified", + "integrityState": "valid", + "issuedAt": "2026-03-13T09:06:47.000Z", + "display": { + "label": "TrustSignal Verified", + "tone": "success", + "statement": "This artifact has a signed verification receipt and can be checked later for integrity drift." + } +} +``` diff --git a/docs/partner-eval/benchmark-summary.md b/docs/partner-eval/benchmark-summary.md new file mode 100644 index 0000000..0d5f721 --- /dev/null +++ b/docs/partner-eval/benchmark-summary.md @@ -0,0 +1,114 @@ +# TrustSignal Evaluator Benchmark Summary + +> TrustSignal is evidence integrity infrastructure for signed verification receipts and later verification. + +Short description: +This evaluator-facing brief summarizes the latest local TrustSignal benchmark snapshot, scenario coverage, benchmark metadata, and the right way to interpret the numbers. + +Audience: +- partner evaluators +- technical sponsors +- developers validating benchmark artifacts + +This page summarizes the most recent local evaluator benchmark snapshot from [bench/results/latest.json](/Users/christopher/Projects/trustsignal/bench/results/latest.json) and [bench/results/latest.md](/Users/christopher/Projects/trustsignal/bench/results/latest.md). + +## Executive Summary + +The current local evaluator run shows that the public `/api/v1/*` lifecycle is fast and stable in a reproducible local setup. Clean verification, receipt lookup, later verification, and repeated submissions all completed successfully across the sampled runs, with clean verification averaging `5.24 ms`, receipt lookup `0.57 ms`, and later verification `0.77 ms`. + +The tampered artifact path also completed successfully across all sampled runs, with a median of `5.13 ms`. Its `42.82 ms` p95 is materially higher than the median and should be treated as a follow-up item rather than a normal steady-state expectation. The current evidence suggests local first-run or parser-path variance, not a correctness failure, but the spread is large enough to call out explicitly. + +## Key Facts / Scope + +- Scope: current local evaluator benchmark run against the public `/api/v1/*` lifecycle +- Primary sample size: `15` iterations per applicable scenario +- Sequential batch size: `10` +- Raw artifacts: [latest.json](/Users/christopher/Projects/trustsignal/bench/results/latest.json), [latest.md](/Users/christopher/Projects/trustsignal/bench/results/latest.md) +- Integrity layer focus: signed verification receipts, verification signals, verifiable provenance, later verification, and existing workflow integration + +## Main Content + +### Metric Table + +| Metric | Samples | Min (ms) | Max (ms) | Mean (ms) | Median (ms) | p95 (ms) | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| Verification request latency | 15 | 3.21 | 21.65 | 5.24 | 4.11 | 21.65 | +| Signed receipt generation latency | 15 | 0.27 | 0.63 | 0.34 | 0.32 | 0.63 | +| Receipt lookup latency | 15 | 0.51 | 0.63 | 0.57 | 0.56 | 0.63 | +| Later verification latency | 15 | 0.67 | 1.08 | 0.77 | 0.71 | 1.08 | +| Tampered artifact detection latency | 15 | 4.74 | 42.82 | 7.76 | 5.13 | 42.82 | +| Repeated-run stability latency | 15 | 3.03 | 3.69 | 3.24 | 3.16 | 3.69 | + +### Scenario Coverage + +- `clean`: end-to-end `POST /api/v1/verify` with signed receipt issuance +- `tampered`: declared-hash vs observed-digest mismatch path +- `repeat`: repeated verification of the same artifact payload +- `lookup`: `GET /api/v1/receipt/:receiptId` +- `later-verification`: `POST /api/v1/receipt/:receiptId/verify` +- `bad-auth`: missing or invalid `x-api-key` +- `malformed`: missing or malformed request payload +- `dependency-failure`: safe fail-closed registry-screening path +- `batch`: short sequential batch run + +Reliability notes from the latest run: + +- `15/15` clean verification requests returned signed receipts. +- `15/15` tampered runs surfaced a declared-hash vs observed-digest mismatch. +- `15/15` later verification requests returned `verified=true`. +- `10/10` sequential batch requests returned `HTTP 200`. + +### Environment And Caveats + +- Benchmark timestamp: `2026-03-12T22:30:04.260Z` +- Runtime: Node `v22.14.0` +- Platform: `darwin (arm64)` +- Database: temporary local PostgreSQL instance on `127.0.0.1:64030` +- Primary sample size: `15` iterations per applicable scenario +- Sequential batch sample size: `10` +- Harness command: `npx tsx bench/run-bench.ts --scenario all --runs 15 --batch-size 10` + +Current caveats: + +- This is a local developer-workstation benchmark, not a hosted environment benchmark. +- The harness uses Fastify injection to exercise the public evaluator routes without adding external network-hop latency. +- The tampered scenario uses a local byte fixture to force a declared-hash mismatch. It is useful for evaluator behavior checks, not for claiming document-parser completeness. + +### How To Interpret These Numbers + +Treat these values as recent local evaluator benchmark results. They are useful for comparing request classes, spotting regressions, and demonstrating lifecycle behavior, but they are not production SLA claims and should not be read as guaranteed deployment latency. + +The medians are the best quick read for typical local behavior. The p95 values are more useful for spotting variance and warm-up effects. In this run, the tampered-path p95 spike is large enough to watch in future snapshots. + +### What The Benchmark Does Measure + +- Public evaluator lifecycle timing for the current `/api/v1/*` verification path +- Signed receipt issuance timing using the same receipt-building and signing primitives used by the evaluator flow +- Receipt retrieval and later verification timing +- Repeatability across multiple local runs +- API-boundary failure behavior for bad auth and malformed payloads +- A safe local fail-closed dependency scenario + +### What The Benchmark Does Not Measure + +- Production network latency, cold starts behind hosting infrastructure, or edge routing effects +- Multi-tenant concurrency, sustained throughput, or horizontal scaling behavior +- Remote database latency, failover behavior, or managed-service variance +- Full end-user browser timing +- Proof internals, signer infrastructure specifics, internal topology, or any non-public runtime surfaces + +### Tampered Path Variance Review + +The tampered artifact path recorded a median of `5.13 ms` but a p95 of `42.82 ms`. Given the local fixture-driven setup and the parser/compliance code touched by that path, this looks more like local path variance than an indication that tamper detection is unreliable. Even so, the spread is large enough that it should be treated as a follow-up item in future benchmark runs rather than dismissed as unimportant expected variance. + +## Claims Boundary + +> [!NOTE] +> Claims boundary: this brief describes local benchmark behavior for the public TrustSignal integration surface. It should not be read as a production SLA, a claim about internal proof systems, or a statement about non-public infrastructure. + +## Related Artifacts / References + +- [docs/partner-eval/overview.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [docs/partner-eval/try-the-api.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/try-the-api.md) +- [docs/partner-eval/security-summary.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/security-summary.md) +- [bench/README.md](/Users/christopher/Projects/trustsignal/bench/README.md) diff --git a/docs/partner-eval/overview.md b/docs/partner-eval/overview.md index 15b091f..b61f5c8 100644 --- a/docs/partner-eval/overview.md +++ b/docs/partner-eval/overview.md @@ -1,10 +1,20 @@ # TrustSignal Partner Evaluation Overview -## Problem +> TrustSignal is evidence integrity infrastructure for signed verification receipts and later verification. + +Short description: +This overview is the evaluator-facing entry point for the TrustSignal integrity layer, public lifecycle, benchmark materials, and existing workflow integration path. + +Audience: +- partner evaluators +- solutions engineers +- technical sponsors + +## Problem / Context Teams often have a workflow record that says an artifact was reviewed, approved, or submitted, but they cannot easily prove later that the same artifact is still the one tied to that decision. In high-loss and highly scrutinized workflows, that creates an attack surface around tampered evidence, provenance loss, artifact substitution, and stale evidence in later review paths. -## Verification Lifecycle +## Integrity Model The canonical lifecycle diagram and trust-boundary diagram are documented in [../verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md). @@ -17,24 +27,37 @@ TrustSignal is designed to support: - verifiable provenance - later verification without replacing the upstream workflow owner +## How It Works + +TrustSignal supports evaluator review through: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification +- existing workflow integration through the public API boundary + ## Demo Start with the local developer trial when you want the shortest path to the verification lifecycle: - [5-minute developer trial](/Users/christopher/Projects/trustsignal/demo/README.md) +- [Evaluator start here](/Users/christopher/Projects/trustsignal/docs/partner-eval/start-here.md) +- [Try the API](/Users/christopher/Projects/trustsignal/docs/partner-eval/try-the-api.md) -## Integration Model +## Partner Evaluation Start with these evaluator assets: - [Evaluator quickstart](/Users/christopher/Projects/trustsignal/docs/partner-eval/quickstart.md) - [API playground](/Users/christopher/Projects/trustsignal/docs/partner-eval/api-playground.md) +- [Benchmark summary](/Users/christopher/Projects/trustsignal/docs/partner-eval/benchmark-summary.md) - [OpenAPI contract](/Users/christopher/Projects/trustsignal/openapi.yaml) - [Postman collection](/Users/christopher/Projects/trustsignal/postman/TrustSignal.postman_collection.json) The evaluator flow is designed to show the verification lifecycle safely before production integration requirements are introduced. -## Technical Details +## API And Examples The public evaluation path in this repository is the `/api/v1/*` surface: @@ -46,6 +69,24 @@ The public evaluation path in this repository is the `/api/v1/*` surface: Canonical contract and payload examples live in [openapi.yaml](/Users/christopher/Projects/trustsignal/openapi.yaml) and the [`examples/`](../../examples) directory. +## Benchmarks And Evaluator Materials + +Recent local benchmark snapshot from [bench/results/latest.md](/Users/christopher/Projects/trustsignal/bench/results/latest.md) at `2026-03-12T22:30:04.260Z`. For evaluator-facing interpretation and caveats, see [benchmark-summary.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/benchmark-summary.md). + +- clean verification request latency: mean `5.24 ms`, median `4.11 ms`, p95 `21.65 ms` +- signed receipt generation latency: mean `0.34 ms`, median `0.32 ms`, p95 `0.63 ms` +- receipt lookup latency: mean `0.57 ms`, median `0.56 ms`, p95 `0.63 ms` +- later verification latency: mean `0.77 ms`, median `0.71 ms`, p95 `1.08 ms` +- tampered artifact detection latency: mean `7.76 ms`, median `5.13 ms`, p95 `42.82 ms` +- repeated-run stability for the same artifact payload: mean `3.24 ms`, median `3.16 ms`, p95 `3.69 ms` + +This snapshot comes from a recent local evaluator run. It is useful for comparing request classes and checking regressions, not for inferring guaranteed deployment latency. + +## Production Considerations + +> [!IMPORTANT] +> Production considerations: the evaluator path demonstrates the TrustSignal integrity layer before full deployment configuration. It does not replace deployment-specific authentication, signing configuration, or infrastructure review. + ## Integration Fit TrustSignal fits behind an existing workflow such as: @@ -57,6 +98,18 @@ TrustSignal fits behind an existing workflow such as: The upstream platform remains the system of record. TrustSignal adds an integrity layer and returns technical verification artifacts that can be stored alongside the workflow record. +## Security And Claims Boundary + +> [!NOTE] +> Claims boundary: this overview covers the public evaluation surface only. It does not expose proof internals, circuit identifiers, model outputs, signing infrastructure specifics, or internal service topology. + ## Production Deployment Requirements Local and evaluator paths are deliberate evaluator paths. Production deployment requires explicit authentication, signing configuration, and environment setup. Fail-closed defaults are part of the security posture and are intended to stop unsafe production assumptions from being applied implicitly. + +## Related Documentation + +- [docs/partner-eval/try-the-api.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/try-the-api.md) +- [docs/partner-eval/benchmark-summary.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/benchmark-summary.md) +- [docs/partner-eval/security-summary.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/security-summary.md) +- [docs/verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md) diff --git a/docs/partner-eval/security-summary.md b/docs/partner-eval/security-summary.md index c8015e4..35f5d94 100644 --- a/docs/partner-eval/security-summary.md +++ b/docs/partner-eval/security-summary.md @@ -1,14 +1,46 @@ # Partner Security Summary -## Problem +> TrustSignal is evidence integrity infrastructure for signed verification receipts and later verification. + +Short description: +This partner-facing security summary explains the public TrustSignal integration boundary, key controls, and claims boundary for existing workflow integration. + +Audience: +- partner security reviewers +- evaluators +- technical sponsors + +## Executive Summary + +TrustSignal exposes a public integration boundary built around signed verification receipts, verification signals, verifiable provenance, and later verification. The public security posture is focused on route-level authorization, explicit lifecycle controls, and fail-closed defaults without exposing non-public implementation details. + +## Key Facts / Scope + +- Scope: public `/api/v1/*` integration boundary +- Focus: evaluator-safe security posture +- Out of scope: internal implementation details and deployment-specific infrastructure controls + +## Main Content + +### Problem / Context Partners need enough security detail to evaluate the integration boundary without exposing internal implementation details that are not required for integration. -## Integrity Model +### Integrity Model TrustSignal provides a public API boundary that is centered on signed verification receipts, verification signals, verifiable provenance metadata, and later verification. -## Integration Fit +### How It Works + +TrustSignal public security materials focus on: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification +- existing workflow integration + +### Integration Fit For the public `/api/v1/*` surface in this repository: @@ -17,7 +49,12 @@ For the public `/api/v1/*` surface in this repository: - Request validation, rate limiting, and structured service logging are implemented at the API gateway. - Receipt revocation requires additional issuer authorization headers. -## Technical Detail +### Security And Claims Boundary + +> [!NOTE] +> Claims boundary: this summary covers the public-safe integration surface only. It does not expose proof internals, signer infrastructure specifics, internal topology, or unsupported legal/compliance claims. + +### Technical Detail TrustSignal does not require partners to understand internal proof systems, internal service topology, or signing infrastructure details in order to integrate. @@ -29,3 +66,10 @@ For public evaluation, the important security properties are: - authorization boundaries are explicit at the route level Operational deployment details such as environment-specific key custody, hosting controls, and external provider posture remain infrastructure concerns outside the public integration contract. + +## Related Artifacts / References + +- [docs/security-summary.md](/Users/christopher/Projects/trustsignal/docs/security-summary.md) +- [docs/partner-eval/overview.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [wiki/Claims-Boundary.md](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md) +- [SECURITY_CHECKLIST.md](/Users/christopher/Projects/trustsignal/SECURITY_CHECKLIST.md) diff --git a/docs/partner-eval/start-here.md b/docs/partner-eval/start-here.md new file mode 100644 index 0000000..ae410fc --- /dev/null +++ b/docs/partner-eval/start-here.md @@ -0,0 +1,76 @@ +# TrustSignal Evaluator Start Here + +This is the canonical evaluator entry point for partner engineers reviewing TrustSignal. + +## 1. What TrustSignal Does + +TrustSignal is evidence integrity infrastructure for existing workflow integration. It acts as an integrity layer that returns signed verification receipts, verification signals, verifiable provenance, and later verification capability without replacing the system of record. + +TrustSignal provides: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification capability + +TrustSignal does not provide: + +- legal determinations +- compliance certification +- fraud adjudication +- replacement for the system of record + +## 2. The Verification Lifecycle + +Start with the canonical lifecycle and trust-boundary diagrams in [verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md). + +The public lifecycle is: + +1. submit a verification request +2. receive verification signals and a signed verification receipt +3. store the receipt with the workflow record +4. run later verification before downstream reliance +5. detect tampering, substitution, or provenance drift through later verification + +## 3. What You Can Evaluate In 5 Minutes + +Use the local evaluator and public API artifacts to confirm: + +- whether the public API returns verification signals you can store in an existing workflow +- whether signed verification receipts are easy to retrieve and inspect +- whether later verification is explicit and easy to re-run during audit review +- whether the contract exposes verifiable provenance without exposing internal implementation details + +Start here: + +- [overview.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [try-the-api.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/try-the-api.md) +- [demo/README.md](/Users/christopher/Projects/trustsignal/demo/README.md) + +## 4. Public API Contract + +- [openapi.yaml](/Users/christopher/Projects/trustsignal/openapi.yaml) +- [TrustSignal.postman_collection.json](/Users/christopher/Projects/trustsignal/postman/TrustSignal.postman_collection.json) +- [TrustSignal.local.postman_environment.json](/Users/christopher/Projects/trustsignal/postman/TrustSignal.local.postman_environment.json) + +The public evaluator path uses the existing `/api/v1/*` contract only. + +## 5. Example Payloads + +- [examples/verification-request.json](/Users/christopher/Projects/trustsignal/examples/verification-request.json) +- [examples/verification-response.json](/Users/christopher/Projects/trustsignal/examples/verification-response.json) +- [examples/verification-receipt.json](/Users/christopher/Projects/trustsignal/examples/verification-receipt.json) +- [examples/verification-status.json](/Users/christopher/Projects/trustsignal/examples/verification-status.json) + +## 6. Security / Claims Boundary + +- [security-summary.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/security-summary.md) +- [claims-boundary.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/claims-boundary.md) + +Public evaluator materials intentionally do not expose proof internals, circuit identifiers, model outputs, signing infrastructure specifics, internal service topology, witness or prover details, or registry scoring algorithms. + +## 7. Where To Go Next + +- [integration-model.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/integration-model.md) +- [api-playground.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/api-playground.md) +- [quickstart.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/quickstart.md) diff --git a/docs/partner-eval/try-the-api.md b/docs/partner-eval/try-the-api.md new file mode 100644 index 0000000..352d067 --- /dev/null +++ b/docs/partner-eval/try-the-api.md @@ -0,0 +1,134 @@ +# Try The TrustSignal API + +> TrustSignal is evidence integrity infrastructure for signed verification receipts and later verification. + +Short description: +This page is the copy-paste API trial path for the public TrustSignal evaluator contract and existing workflow integration flow. + +Audience: +- integration engineers +- evaluators +- developers + +This page is the copy-paste API trial path for the public evaluator contract. + +## Problem / Context + +Evaluators and developers need a compact path to see how verification signals, signed verification receipts, verifiable provenance, and later verification work together at the API boundary. + +## Integrity Model + +The public evaluator path demonstrates: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification +- existing workflow integration + +## How It Works + +## 1. Submit A Verification Request + +Request body: [verification-request.json](/Users/christopher/Projects/trustsignal/examples/verification-request.json) + +```bash +curl -X POST "$TRUSTSIGNAL_BASE_URL/api/v1/verify" \ + -H "Content-Type: application/json" \ + -H "x-api-key: $TRUSTSIGNAL_API_KEY" \ + --data @examples/verification-request.json +``` + +Sample response: [verification-response.json](/Users/christopher/Projects/trustsignal/examples/verification-response.json) + +```json +{ + "receiptVersion": "2.0", + "decision": "ALLOW", + "reasons": ["receipt issued"], + "receiptId": "623e0b54-87b3-42b7-bc89-65fae0ad8d5e", + "receiptHash": "0x4e7f2ce9d3f7a8d3b0e4c9f2aa17fd59d6b4fda2d7b7b7d1cce8124d7ee39d04", + "receiptSignature": { + "alg": "EdDSA", + "kid": "trustsignal-current", + "signature": "eyJleGFtcGxlIjoic2lnbmVkLXJlY2VpcHQifQ" + }, + "anchor": { + "status": "PENDING", + "subjectDigest": "0x8c0f95cda31274e7b61adfd1dd1e0c03a4b96f78d90da52d42fd93d9a38fc112", + "subjectVersion": "trustsignal.anchor_subject.v1" + }, + "revocation": { + "status": "ACTIVE" + } +} +``` + +## 2. Retrieve The Stored Receipt + +Receipt example: [verification-receipt.json](/Users/christopher/Projects/trustsignal/examples/verification-receipt.json) + +```bash +curl "$TRUSTSIGNAL_BASE_URL/api/v1/receipt/$RECEIPT_ID" \ + -H "x-api-key: $TRUSTSIGNAL_API_KEY" +``` + +## 3. Run Later Verification + +Status example: [verification-status.json](/Users/christopher/Projects/trustsignal/examples/verification-status.json) + +```bash +curl -X POST "$TRUSTSIGNAL_BASE_URL/api/v1/receipt/$RECEIPT_ID/verify" \ + -H "x-api-key: $TRUSTSIGNAL_API_KEY" +``` + +## 4. Optional Public Lifecycle Actions + +If your evaluation includes provenance-state review: + +```bash +curl -X POST "$TRUSTSIGNAL_BASE_URL/api/v1/anchor/$RECEIPT_ID" \ + -H "x-api-key: $TRUSTSIGNAL_API_KEY" +``` + +Revocation is part of the public contract, but it requires issuer authorization headers in addition to the API key. Use the Postman collection for the full request template. + +## Example Or Diagram + +The request and response examples below show the public evaluator flow from verification request to later verification. + +## Recent Verification Timing + +Recent local benchmark snapshot from [bench/results/latest.md](/Users/christopher/Projects/trustsignal/bench/results/latest.md) at `2026-03-12T22:30:04.260Z`. For a fuller evaluator-facing summary, see [benchmark-summary.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/benchmark-summary.md). + +- `POST /api/v1/verify` clean-path latency: mean `5.24 ms`, median `4.11 ms`, p95 `21.65 ms` +- signed receipt generation latency: mean `0.34 ms`, median `0.32 ms`, p95 `0.63 ms` +- `GET /api/v1/receipt/:receiptId` lookup latency: mean `0.57 ms`, median `0.56 ms`, p95 `0.63 ms` +- `POST /api/v1/receipt/:receiptId/verify` later verification latency: mean `0.77 ms`, median `0.71 ms`, p95 `1.08 ms` +- tampered artifact detection path: mean `7.76 ms`, median `5.13 ms`, p95 `42.82 ms` + +These numbers come from a recent local benchmark harness run against the current evaluator path. They are current validation data, not guaranteed service latency. + +## Production Considerations + +> [!IMPORTANT] +> Production considerations: use this evaluator flow as a technical trial path, not as a complete production deployment checklist. + +## Production Readiness + +- Authentication: use `x-api-key` with the scopes required for verify, read, anchor, or revoke operations. +- Environment configuration: separate local, staging, and production base URLs, API keys, and lifecycle identifiers. +- Lifecycle monitoring: monitor receipt retrieval, lifecycle state changes, and later verification outcomes in the surrounding workflow. +- Verification checks before relying on prior results: run later verification before audit review, handoff, or another high-trust workflow step. + +## Security And Claims Boundary + +> [!NOTE] +> Claims boundary: this page documents the public evaluator contract only. It does not expose proof internals, signer infrastructure specifics, internal topology, or unsupported performance guarantees. + +## Related Documentation + +- [docs/partner-eval/overview.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [docs/partner-eval/benchmark-summary.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/benchmark-summary.md) +- [docs/verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md) +- [wiki/Claims-Boundary.md](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md) diff --git a/docs/security-summary.md b/docs/security-summary.md index 91a2d73..5e562a2 100644 --- a/docs/security-summary.md +++ b/docs/security-summary.md @@ -1,6 +1,16 @@ # TrustSignal Public Security Summary -## Problem +> TrustSignal is evidence integrity infrastructure for signed verification receipts and later verification. + +Short description: +This public-safe security summary explains the TrustSignal integration boundary, security posture, and claims boundary without exposing non-public implementation details. + +Audience: +- partner security reviewers +- evaluators +- developers + +## Problem / Context Partners and evaluators need a public-safe security summary that explains the attack surface without exposing internal implementation details. In high-stakes workflows, evidence can be challenged after collection through tampered evidence, provenance loss, artifact substitution, or stale records that are no longer independently verifiable. @@ -8,6 +18,16 @@ Partners and evaluators need a public-safe security summary that explains the at TrustSignal provides signed verification receipts, verification signals, verifiable provenance metadata, and later verification capability for existing workflow integration. +## How It Works + +TrustSignal public security materials focus on the integration-facing integrity layer: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification +- existing workflow integration + ## Integration Fit For the public `/api/v1/*` surface in this repository: @@ -17,13 +37,29 @@ For the public `/api/v1/*` surface in this repository: - request validation and rate limiting are enforced at the API boundary - receipt revocation requires additional issuer authorization headers - later verification is available through a dedicated receipt verification route +- public receipt inspection is available through `GET /api/v1/receipt/{receiptId}` and `GET /api/v1/receipt/{receiptId}/summary` for artifact receipts backed by unguessable receipt IDs +- the GitHub Action calls TrustSignal API, not Supabase directly +- artifact receipts are persisted server-side behind the API boundary +- Supabase service-role credentials are backend-only and must never be exposed to browser or action code +- Row Level Security is enabled on the artifact receipt table as defense in depth +- active later verification stays behind authenticated API calls even when public inspection is enabled Evaluator and demo flows are deliberate evaluator paths. They are designed to show the verification lifecycle safely before production integration. +## Production Considerations + +> [!IMPORTANT] +> Production considerations: public evaluator documentation does not replace environment-specific authentication, signing configuration, hosting controls, secret management, or operational review. + ## Production Deployment Requirements Local development defaults are intentionally constrained and fail closed where production trust assumptions are not satisfied. Production deployment requires explicit authentication, signing configuration, and environment setup. +## Security And Claims Boundary + +> [!NOTE] +> Claims boundary: this summary covers the public-safe security posture only. It does not expose proof internals, signing infrastructure specifics, internal service topology, or unsupported legal/compliance claims. + ## Technical Detail TrustSignal public materials should be understood within this boundary: @@ -43,3 +79,10 @@ TrustSignal does not provide: - fraud adjudication - a replacement for the system of record - infrastructure claims that depend on deployment-specific evidence outside this repository + +## Related Documentation + +- [docs/partner-eval/security-summary.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/security-summary.md) +- [SECURITY_CHECKLIST.md](/Users/christopher/Projects/trustsignal/SECURITY_CHECKLIST.md) +- [docs/SECURITY.md](/Users/christopher/Projects/trustsignal/docs/SECURITY.md) +- [wiki/Claims-Boundary.md](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md) diff --git a/docs/security-workflows.md b/docs/security-workflows.md new file mode 100644 index 0000000..c4ac960 --- /dev/null +++ b/docs/security-workflows.md @@ -0,0 +1,117 @@ +# TrustSignal Security Workflows + +> TrustSignal manages a minimal set of security-focused GitHub Actions workflows in-repo. These checks improve repository hygiene and visibility, but they do not replace manual GitHub settings that must still be enabled by a repository administrator. + +Short description: +This document explains which security and governance controls are now defined in repository files, when they run, and how to interpret advisory versus blocking outcomes. + +Audience: +- repository administrators +- security reviewers +- maintainers + +## In-Repo Automation + +The repository now defines these security workflows: + +- `.github/workflows/dependency-review.yml` +- `.github/workflows/trivy.yml` +- `.github/workflows/scorecard.yml` +- `.github/workflows/zizmor.yml` + +## Dependency Review + +What it does: +- reviews dependency diffs on pull requests +- blocks only when a pull request introduces `high` or `critical` vulnerabilities through dependency changes + +When it runs: +- on pull requests + +Mode: +- blocking + +How to interpret failures: +- a failing result means the dependency diff introduced a clearly risky dependency update +- review the dependency review summary in the GitHub workflow run before merging + +## Trivy Filesystem Scan + +What it does: +- scans the repository filesystem for `HIGH` and `CRITICAL` vulnerabilities +- ignores unfixed issues in the first rollout to reduce noise +- uploads SARIF results when GitHub token permissions allow it + +When it runs: +- on every pull request +- on pushes to `main` + +Mode: +- advisory + +How to interpret failures: +- this workflow currently does not fail the job on findings +- review SARIF/code scanning results for actionable issues +- on forked pull requests, SARIF upload may be skipped because GitHub does not grant `security-events: write` to untrusted fork tokens + +## OpenSSF Scorecard + +What it does: +- runs OpenSSF Scorecard against the repository +- uploads SARIF results and stores the SARIF file as an artifact +- publishes Scorecard results through the supported Scorecard path + +When it runs: +- on pushes to `main` +- weekly on schedule + +Mode: +- advisory + +How to interpret failures: +- failures usually indicate a workflow/configuration issue, a permissions problem, or a Scorecard execution issue +- review the workflow logs and SARIF upload details first + +## zizmor Workflow Audit + +What it does: +- audits GitHub Actions workflows for common workflow security issues +- emits annotations and logs for maintainers reviewing workflow changes + +When it runs: +- only when files in `.github/workflows/**` change + +Mode: +- advisory + +How to interpret failures: +- findings are intentionally non-blocking during the rollout period +- maintainers should still review and address findings before merging workflow changes + +## Least-Privilege Design + +These workflows follow a least-privilege model: + +- `contents: read` is used where checkout or repository metadata access is required +- `security-events: write` is granted only to SARIF-uploading workflows +- `id-token: write` is granted only to Scorecard because its standard publishing flow requires it +- no workflow uses `pull_request_target` +- no workflow exposes repository secrets unnecessarily + +## What Is Not Controlled By Repo Files + +These workflows do not automatically configure repository settings such as: + +- enabling Dependency Graph +- enabling Dependabot alerts or security updates +- enabling secret scanning +- enabling CodeQL or GitHub code scanning defaults +- configuring branch protection or rulesets + +Those controls still require manual verification in GitHub after merge. + +## Related Documentation + +- [GitHub settings checklist](/Users/christopher/Projects/trustsignal/docs/github-settings-checklist.md) +- [Security summary](/Users/christopher/Projects/trustsignal/docs/security-summary.md) +- [Documentation index](/Users/christopher/Projects/trustsignal/docs/README.md) diff --git a/docs/security/public-repo-safety.md b/docs/security/public-repo-safety.md new file mode 100644 index 0000000..e1ea39d --- /dev/null +++ b/docs/security/public-repo-safety.md @@ -0,0 +1,44 @@ +# Public Repo Safety + +TrustSignal is a public repository. It is intended to expose the integration-facing verification surface, public-safe documentation, and example workflows for signed verification receipts. + +## Intentionally Public + +- public API contracts +- integration examples +- public-safe receipt lookup and summary responses +- generic verification lifecycle documentation +- placeholder environment examples + +## Must Never Be Committed + +- live secrets or tokens +- service-role or admin credentials +- database passwords or full connection strings +- signing private keys or raw key exports +- private evidence, private customer data, or raw production payloads + +## Supabase Boundary + +Supabase persistence is backend-only. Service-role credentials are used only by the TrustSignal API server and must never appear in browser code, GitHub Actions workflows, or public client examples. + +The intended architecture is: + +`Client or GitHub Action -> TrustSignal API -> Supabase` + +## Public Verification Surface + +Public receipt endpoints expose only safe receipt metadata needed for inspection: + +- receipt identifier +- artifact hash and algorithm +- source metadata +- verification status +- issued timestamp +- safe receipt-signature metadata such as algorithm and key identifier + +They do not expose signing key material, service-role credentials, private infrastructure details, or internal verification engine state. + +## Private Material Stays Outside The Repo + +Operational secrets, private evidence, full environment values, and internal infrastructure details must remain outside the public repository and outside public docs. diff --git a/docs/templates/doc-template.md b/docs/templates/doc-template.md new file mode 100644 index 0000000..e8de47e --- /dev/null +++ b/docs/templates/doc-template.md @@ -0,0 +1,54 @@ +# Document Title + +> TrustSignal is evidence integrity infrastructure for signed verification receipts and later verification. + +Short description: +One or two sentences explaining what this document covers and how it supports existing workflow integration. + +Audience: +- evaluators +- developers +- partner reviewers + +## Problem / Context + +Explain the reader problem first. Keep the framing aligned to evidence integrity infrastructure, signed verification receipts, verification signals, verifiable provenance, later verification, and existing workflow integration. + +## Integrity Model + +Describe how this document relates to the TrustSignal integrity layer. Prefer the canonical terms: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification +- integrity layer +- existing workflow integration + +## How It Works + +Provide the practical explanation. Use short subsections or steps when useful. + +## Example Or Diagram + +Add a code example, lifecycle bullets, or a Mermaid diagram when that helps the reader. + +## Production Considerations + +> [!IMPORTANT] +> Production considerations: local evaluator paths are not substitutes for deployment-specific authentication, signing configuration, infrastructure controls, and operational review. + +List production-sensitive considerations that belong in this document. + +## Security And Claims Boundary + +> [!NOTE] +> Claims boundary: this document describes the public integration and evaluation surface only. Do not read it as a claim about internal proof systems, signer infrastructure, internal topology, or environment-specific controls outside this repository. + +Document the relevant public-safe security and claims boundary notes. + +## Related Documentation + +- [Related doc one](/Users/christopher/Projects/trustsignal/docs/README.md) +- [Related doc two](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md) +- [Related doc three](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md) diff --git a/docs/templates/docs-architecture.md b/docs/templates/docs-architecture.md new file mode 100644 index 0000000..2764a88 --- /dev/null +++ b/docs/templates/docs-architecture.md @@ -0,0 +1,236 @@ +# TrustSignal Documentation Architecture + +> TrustSignal is evidence integrity infrastructure for signed verification receipts and later verification. + +This guide defines the canonical information architecture for TrustSignal documentation. It is designed for GitHub markdown today and for later mirroring into website documentation with minimal restructuring. + +## Purpose + +TrustSignal documentation should make it easy to understand: + +- what TrustSignal is +- how the integrity layer fits into existing workflow integration +- how signed verification receipts, verification signals, verifiable provenance, and later verification relate to one another +- where evaluators, developers, and partner reviewers should start +- what claims are in scope and what remains outside the public boundary + +## Canonical Sections + +### 1. Overview / Start Here + +Audience: +- new evaluators +- partner reviewers +- developers new to the repository + +Content: +- product-level orientation +- short repository description +- start-here navigation +- reading order for first-time readers +- high-level explanation of existing workflow integration + +Examples: +- `README.md` +- `docs/README.md` +- `docs/partner-eval/start-here.md` +- `wiki/Home.md` + +### 2. Core Concepts + +Audience: +- evaluators +- product and partnership stakeholders +- developers who need the terminology before the API details + +Content: +- evidence integrity infrastructure +- integrity layer positioning +- signed verification receipts +- verification signals +- verifiable provenance +- later verification +- existing workflow integration framing + +Examples: +- `wiki/What-is-TrustSignal.md` +- `wiki/Verification-Receipts.md` + +### 3. Verification Lifecycle + +Audience: +- evaluators +- implementation owners +- security reviewers + +Content: +- lifecycle diagrams +- step-by-step explanations +- trust boundary framing +- how the receipt lifecycle works from request through later verification + +Examples: +- `docs/verification-lifecycle.md` +- `wiki/Quick-Verification-Example.md` + +### 4. API and Examples + +Audience: +- developers +- integration engineers +- technical evaluators + +Content: +- public endpoint overview +- request and response examples +- auth expectations +- lifecycle actions +- error semantics + +Examples: +- `openapi.yaml` +- `docs/partner-eval/try-the-api.md` +- `wiki/API-Overview.md` + +### 5. Security and Threat Model + +Audience: +- security reviewers +- partner security teams +- technical decision-makers + +Content: +- public-safe security posture +- security controls at the integration boundary +- what is intentionally not exposed +- threat model links +- production security considerations + +Examples: +- `docs/security-summary.md` +- `docs/partner-eval/security-summary.md` +- `SECURITY_CHECKLIST.md` +- `docs/SECURITY.md` + +### 6. Benchmarks and Evaluator Materials + +Audience: +- evaluators +- partner technical reviewers +- internal teams validating performance snapshots + +Content: +- benchmark methodology +- benchmark metadata +- scenario coverage +- local benchmark caveats +- links to raw artifacts + +Examples: +- `bench/README.md` +- `docs/partner-eval/benchmark-summary.md` +- `bench/results/latest.md` + +### 7. Partner Evaluation + +Audience: +- partner evaluators +- solutions engineers +- technical sponsors + +Content: +- overview of evaluator path +- benchmark summary +- security summary +- quickstart links +- integration briefing materials + +Examples: +- `docs/partner-eval/overview.md` +- `docs/partner-eval/try-the-api.md` +- `docs/partner-eval/benchmark-summary.md` + +### 8. Claims Boundary + +Audience: +- partner reviewers +- legal/compliance-adjacent reviewers +- internal authors of public docs + +Content: +- what TrustSignal does claim +- what TrustSignal does not claim +- phrasing guardrails +- public/private boundary references + +Examples: +- `wiki/Claims-Boundary.md` +- `docs/public-private-boundary.md` +- `README.md` claims sections + +### 9. Reference / Related Docs + +Audience: +- all readers once they need depth + +Content: +- related document lists +- archival references +- legal, policy, and operational references +- specialized evaluator materials + +Examples: +- `docs/README.md` +- related documentation sections across public docs + +## Linking Model + +TrustSignal docs should link in layers: + +1. Overview documents link down into lifecycle, API, security, benchmarks, and claims boundary. +2. Concept and lifecycle docs link sideways to API examples, security summaries, and evaluator materials. +3. Deep technical or evaluator docs link back up to overview/start-here pages so readers do not dead-end. + +Preferred link behavior: + +- Every public-facing doc should end with a `Related Documentation` section. +- API docs should link to lifecycle, evaluator overview, and claims boundary. +- Security docs should link to claims boundary and public-safe architecture docs. +- Benchmark docs should link to evaluator overview, API trial docs, and raw benchmark artifacts. + +## Preferred Reading Order + +### Evaluator Reading Order + +1. `README.md` +2. `docs/partner-eval/overview.md` +3. `docs/verification-lifecycle.md` +4. `docs/partner-eval/try-the-api.md` +5. `docs/partner-eval/benchmark-summary.md` +6. `docs/partner-eval/security-summary.md` +7. `wiki/Claims-Boundary.md` + +### Developer Reading Order + +1. `README.md` +2. `docs/README.md` +3. `docs/verification-lifecycle.md` +4. `wiki/API-Overview.md` +5. `docs/partner-eval/try-the-api.md` +6. `docs/security-summary.md` +7. `bench/README.md` + +## Authoring Rules + +- Lead with a short description and audience label where useful. +- Use the canonical TrustSignal phrases consistently: + - evidence integrity infrastructure + - signed verification receipts + - verification signals + - verifiable provenance + - later verification + - integrity layer + - existing workflow integration +- Keep GitHub-markdown-friendly structure. +- Do not expose internal proof systems, circuit identifiers, model outputs, signing infrastructure specifics, internal service topology, witness/prover details, or registry scoring algorithms. +- Do not force identical headings into every file when the result would reduce clarity. Use the common structure intelligently. diff --git a/docs/templates/partner-brief-template.md b/docs/templates/partner-brief-template.md new file mode 100644 index 0000000..e88b68e --- /dev/null +++ b/docs/templates/partner-brief-template.md @@ -0,0 +1,54 @@ +# TrustSignal Brief Title + +> TrustSignal is evidence integrity infrastructure for signed verification receipts and later verification. + +Short description: +One or two sentences that frame the brief for partner evaluators, technical sponsors, or security reviewers. + +Audience: +- partner evaluators +- technical decision-makers +- integration leads + +## Executive Summary + +Summarize the main conclusion in one short paragraph. + +## Key Facts / Scope + +- Scope item one +- Scope item two +- Scope item three +- Scope limitation or key boundary + +## Main Content + +Use the sections that fit the brief: + +### Context + +Explain why the brief exists. + +### Current State + +Summarize the current TrustSignal position, benchmark, security posture, or integration state. + +### Evidence Or Examples + +Add the supporting details, table, example, or diagram. + +## Production Considerations + +> [!IMPORTANT] +> Production considerations: this brief supports evaluation and existing workflow integration planning. It does not replace deployment-specific authentication, signing, infrastructure, or operational review. + +## Claims Boundary + +> [!NOTE] +> Claims boundary: TrustSignal public briefs describe the integrity layer, signed verification receipts, verification signals, verifiable provenance, and later verification. They do not expose proof internals, model outputs, signer infrastructure specifics, internal service topology, or unsupported legal/compliance claims. + +## Related Artifacts / References + +- [Related artifact one](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [Related artifact two](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md) +- [Related artifact three](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md) diff --git a/docs/verification-lifecycle.md b/docs/verification-lifecycle.md index be3568e..54f8430 100644 --- a/docs/verification-lifecycle.md +++ b/docs/verification-lifecycle.md @@ -1,8 +1,33 @@ # TrustSignal Verification Lifecycle +> TrustSignal is evidence integrity infrastructure for signed verification receipts and later verification. + +Short description: +This document explains the externally visible TrustSignal verification lifecycle for verification signals, signed verification receipts, verifiable provenance, and later verification in existing workflow integration. + +Audience: +- evaluators +- developers +- security reviewers + TrustSignal is evidence integrity infrastructure for existing workflows. The verification lifecycle below shows the externally visible flow for producing verification signals, issuing signed verification receipts, and supporting later verification without exposing private verification engine internals. -## Lifecycle Diagram +## Problem / Context + +Partner workflows often need to rely on evidence after collection, not just at intake. The lifecycle matters because later verification is where tampered evidence, provenance loss, artifact substitution, and stale records become visible. + +## Integrity Model + +TrustSignal acts as an integrity layer around an existing system of record. It returns: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification capability + +## Example Or Diagram + +### Lifecycle Diagram ```mermaid flowchart TD @@ -24,7 +49,9 @@ flowchart TD G --> H ``` -## Step Explanations +## How It Works + +### Step Explanations ### 1. Artifact or Evidence @@ -58,7 +85,12 @@ Before relying on the earlier result during audit review, partner review, or ano If the current artifact or stored state no longer matches the receipt-bound record, later verification produces a mismatch signal that exposes tampering, substitution, or provenance drift. -## Trust Boundary Diagram +## Security And Claims Boundary + +> [!NOTE] +> Claims boundary: this lifecycle describes the public TrustSignal integration surface. It does not document proof internals, signer infrastructure specifics, internal service topology, or non-public runtime details. + +### Trust Boundary Diagram ```mermaid flowchart TD @@ -72,9 +104,28 @@ flowchart TD C --> D ``` -## Boundary Explanation +### Boundary Explanation - The external workflow or partner system remains the system of record. - The TrustSignal API Gateway is the public integration boundary for verification and later verification requests. - The private verification engine remains non-public. - The public outputs are verification signals, signed verification receipts, and verifiable provenance suitable for later verification. + +## Current Evaluator Metrics + +Recent local benchmark snapshot from [bench/results/latest.md](/Users/christopher/Projects/trustsignal/bench/results/latest.md) at `2026-03-12T22:30:04.260Z`: + +- clean verification request latency: mean `5.24 ms`, median `4.11 ms`, p95 `21.65 ms` +- signed receipt generation latency: mean `0.34 ms`, median `0.32 ms`, p95 `0.63 ms` +- receipt lookup latency: mean `0.57 ms`, median `0.56 ms`, p95 `0.63 ms` +- later verification latency: mean `0.77 ms`, median `0.71 ms`, p95 `1.08 ms` +- tampered artifact detection latency: mean `7.76 ms`, median `5.13 ms`, p95 `42.82 ms` + +This benchmark snapshot is from a recent local evaluator run using the current public lifecycle. It helps characterize the flow in this repository without making production-performance claims. + +## Related Documentation + +- [docs/partner-eval/overview.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [docs/partner-eval/try-the-api.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/try-the-api.md) +- [docs/partner-eval/benchmark-summary.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/benchmark-summary.md) +- [wiki/Claims-Boundary.md](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md) diff --git a/examples/verification-receipt.json b/examples/verification-receipt.json index 857510c..fb44e53 100644 --- a/examples/verification-receipt.json +++ b/examples/verification-receipt.json @@ -4,7 +4,7 @@ "reasons": [ "receipt issued" ], - "receiptId": "2c17d2f5-4de6-48c3-b22c-0b7ea9eb5c0a", + "receiptId": "623e0b54-87b3-42b7-bc89-65fae0ad8d5e", "receiptHash": "0x4e7f2ce9d3f7a8d3b0e4c9f2aa17fd59d6b4fda2d7b7b7d1cce8124d7ee39d04", "receiptSignature": { "alg": "EdDSA", @@ -21,7 +21,7 @@ }, "receipt": { "receiptVersion": "2.0", - "receiptId": "2c17d2f5-4de6-48c3-b22c-0b7ea9eb5c0a", + "receiptId": "623e0b54-87b3-42b7-bc89-65fae0ad8d5e", "createdAt": "2026-03-12T15:24:01.000Z", "policyProfile": "CONTROL_CC_001", "inputsCommitment": "0x2dded9c1b5c4c6d91df58a1b1793cb527f2b0cf5ddaf447f5b7d9839f7ab7d01", @@ -43,6 +43,6 @@ "signature": "eyJleGFtcGxlIjoic2lnbmVkLXJlY2VpcHQifQ" } }, - "canonicalReceipt": "{\"checks\":[{\"checkId\":\"registry.status\",\"details\":\"Source responded with a current record\",\"status\":\"PASS\"}],\"createdAt\":\"2026-03-12T15:24:01.000Z\",\"decision\":\"ALLOW\",\"inputsCommitment\":\"0x2dded9c1b5c4c6d91df58a1b1793cb527f2b0cf5ddaf447f5b7d9839f7ab7d01\",\"policyProfile\":\"CONTROL_CC_001\",\"reasons\":[\"receipt issued\"],\"receiptId\":\"2c17d2f5-4de6-48c3-b22c-0b7ea9eb5c0a\",\"receiptVersion\":\"2.0\"}", - "pdfUrl": "/api/v1/receipt/2c17d2f5-4de6-48c3-b22c-0b7ea9eb5c0a/pdf" + "canonicalReceipt": "{\"checks\":[{\"checkId\":\"registry.status\",\"details\":\"Source responded with a current record\",\"status\":\"PASS\"}],\"createdAt\":\"2026-03-12T15:24:01.000Z\",\"decision\":\"ALLOW\",\"inputsCommitment\":\"0x2dded9c1b5c4c6d91df58a1b1793cb527f2b0cf5ddaf447f5b7d9839f7ab7d01\",\"policyProfile\":\"CONTROL_CC_001\",\"reasons\":[\"receipt issued\"],\"receiptId\":\"623e0b54-87b3-42b7-bc89-65fae0ad8d5e\",\"receiptVersion\":\"2.0\"}", + "pdfUrl": "/api/v1/receipt/623e0b54-87b3-42b7-bc89-65fae0ad8d5e/pdf" } diff --git a/examples/verification-request.json b/examples/verification-request.json index 2e446f0..3823f16 100644 --- a/examples/verification-request.json +++ b/examples/verification-request.json @@ -1,5 +1,5 @@ { - "bundleId": "verification-2026-03-12-001", + "bundleId": "verification-2026-03-12-start-here", "transactionType": "deed_transfer", "ron": { "provider": "source-system", @@ -14,9 +14,9 @@ "profile": "CONTROL_CC_001" }, "property": { - "parcelId": "PARCEL-EXAMPLE-1001", + "parcelId": "PARCEL-EVAL-1001", "county": "Cook", "state": "IL" }, - "timestamp": "2026-03-12T15:24:00.000Z" + "timestamp": "2026-03-12T18:24:00.000Z" } diff --git a/examples/verification-response.json b/examples/verification-response.json index 83eba8a..9a968de 100644 --- a/examples/verification-response.json +++ b/examples/verification-response.json @@ -4,7 +4,7 @@ "reasons": [ "receipt issued" ], - "receiptId": "2c17d2f5-4de6-48c3-b22c-0b7ea9eb5c0a", + "receiptId": "623e0b54-87b3-42b7-bc89-65fae0ad8d5e", "receiptHash": "0x4e7f2ce9d3f7a8d3b0e4c9f2aa17fd59d6b4fda2d7b7b7d1cce8124d7ee39d04", "receiptSignature": { "alg": "EdDSA", diff --git a/github-actions/trustsignal-verify-artifact/.gitignore b/github-actions/trustsignal-verify-artifact/.gitignore new file mode 100644 index 0000000..fe03786 --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +.DS_Store +coverage/ +*.log +tmp/ +!dist/ +!dist/index.js diff --git a/github-actions/trustsignal-verify-artifact/CONTRIBUTING.md b/github-actions/trustsignal-verify-artifact/CONTRIBUTING.md new file mode 100644 index 0000000..7eddfd9 --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/CONTRIBUTING.md @@ -0,0 +1,34 @@ +# Contributing + +## Local Validation + +Run the lightweight validation checks before opening a change: + +```bash +node --check src/index.js +node --check dist/index.js +node scripts/test-local.js +``` + +Or use package scripts: + +```bash +npm run check +npm run test:local +npm run validate:local +``` + +## Repository Structure + +- `action.yml`: GitHub Action metadata +- `src/`: source implementation +- `dist/`: committed runtime entrypoint for action consumers +- `scripts/`: local validation helpers +- `docs/`: integration-facing documentation + +## Release Basics + +- Follow semantic versioning. +- Commit updated `dist/index.js` with each release. +- Publish immutable tags such as `v0.1.0` and maintain a major tag such as `v1`. +- GitHub Marketplace publication requires a public repository with `action.yml` at the repository root. diff --git a/github-actions/trustsignal-verify-artifact/LICENSE b/github-actions/trustsignal-verify-artifact/LICENSE new file mode 100644 index 0000000..bdbdc1d --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 TrustSignal + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/github-actions/trustsignal-verify-artifact/README.md b/github-actions/trustsignal-verify-artifact/README.md new file mode 100644 index 0000000..496f6ae --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/README.md @@ -0,0 +1,209 @@ +# TrustSignal Verify Artifact + +Verify release artifacts in CI, issue signed verification receipts, and preserve provenance for downstream verification and audit workflows. + +[![License: MIT](https://img.shields.io/badge/license-MIT-informational)](LICENSE) +[![Node.js](https://img.shields.io/badge/node-%3E%3D20-339933?logo=node.js&logoColor=white)](package.json) + +`TrustSignal Verify Artifact` is a JavaScript GitHub Action for teams that need a reliable verification checkpoint inside CI/CD. It hashes a build artifact or accepts a precomputed SHA-256 digest, submits that artifact identity to TrustSignal, and returns receipt metadata that can be retained with release records, provenance evidence, and downstream audit workflows. + +TrustSignal is designed for artifact integrity, signed verification receipts, verifiable provenance, and audit-ready release controls. + +## Features + +- Verify build artifacts directly inside GitHub Actions +- Issue signed verification receipts for CI outputs +- Preserve provenance context from the GitHub workflow runtime +- Support later verification and audit workflows through `receipt_id` +- Fail closed on invalid or mismatched verification results when required + +## Why Teams Use It + +- Add a lightweight integrity control to release workflows +- Preserve a verifiable record of what was checked in CI +- Improve traceability across build, release, and audit paths +- Standardize artifact verification without embedding internal platform logic in workflows + +## Quick Start + +1. Add `TRUSTSIGNAL_API_BASE_URL` and `TRUSTSIGNAL_API_KEY` to GitHub Actions secrets. +2. Call the action with either `artifact_path` or `artifact_hash`. +3. Capture `receipt_id` and `receipt_signature` in downstream steps. +4. Store receipt metadata anywhere you need later verification or audit evidence. + +## Inputs + +| Input | Required | Description | +| --- | --- | --- | +| `api_base_url` | Yes | Base URL for the TrustSignal public API, for example `https://api.trustsignal.dev`. | +| `api_key` | Yes | TrustSignal API key. Pass it from GitHub Actions secrets. | +| `artifact_path` | No | Local path to the artifact file to hash with SHA-256. | +| `artifact_hash` | No | Precomputed SHA-256 digest to verify instead of hashing a local file. | +| `source` | No | Source provider label sent in the verification request. Defaults to `github-actions`. | +| `fail_on_mismatch` | No | When `true`, the action fails on non-valid verification results. Defaults to `true`. | + +Provide exactly one of `artifact_path` or `artifact_hash`. + +## Outputs + +| Output | Description | +| --- | --- | +| `verification_id` | Verification identifier returned by TrustSignal. For compatibility, this aliases `receipt_id` when the API does not return a separate verification identifier. | +| `status` | Normalized verification status returned by the API. | +| `receipt_id` | Signed receipt identifier returned by TrustSignal. | +| `receipt_signature` | Signed receipt signature returned by TrustSignal. | + +## Example Usage + +### Verify An Artifact File + +```yaml +name: Verify Build Artifact + +on: + push: + branches: [main] + +jobs: + verify-artifact: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build artifact + run: | + mkdir -p dist + echo "release bundle" > dist/release.txt + + - name: Verify artifact with TrustSignal + id: trustsignal + uses: trustsignal-dev/trustsignal-verify-artifact@v1 + with: + api_base_url: ${{ secrets.TRUSTSIGNAL_API_BASE_URL }} + api_key: ${{ secrets.TRUSTSIGNAL_API_KEY }} + artifact_path: dist/release.txt + source: github-actions + fail_on_mismatch: "true" + + - name: Record verification outputs + run: | + echo "Verification ID: ${{ steps.trustsignal.outputs.verification_id }}" + echo "Status: ${{ steps.trustsignal.outputs.status }}" + echo "Receipt ID: ${{ steps.trustsignal.outputs.receipt_id }}" + echo "Receipt Signature: ${{ steps.trustsignal.outputs.receipt_signature }}" +``` + +### Verify A Precomputed Hash + +```yaml +name: Verify Artifact Hash + +on: + workflow_dispatch: + +jobs: + verify-hash: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Verify known digest + id: trustsignal + uses: trustsignal-dev/trustsignal-verify-artifact@v1 + with: + api_base_url: ${{ secrets.TRUSTSIGNAL_API_BASE_URL }} + api_key: ${{ secrets.TRUSTSIGNAL_API_KEY }} + artifact_hash: 2f77668a9dfbf8d5847cf2d5d0370740e0c0601b4f061c1181f58c77c2b8f486 + source: github-actions + fail_on_mismatch: "true" + + - name: Print verification result + run: | + echo "Verification ID: ${{ steps.trustsignal.outputs.verification_id }}" + echo "Status: ${{ steps.trustsignal.outputs.status }}" +``` + +## Request Contract + +The action calls `POST /api/v1/verify` with a generic artifact verification payload: + +```json +{ + "artifact": { + "hash": "", + "algorithm": "sha256" + }, + "source": { + "provider": "github-actions", + "repository": "", + "workflow": "", + "runId": "", + "commit": "", + "actor": "" + }, + "metadata": { + "artifactPath": "" + } +} +``` + +GitHub workflow context is added automatically when those environment variables are available at runtime. + +## Security Considerations + +- The API key is sent only in the `x-api-key` header. +- The action does not log secrets. +- Error messages are concise and avoid raw internal details. +- Local artifact hashing uses SHA-256 from Node.js `crypto`. +- `fail_on_mismatch` allows pipelines to enforce fail-closed verification behavior. + +## Why TrustSignal + +TrustSignal gives security and release teams a consistent way to verify artifact identity inside CI/CD while preserving signed evidence for later validation. The action is built to support integrity controls, provenance continuity, and audit-ready release workflows without forcing teams to reimplement verification logic in every pipeline. + +## Current Limitations + +- Local validation uses a fetch mock rather than a live TrustSignal deployment. +- GitHub Marketplace publication requires this action to be published from a dedicated public repository root with `action.yml` at the top level. +- Live end-to-end validation against a deployed TrustSignal API should remain part of the release process. + +## Local Validation + +Run the lightweight validation checks with: + +```bash +node --check src/index.js +node --check dist/index.js +node scripts/test-local.js +``` + +Or use the package scripts: + +```bash +npm run check +npm run test:local +npm run validate:local +``` + +## Versioning Guidance + +- Follow semantic versioning. +- Publish immutable release tags for each shipped version. +- Maintain a major tag such as `v1` for stable consumers. + +## Release Checklist + +- Commit the built `dist/index.js` artifact with every release. +- Create signed or otherwise controlled release tags according to your release process. +- Update documentation when the public API contract or output mapping changes. + +## Roadmap + +- Add a live integration test against a deployed TrustSignal verification endpoint +- Publish the action from a dedicated public repository root +- Add example workflows for release pipelines and provenance retention patterns diff --git a/github-actions/trustsignal-verify-artifact/SECURITY.md b/github-actions/trustsignal-verify-artifact/SECURITY.md new file mode 100644 index 0000000..c351639 --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/SECURITY.md @@ -0,0 +1,23 @@ +# Security Policy + +## Reporting A Vulnerability + +Report suspected vulnerabilities privately to `security@trustsignal.dev`. + +Include: + +- a clear description of the issue +- reproduction steps +- affected versions or commit references +- impact assessment if known + +Do not open public GitHub issues for suspected security vulnerabilities. + +## Sensitive Information + +- Do not include secrets, API keys, tokens, customer data, or private receipts in reports. +- Sanitize logs, payloads, and screenshots before sharing them. + +## Responsible Disclosure + +TrustSignal reviews reports as quickly as possible, validates impact, and coordinates remediation and disclosure timing with reporters when appropriate. diff --git a/github-actions/trustsignal-verify-artifact/action.yml b/github-actions/trustsignal-verify-artifact/action.yml new file mode 100644 index 0000000..b0ee087 --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/action.yml @@ -0,0 +1,42 @@ +name: TrustSignal Verify Artifact +description: Verify build artifacts with TrustSignal and capture signed verification receipt metadata in GitHub Actions. +author: TrustSignal +branding: + icon: shield + color: blue + +inputs: + api_base_url: + description: Base URL for the TrustSignal public API. + required: true + api_key: + description: API key for the TrustSignal public API. + required: true + artifact_path: + description: Local path to the artifact to hash and verify. + required: false + artifact_hash: + description: Precomputed artifact hash to verify instead of hashing a file. + required: false + source: + description: Source label for the artifact verification request. + required: false + default: github-actions + fail_on_mismatch: + description: Fail the action when TrustSignal does not return a valid verification result. + required: false + default: "true" + +outputs: + verification_id: + description: TrustSignal verification identifier returned by the API, or a compatibility alias to receipt_id when the API omits a separate verification id. + status: + description: Normalized verification status returned by TrustSignal. + receipt_id: + description: TrustSignal receipt identifier returned by the API. + receipt_signature: + description: Signed receipt signature returned by the API. + +runs: + using: node20 + main: dist/index.js diff --git a/github-actions/trustsignal-verify-artifact/dist/index.js b/github-actions/trustsignal-verify-artifact/dist/index.js new file mode 100644 index 0000000..05625ea --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/dist/index.js @@ -0,0 +1,281 @@ +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); + +function getInput(name, options = {}) { + const envName = `INPUT_${name.replace(/ /g, '_').toUpperCase()}`; + const raw = process.env[envName]; + const value = typeof raw === 'string' ? raw.trim() : ''; + + if (options.required && !value) { + throw new Error(`Missing required input: ${name}`); + } + + return value; +} + +function getBooleanInput(name, defaultValue = false) { + const value = getInput(name); + if (!value) return defaultValue; + + const normalized = value.toLowerCase(); + if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) return true; + if (['false', '0', 'no', 'n', 'off'].includes(normalized)) return false; + + throw new Error(`Invalid boolean input for ${name}: expected true or false`); +} + +function setOutput(name, value) { + const outputPath = process.env.GITHUB_OUTPUT; + if (!outputPath) { + process.stdout.write(`${name}=${value}\n`); + return; + } + + fs.appendFileSync(outputPath, `${name}=${String(value ?? '')}\n`, 'utf8'); +} + +function setFailed(message) { + process.stderr.write(`::error::${message}\n`); + process.exitCode = 1; +} + +function sha256File(filePath) { + const absolutePath = path.resolve(filePath); + if (!fs.existsSync(absolutePath)) { + throw new Error(`Artifact file not found: ${absolutePath}`); + } + + const stats = fs.statSync(absolutePath); + if (!stats.isFile()) { + throw new Error(`Artifact path is not a file: ${absolutePath}`); + } + + const hash = crypto.createHash('sha256'); + const fileBuffer = fs.readFileSync(absolutePath); + hash.update(fileBuffer); + return hash.digest('hex'); +} + +function validateHash(value) { + const normalized = value.toLowerCase().replace(/^sha256:/, ''); + if (!/^[a-f0-9]{64}$/.test(normalized)) { + throw new Error('artifact_hash must be a valid SHA-256 hex digest'); + } + return normalized; +} + +function normalizeBaseUrl(value) { + let url; + + try { + url = new URL(value); + } catch { + throw new Error('api_base_url must be a valid URL'); + } + + if (!/^https?:$/.test(url.protocol)) { + throw new Error('api_base_url must use http or https'); + } + + url.pathname = url.pathname.replace(/\/+$/, ''); + url.search = ''; + url.hash = ''; + return url.toString().replace(/\/$/, ''); +} + +function getGitHubContext() { + return { + repository: process.env.GITHUB_REPOSITORY || undefined, + runId: process.env.GITHUB_RUN_ID || undefined, + workflow: process.env.GITHUB_WORKFLOW || undefined, + actor: process.env.GITHUB_ACTOR || undefined, + sha: process.env.GITHUB_SHA || undefined + }; +} + +function buildVerificationRequest({ artifactHash, artifactPath, source }) { + const github = getGitHubContext(); + const provider = source.replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 64) || 'github-actions'; + + return { + artifact: { + hash: artifactHash, + algorithm: 'sha256' + }, + source: { + provider, + repository: github.repository, + workflow: github.workflow, + runId: github.runId, + actor: github.actor, + commit: github.sha + }, + metadata: { + ...(artifactPath ? { artifactPath } : {}) + } + }; +} + +function deriveStatus(responseBody) { + return ( + responseBody.status || + responseBody.verificationStatus || + responseBody.result || + (responseBody.verified === true ? 'verified' : undefined) || + (responseBody.valid === true ? 'verified' : undefined) || + (responseBody.match === true ? 'verified' : undefined) || + 'unknown' + ); +} + +function extractReceiptSignature(responseBody) { + if (typeof responseBody.receipt_signature === 'string') { + return responseBody.receipt_signature; + } + + if (typeof responseBody.receiptSignature === 'string') { + return responseBody.receiptSignature; + } + + if ( + responseBody.receiptSignature && + typeof responseBody.receiptSignature.signature === 'string' + ) { + return responseBody.receiptSignature.signature; + } + + return ''; +} + +function isVerificationValid(responseBody, status) { + if ([responseBody.valid, responseBody.verified, responseBody.match].includes(true)) { + return true; + } + + if ([responseBody.valid, responseBody.verified, responseBody.match].includes(false)) { + return false; + } + + const normalizedStatus = String(status || '').toLowerCase(); + if (['verified', 'valid', 'match', 'matched', 'success', 'ok'].includes(normalizedStatus)) { + return true; + } + + if (['invalid', 'mismatch', 'failed', 'error', 'tampered'].includes(normalizedStatus)) { + return false; + } + + return false; +} + +function extractMessage(responseBody) { + if (!responseBody || typeof responseBody !== 'object') { + return ''; + } + + return ( + responseBody.error || + responseBody.message || + responseBody.detail || + responseBody.title || + '' + ); +} + +async function parseJsonResponse(response) { + const rawBody = await response.text(); + if (!rawBody) { + return {}; + } + + try { + return JSON.parse(rawBody); + } catch { + throw new Error(`TrustSignal API returned a non-JSON response with status ${response.status}`); + } +} + +async function callVerificationApi({ apiBaseUrl, apiKey, artifactHash, artifactPath, source }) { + const endpoint = `${apiBaseUrl}/api/v1/verify`; + const payload = buildVerificationRequest({ artifactHash, artifactPath, source }); + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-api-key': apiKey + }, + body: JSON.stringify(payload) + }); + + const responseBody = await parseJsonResponse(response); + + if (!response.ok) { + const message = extractMessage(responseBody); + throw new Error( + `TrustSignal API request failed with status ${response.status}${ + message ? `: ${message}` : '' + }` + ); + } + + return responseBody || {}; +} + +async function run() { + try { + const apiBaseUrl = normalizeBaseUrl(getInput('api_base_url', { required: true })); + const apiKey = getInput('api_key', { required: true }); + const artifactPath = getInput('artifact_path'); + const providedArtifactHash = getInput('artifact_hash'); + const source = getInput('source') || 'github-actions'; + const failOnMismatch = getBooleanInput('fail_on_mismatch', true); + + if (!artifactPath && !providedArtifactHash) { + throw new Error('Either artifact_path or artifact_hash must be provided'); + } + + if (artifactPath && providedArtifactHash) { + throw new Error('Provide only one of artifact_path or artifact_hash'); + } + + const artifactHash = artifactPath + ? sha256File(artifactPath) + : validateHash(providedArtifactHash); + + const responseBody = await callVerificationApi({ + apiBaseUrl, + apiKey, + artifactHash, + artifactPath, + source + }); + + const verificationId = + responseBody.verification_id || + responseBody.verificationId || + responseBody.id || + responseBody.receipt_id || + responseBody.receiptId || + ''; + const receiptId = responseBody.receipt_id || responseBody.receiptId || ''; + const status = deriveStatus(responseBody); + const receiptSignature = extractReceiptSignature(responseBody); + const isValid = isVerificationValid(responseBody, status); + + setOutput('verification_id', verificationId); + setOutput('status', status); + setOutput('receipt_id', receiptId); + setOutput('receipt_signature', receiptSignature); + + if (failOnMismatch && !isValid) { + throw new Error(`TrustSignal verification was not valid. Status: ${status}`); + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown action failure'; + setFailed(message); + } +} + +run(); diff --git a/github-actions/trustsignal-verify-artifact/docs/integration.md b/github-actions/trustsignal-verify-artifact/docs/integration.md new file mode 100644 index 0000000..9483297 --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/docs/integration.md @@ -0,0 +1,55 @@ +# Integration Guide + +## Overview + +`TrustSignal Verify Artifact` verifies build artifacts in CI, issues signed verification receipts, and returns receipt metadata that downstream systems can use for provenance and later verification workflows. + +## Verification Flow + +1. The action accepts either `artifact_path` or `artifact_hash`. +2. A SHA-256 digest is computed locally when a file path is provided. +3. The action sends the artifact identity and GitHub workflow context to `POST /api/v1/verify`. +4. TrustSignal returns verification metadata, including a receipt identifier and receipt signature. +5. The workflow stores `receipt_id` for later verification, audit, or provenance workflows. + +## Request Contract + +```json +{ + "artifact": { + "hash": "", + "algorithm": "sha256" + }, + "source": { + "provider": "github-actions", + "repository": "", + "workflow": "", + "runId": "", + "commit": "", + "actor": "" + }, + "metadata": { + "artifactPath": "" + } +} +``` + +## Outputs + +- `verification_id` +- `status` +- `receipt_id` +- `receipt_signature` + +If the API omits a distinct verification identifier, the action uses `receipt_id` as a compatibility alias for `verification_id`. + +## Current Limitations + +- The included test path uses a local fetch mock rather than a live TrustSignal deployment. +- Marketplace publication still requires extraction into a dedicated public repository. + +## Next Steps + +- Add a live integration test against a deployed TrustSignal API environment. +- Publish semantic version tags and maintain a stable major tag. +- Move this package to the repository root of a dedicated public action repository. diff --git a/github-actions/trustsignal-verify-artifact/package.json b/github-actions/trustsignal-verify-artifact/package.json new file mode 100644 index 0000000..f2b1d7e --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/package.json @@ -0,0 +1,36 @@ +{ + "name": "trustsignal-verify-artifact", + "version": "0.1.0", + "description": "GitHub Action for verifying build artifacts with TrustSignal and capturing signed verification receipts.", + "main": "dist/index.js", + "type": "commonjs", + "files": [ + "action.yml", + "dist", + "README.md", + "LICENSE" + ], + "scripts": { + "build": "mkdir -p dist && cp src/index.js dist/index.js", + "package": "npm run build", + "check": "node --check src/index.js && node --check dist/index.js", + "test:local": "node scripts/test-local.js", + "validate:local": "npm run check && npm run test:local" + }, + "keywords": [ + "github-action", + "trustsignal", + "verification", + "devsecops", + "ci-cd", + "supply-chain", + "artifact", + "provenance", + "compliance" + ], + "author": "TrustSignal", + "license": "MIT", + "engines": { + "node": ">=20" + } +} diff --git a/github-actions/trustsignal-verify-artifact/scripts/mock-fetch.js b/github-actions/trustsignal-verify-artifact/scripts/mock-fetch.js new file mode 100644 index 0000000..214f7ac --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/scripts/mock-fetch.js @@ -0,0 +1,50 @@ +const crypto = require('node:crypto'); + +function sha256(value) { + return crypto.createHash('sha256').update(value).digest('hex'); +} + +function jsonResponse(status, body) { + return { + ok: status >= 200 && status < 300, + status, + async text() { + return JSON.stringify(body); + } + }; +} + +global.fetch = async function mockFetch(url, options = {}) { + const parsedUrl = new URL(url); + const apiKey = options.headers && (options.headers['x-api-key'] || options.headers['X-API-Key']); + if (apiKey !== 'test-key') { + return jsonResponse(403, { error: 'Forbidden: invalid API key' }); + } + + if (parsedUrl.pathname === '/api/v1/verify' && options.method === 'POST') { + const payload = JSON.parse(options.body || '{}'); + const receiptId = process.env.MOCK_RECEIPT_ID || '00000000-0000-4000-8000-000000000001'; + const verificationId = process.env.MOCK_VERIFICATION_ID || `verify-${receiptId}`; + const validHash = process.env.MOCK_VALID_ARTIFACT_HASH || sha256('valid artifact'); + const isValid = + payload?.artifact?.hash === validHash && + payload?.artifact?.algorithm === 'sha256' && + payload?.source?.provider === 'local-test' && + payload?.source?.repository === 'trustsignal-dev/trustsignal-verify-artifact' && + payload?.source?.workflow === 'Artifact Verification' && + payload?.source?.runId === '12345' && + payload?.source?.actor === 'octocat' && + payload?.source?.commit === 'abc123def456' && + typeof payload?.metadata?.artifactPath === 'string'; + + return jsonResponse(200, { + verification_id: verificationId, + status: isValid ? 'verified' : 'invalid', + receipt_id: receiptId, + receipt_signature: `sig-${receiptId}`, + valid: isValid + }); + } + + return jsonResponse(404, { error: 'not_found' }); +}; diff --git a/github-actions/trustsignal-verify-artifact/scripts/test-local.js b/github-actions/trustsignal-verify-artifact/scripts/test-local.js new file mode 100644 index 0000000..c5e981e --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/scripts/test-local.js @@ -0,0 +1,105 @@ +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); + +function readOutputs(filePath) { + const raw = fs.readFileSync(filePath, 'utf8'); + return Object.fromEntries( + raw + .trim() + .split('\n') + .filter(Boolean) + .map((line) => { + const index = line.indexOf('='); + return [line.slice(0, index), line.slice(index + 1)]; + }) + ); +} + +function runAction({ artifactContents, failOnMismatch, receiptId }) { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'trustsignal-action-')); + const artifactPath = path.join(tempDir, 'artifact.txt'); + const outputPath = path.join(tempDir, 'github-output.txt'); + fs.writeFileSync(artifactPath, artifactContents, 'utf8'); + + const result = spawnSync( + process.execPath, + ['-r', './scripts/mock-fetch.js', 'dist/index.js'], + { + cwd: path.resolve(__dirname, '..'), + env: { + ...process.env, + INPUT_API_BASE_URL: 'https://api.trustsignal.dev', + INPUT_API_KEY: 'test-key', + INPUT_ARTIFACT_PATH: artifactPath, + INPUT_SOURCE: 'local-test', + INPUT_FAIL_ON_MISMATCH: String(failOnMismatch), + GITHUB_OUTPUT: outputPath, + GITHUB_RUN_ID: '12345', + GITHUB_REPOSITORY: 'trustsignal-dev/trustsignal-verify-artifact', + GITHUB_WORKFLOW: 'Artifact Verification', + GITHUB_ACTOR: 'octocat', + GITHUB_SHA: 'abc123def456', + MOCK_RECEIPT_ID: receiptId + }, + encoding: 'utf8' + } + ); + + const outputs = fs.existsSync(outputPath) ? readOutputs(outputPath) : {}; + return { + status: result.status, + stdout: result.stdout, + stderr: result.stderr, + outputs + }; +} + +function assert(condition, message) { + if (!condition) { + throw new Error(message); + } +} + +function main() { + const validRun = runAction({ + artifactContents: 'valid artifact', + failOnMismatch: true, + receiptId: '00000000-0000-4000-8000-000000000001' + }); + + const tamperedRun = runAction({ + artifactContents: 'tampered artifact', + failOnMismatch: false, + receiptId: '00000000-0000-4000-8000-000000000002' + }); + + const failingMismatchRun = runAction({ + artifactContents: 'tampered artifact', + failOnMismatch: true, + receiptId: '00000000-0000-4000-8000-000000000003' + }); + + assert(validRun.status === 0, `Expected valid run to succeed, got ${validRun.status}: ${validRun.stderr}`); + assert(validRun.outputs.verification_id === 'verify-00000000-0000-4000-8000-000000000001', 'Valid run verification_id mismatch'); + assert(validRun.outputs.receipt_id === '00000000-0000-4000-8000-000000000001', 'Valid run receipt_id mismatch'); + assert(validRun.outputs.status === 'verified', `Expected valid status to be verified, got ${validRun.outputs.status}`); + assert(validRun.outputs.receipt_signature === 'sig-00000000-0000-4000-8000-000000000001', 'Valid run receipt_signature mismatch'); + + assert(tamperedRun.status === 0, `Expected tampered run to complete when fail_on_mismatch=false, got ${tamperedRun.status}: ${tamperedRun.stderr}`); + assert(tamperedRun.outputs.verification_id === 'verify-00000000-0000-4000-8000-000000000002', 'Tampered run verification_id mismatch'); + assert(tamperedRun.outputs.receipt_id === '00000000-0000-4000-8000-000000000002', 'Tampered run receipt_id mismatch'); + assert(tamperedRun.outputs.status === 'invalid', `Expected tampered status to be invalid, got ${tamperedRun.outputs.status}`); + assert(tamperedRun.outputs.receipt_signature === 'sig-00000000-0000-4000-8000-000000000002', 'Tampered run receipt_signature mismatch'); + assert(failingMismatchRun.status !== 0, 'Expected mismatch run to fail when fail_on_mismatch=true'); + + process.stdout.write('Local action contract test passed\n'); +} + +try { + main(); +} catch (error) { + process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); + process.exit(1); +} diff --git a/github-actions/trustsignal-verify-artifact/src/index.js b/github-actions/trustsignal-verify-artifact/src/index.js new file mode 100644 index 0000000..05625ea --- /dev/null +++ b/github-actions/trustsignal-verify-artifact/src/index.js @@ -0,0 +1,281 @@ +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); + +function getInput(name, options = {}) { + const envName = `INPUT_${name.replace(/ /g, '_').toUpperCase()}`; + const raw = process.env[envName]; + const value = typeof raw === 'string' ? raw.trim() : ''; + + if (options.required && !value) { + throw new Error(`Missing required input: ${name}`); + } + + return value; +} + +function getBooleanInput(name, defaultValue = false) { + const value = getInput(name); + if (!value) return defaultValue; + + const normalized = value.toLowerCase(); + if (['true', '1', 'yes', 'y', 'on'].includes(normalized)) return true; + if (['false', '0', 'no', 'n', 'off'].includes(normalized)) return false; + + throw new Error(`Invalid boolean input for ${name}: expected true or false`); +} + +function setOutput(name, value) { + const outputPath = process.env.GITHUB_OUTPUT; + if (!outputPath) { + process.stdout.write(`${name}=${value}\n`); + return; + } + + fs.appendFileSync(outputPath, `${name}=${String(value ?? '')}\n`, 'utf8'); +} + +function setFailed(message) { + process.stderr.write(`::error::${message}\n`); + process.exitCode = 1; +} + +function sha256File(filePath) { + const absolutePath = path.resolve(filePath); + if (!fs.existsSync(absolutePath)) { + throw new Error(`Artifact file not found: ${absolutePath}`); + } + + const stats = fs.statSync(absolutePath); + if (!stats.isFile()) { + throw new Error(`Artifact path is not a file: ${absolutePath}`); + } + + const hash = crypto.createHash('sha256'); + const fileBuffer = fs.readFileSync(absolutePath); + hash.update(fileBuffer); + return hash.digest('hex'); +} + +function validateHash(value) { + const normalized = value.toLowerCase().replace(/^sha256:/, ''); + if (!/^[a-f0-9]{64}$/.test(normalized)) { + throw new Error('artifact_hash must be a valid SHA-256 hex digest'); + } + return normalized; +} + +function normalizeBaseUrl(value) { + let url; + + try { + url = new URL(value); + } catch { + throw new Error('api_base_url must be a valid URL'); + } + + if (!/^https?:$/.test(url.protocol)) { + throw new Error('api_base_url must use http or https'); + } + + url.pathname = url.pathname.replace(/\/+$/, ''); + url.search = ''; + url.hash = ''; + return url.toString().replace(/\/$/, ''); +} + +function getGitHubContext() { + return { + repository: process.env.GITHUB_REPOSITORY || undefined, + runId: process.env.GITHUB_RUN_ID || undefined, + workflow: process.env.GITHUB_WORKFLOW || undefined, + actor: process.env.GITHUB_ACTOR || undefined, + sha: process.env.GITHUB_SHA || undefined + }; +} + +function buildVerificationRequest({ artifactHash, artifactPath, source }) { + const github = getGitHubContext(); + const provider = source.replace(/[^a-zA-Z0-9._-]/g, '-').slice(0, 64) || 'github-actions'; + + return { + artifact: { + hash: artifactHash, + algorithm: 'sha256' + }, + source: { + provider, + repository: github.repository, + workflow: github.workflow, + runId: github.runId, + actor: github.actor, + commit: github.sha + }, + metadata: { + ...(artifactPath ? { artifactPath } : {}) + } + }; +} + +function deriveStatus(responseBody) { + return ( + responseBody.status || + responseBody.verificationStatus || + responseBody.result || + (responseBody.verified === true ? 'verified' : undefined) || + (responseBody.valid === true ? 'verified' : undefined) || + (responseBody.match === true ? 'verified' : undefined) || + 'unknown' + ); +} + +function extractReceiptSignature(responseBody) { + if (typeof responseBody.receipt_signature === 'string') { + return responseBody.receipt_signature; + } + + if (typeof responseBody.receiptSignature === 'string') { + return responseBody.receiptSignature; + } + + if ( + responseBody.receiptSignature && + typeof responseBody.receiptSignature.signature === 'string' + ) { + return responseBody.receiptSignature.signature; + } + + return ''; +} + +function isVerificationValid(responseBody, status) { + if ([responseBody.valid, responseBody.verified, responseBody.match].includes(true)) { + return true; + } + + if ([responseBody.valid, responseBody.verified, responseBody.match].includes(false)) { + return false; + } + + const normalizedStatus = String(status || '').toLowerCase(); + if (['verified', 'valid', 'match', 'matched', 'success', 'ok'].includes(normalizedStatus)) { + return true; + } + + if (['invalid', 'mismatch', 'failed', 'error', 'tampered'].includes(normalizedStatus)) { + return false; + } + + return false; +} + +function extractMessage(responseBody) { + if (!responseBody || typeof responseBody !== 'object') { + return ''; + } + + return ( + responseBody.error || + responseBody.message || + responseBody.detail || + responseBody.title || + '' + ); +} + +async function parseJsonResponse(response) { + const rawBody = await response.text(); + if (!rawBody) { + return {}; + } + + try { + return JSON.parse(rawBody); + } catch { + throw new Error(`TrustSignal API returned a non-JSON response with status ${response.status}`); + } +} + +async function callVerificationApi({ apiBaseUrl, apiKey, artifactHash, artifactPath, source }) { + const endpoint = `${apiBaseUrl}/api/v1/verify`; + const payload = buildVerificationRequest({ artifactHash, artifactPath, source }); + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'content-type': 'application/json', + 'x-api-key': apiKey + }, + body: JSON.stringify(payload) + }); + + const responseBody = await parseJsonResponse(response); + + if (!response.ok) { + const message = extractMessage(responseBody); + throw new Error( + `TrustSignal API request failed with status ${response.status}${ + message ? `: ${message}` : '' + }` + ); + } + + return responseBody || {}; +} + +async function run() { + try { + const apiBaseUrl = normalizeBaseUrl(getInput('api_base_url', { required: true })); + const apiKey = getInput('api_key', { required: true }); + const artifactPath = getInput('artifact_path'); + const providedArtifactHash = getInput('artifact_hash'); + const source = getInput('source') || 'github-actions'; + const failOnMismatch = getBooleanInput('fail_on_mismatch', true); + + if (!artifactPath && !providedArtifactHash) { + throw new Error('Either artifact_path or artifact_hash must be provided'); + } + + if (artifactPath && providedArtifactHash) { + throw new Error('Provide only one of artifact_path or artifact_hash'); + } + + const artifactHash = artifactPath + ? sha256File(artifactPath) + : validateHash(providedArtifactHash); + + const responseBody = await callVerificationApi({ + apiBaseUrl, + apiKey, + artifactHash, + artifactPath, + source + }); + + const verificationId = + responseBody.verification_id || + responseBody.verificationId || + responseBody.id || + responseBody.receipt_id || + responseBody.receiptId || + ''; + const receiptId = responseBody.receipt_id || responseBody.receiptId || ''; + const status = deriveStatus(responseBody); + const receiptSignature = extractReceiptSignature(responseBody); + const isValid = isVerificationValid(responseBody, status); + + setOutput('verification_id', verificationId); + setOutput('status', status); + setOutput('receipt_id', receiptId); + setOutput('receipt_signature', receiptSignature); + + if (failOnMismatch && !isValid) { + throw new Error(`TrustSignal verification was not valid. Status: ${status}`); + } + } catch (error) { + const message = error instanceof Error ? error.message : 'Unknown action failure'; + setFailed(message); + } +} + +run(); diff --git a/openapi.yaml b/openapi.yaml index e466577..73c4c6a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -24,8 +24,8 @@ paths: tags: [Verification] summary: Create a verification and receive a signed verification receipt description: | - Submit a verification request from an existing workflow. TrustSignal returns verification signals, - a signed verification receipt, and verifiable provenance metadata that can be used for later verification. + Submit a generic artifact verification request from an existing workflow. TrustSignal returns + a signed verification receipt and persists it for later verification. security: - ApiKeyAuth: [] requestBody: @@ -33,54 +33,38 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/VerificationRequest' + $ref: '#/components/schemas/ArtifactVerificationRequest' examples: default: summary: Verification request value: - bundleId: verification-2026-03-12-001 - transactionType: deed_transfer - ron: - provider: source-system - notaryId: NOTARY-EXAMPLE-01 - commissionState: IL - sealPayload: simulated-seal-payload - doc: - docHash: "0x8b7b2f52f2a2e19f8f3fe0d815d1c1d8d1e0d120e8cc60d1baf5e7a6f9d211aa" - policy: - profile: CONTROL_CC_001 - property: - parcelId: PARCEL-EXAMPLE-1001 - county: Cook - state: IL - timestamp: "2026-03-12T15:24:00.000Z" + artifact: + hash: 2f77668a9dfbf8d5847cf2d5d0370740e0c0601b4f061c1181f58c77c2b8f486 + algorithm: sha256 + source: + provider: github-actions + repository: TrustSignal-dev/TrustSignal-Verify-Artifact + workflow: Verify Build Artifact + runId: "12345" + commit: abc123def456 + actor: octocat + metadata: + artifactPath: dist/release.txt responses: '200': description: Verification completed and a signed verification receipt was issued. content: application/json: schema: - $ref: '#/components/schemas/VerificationResponse' + $ref: '#/components/schemas/ArtifactVerificationResponse' examples: default: summary: Verification response value: - receiptVersion: '2.0' - decision: ALLOW - reasons: - - receipt issued + verificationId: 2c17d2f5-4de6-48c3-b22c-0b7ea9eb5c0a receiptId: 2c17d2f5-4de6-48c3-b22c-0b7ea9eb5c0a - receiptHash: "0x4e7f2ce9d3f7a8d3b0e4c9f2aa17fd59d6b4fda2d7b7b7d1cce8124d7ee39d04" - receiptSignature: - alg: EdDSA - kid: trustsignal-current - signature: eyJleGFtcGxlIjoic2lnbmVkLXJlY2VpcHQifQ - anchor: - status: PENDING - subjectDigest: "0x8c0f95cda31274e7b61adfd1dd1e0c03a4b96f78d90da52d42fd93d9a38fc112" - subjectVersion: trustsignal.anchor_subject.v1 - revocation: - status: ACTIVE + receiptSignature: eyJleGFtcGxlIjoic2lnbmVkLXJlY2VpcHQifQ + status: verified '400': $ref: '#/components/responses/BadRequest' '401': @@ -94,27 +78,49 @@ paths: /api/v1/receipt/{receiptId}: get: tags: [Receipts] - summary: Retrieve a stored verification receipt + summary: Retrieve a public-safe artifact receipt view description: | - Return the stored receipt view for a previously created verification, - including receipt metadata, the canonical receipt payload, and a PDF URL. - security: - - ApiKeyAuth: [] + Return a compact inspection view for an artifact receipt. The response is safe for + external display and omits internal scoring, raw signatures, and backend details. + Artifact receipt identifiers are unguessable UUIDs, which allows read-only inspection + without exposing private persistence. Legacy non-artifact receipts still require read-scoped auth. + security: [] parameters: - $ref: '#/components/parameters/ReceiptId' responses: '200': - description: Stored receipt returned. + description: Public artifact receipt view returned. content: application/json: schema: - $ref: '#/components/schemas/VerificationReceipt' + $ref: '#/components/schemas/PublicArtifactReceipt' + '400': + $ref: '#/components/responses/BadRequest' + '404': + $ref: '#/components/responses/NotFound' + '429': + $ref: '#/components/responses/TooManyRequests' + '503': + $ref: '#/components/responses/ServiceUnavailable' + /api/v1/receipt/{receiptId}/summary: + get: + tags: [Receipts] + summary: Retrieve a compact partner-ready receipt summary + description: | + Return a stable summary payload that partner platforms can render as a verification badge + or evidence-panel status without understanding TrustSignal internals. + security: [] + parameters: + - $ref: '#/components/parameters/ReceiptId' + responses: + '200': + description: Compact verification summary returned. + content: + application/json: + schema: + $ref: '#/components/schemas/PublicArtifactReceiptSummary' '400': $ref: '#/components/responses/BadRequest' - '401': - $ref: '#/components/responses/Unauthorized' - '403': - $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '429': @@ -154,19 +160,24 @@ paths: tags: [Lifecycle] summary: Check later verification status for a stored receipt description: | - Recompute receipt integrity and return the current verification status for later verification. - This endpoint does not accept a request body. + Compare a supplied artifact hash against a stored receipt and return the current verification status. security: - ApiKeyAuth: [] parameters: - $ref: '#/components/parameters/ReceiptId' + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/ArtifactReceiptVerifyRequest' responses: '200': description: Receipt verification status returned. content: application/json: schema: - $ref: '#/components/schemas/VerificationStatus' + $ref: '#/components/schemas/ArtifactReceiptVerificationStatus' '400': $ref: '#/components/responses/BadRequest' '401': @@ -462,6 +473,233 @@ components: type: string format: date-time description: Caller-provided event timestamp. + ArtifactVerificationRequest: + type: object + additionalProperties: false + required: + - artifact + - source + properties: + artifact: + type: object + additionalProperties: false + required: + - hash + - algorithm + properties: + hash: + type: string + pattern: '^[A-Fa-f0-9]{64}$' + algorithm: + type: string + enum: [sha256] + source: + type: object + additionalProperties: false + required: + - provider + properties: + provider: + type: string + repository: + type: string + workflow: + type: string + runId: + type: string + commit: + type: string + actor: + type: string + metadata: + type: object + additionalProperties: false + properties: + artifactPath: + type: string + ArtifactVerificationResponse: + type: object + additionalProperties: false + required: + - verificationId + - receiptId + - receiptSignature + - status + properties: + verificationId: + type: string + format: uuid + receiptId: + type: string + format: uuid + receiptSignature: + type: string + status: + type: string + enum: [verified] + ArtifactReceiptVerifyRequest: + type: object + additionalProperties: false + required: + - artifact + properties: + artifact: + type: object + additionalProperties: false + required: + - hash + - algorithm + properties: + hash: + type: string + pattern: '^[A-Fa-f0-9]{64}$' + algorithm: + type: string + enum: [sha256] + ArtifactReceiptVerificationStatus: + type: object + additionalProperties: false + required: + - verified + - integrityVerified + - signatureVerified + - status + - receiptId + - receiptSignature + - storedHash + - recomputedHash + properties: + verified: + type: boolean + integrityVerified: + type: boolean + signatureVerified: + type: boolean + status: + type: string + enum: [verified, mismatch] + receiptId: + type: string + format: uuid + receiptSignature: + type: string + storedHash: + type: string + recomputedHash: + type: string + PublicArtifactReceipt: + type: object + additionalProperties: false + required: + - receiptId + - artifact + - source + - status + - createdAt + - receiptSignature + properties: + receiptId: + type: string + format: uuid + artifact: + type: object + additionalProperties: false + required: + - hash + - algorithm + properties: + hash: + type: string + pattern: '^[A-Fa-f0-9]{64}$' + algorithm: + type: string + enum: [sha256] + source: + type: object + additionalProperties: false + required: + - provider + properties: + provider: + type: string + repository: + type: string + workflow: + type: string + runId: + type: string + commit: + type: string + actor: + type: string + status: + type: string + createdAt: + type: string + format: date-time + receiptSignature: + type: object + additionalProperties: false + required: + - alg + - kid + properties: + alg: + type: string + kid: + type: string + verificationUrl: + type: string + format: uri + PublicArtifactReceiptSummary: + type: object + additionalProperties: false + required: + - receiptId + - status + - integrityState + - issuedAt + - source + - display + properties: + receiptId: + type: string + format: uuid + status: + type: string + integrityState: + type: string + enum: [valid, check] + issuedAt: + type: string + format: date-time + source: + type: object + additionalProperties: false + required: + - provider + properties: + provider: + type: string + repository: + type: string + workflow: + type: string + display: + type: object + additionalProperties: false + required: + - label + - tone + - statement + properties: + label: + type: string + tone: + type: string + enum: [success, warning] + statement: + type: string VerificationResponse: type: object additionalProperties: true diff --git a/postman/TrustSignal.local.postman_environment.json b/postman/TrustSignal.local.postman_environment.json index d2213ef..4f38fbe 100644 --- a/postman/TrustSignal.local.postman_environment.json +++ b/postman/TrustSignal.local.postman_environment.json @@ -26,6 +26,12 @@ "type": "default", "enabled": true }, + { + "key": "pdf_url", + "value": "replace-after-verify", + "type": "default", + "enabled": true + }, { "key": "issuer_id", "value": "replace-for-revoke-tests", diff --git a/postman/TrustSignal.postman_collection.json b/postman/TrustSignal.postman_collection.json index b2100c3..4325536 100644 --- a/postman/TrustSignal.postman_collection.json +++ b/postman/TrustSignal.postman_collection.json @@ -1,7 +1,7 @@ { "info": { "name": "TrustSignal Evaluator Trial Path", - "description": "Public evaluator collection for the TrustSignal verification lifecycle. This collection demonstrates how to submit a verification request, receive signed verification receipts and verification signals, retrieve the stored receipt, run later verification, and review authorized lifecycle actions using the public contract only.", + "description": "Public evaluator collection for the TrustSignal verification lifecycle. Start with docs/partner-eval/start-here.md, then use this collection to submit a verification request, receive signed verification receipts and verification signals, retrieve the stored receipt, run later verification, and review authorized lifecycle actions using the public contract only.", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" }, "variable": [ @@ -21,6 +21,10 @@ "key": "receipt_id", "value": "{{receipt_id}}" }, + { + "key": "pdf_url", + "value": "{{pdf_url}}" + }, { "key": "issuer_id", "value": "{{issuer_id}}" @@ -46,6 +50,9 @@ "if (response.receiptId) {", " pm.environment.set('verification_id', response.receiptId);", " pm.environment.set('receipt_id', response.receiptId);", + "}", + "if (response.receiptVersion && response.receiptId) {", + " pm.environment.set('pdf_url', pm.variables.get('base_url') + '/api/v1/receipt/' + response.receiptId + '/pdf');", "}" ], "type": "text/javascript" @@ -66,7 +73,7 @@ ], "body": { "mode": "raw", - "raw": "{\n \"bundleId\": \"verification-2026-03-12-001\",\n \"transactionType\": \"deed_transfer\",\n \"ron\": {\n \"provider\": \"source-system\",\n \"notaryId\": \"NOTARY-EXAMPLE-01\",\n \"commissionState\": \"IL\",\n \"sealPayload\": \"simulated-seal-payload\"\n },\n \"doc\": {\n \"docHash\": \"0x8b7b2f52f2a2e19f8f3fe0d815d1c1d8d1e0d120e8cc60d1baf5e7a6f9d211aa\"\n },\n \"policy\": {\n \"profile\": \"CONTROL_CC_001\"\n },\n \"property\": {\n \"parcelId\": \"PARCEL-EXAMPLE-1001\",\n \"county\": \"Cook\",\n \"state\": \"IL\"\n },\n \"timestamp\": \"2026-03-12T15:24:00.000Z\"\n}" + "raw": "{\n \"bundleId\": \"verification-2026-03-12-start-here\",\n \"transactionType\": \"deed_transfer\",\n \"ron\": {\n \"provider\": \"source-system\",\n \"notaryId\": \"NOTARY-EXAMPLE-01\",\n \"commissionState\": \"IL\",\n \"sealPayload\": \"simulated-seal-payload\"\n },\n \"doc\": {\n \"docHash\": \"0x8b7b2f52f2a2e19f8f3fe0d815d1c1d8d1e0d120e8cc60d1baf5e7a6f9d211aa\"\n },\n \"policy\": {\n \"profile\": \"CONTROL_CC_001\"\n },\n \"property\": {\n \"parcelId\": \"PARCEL-EVAL-1001\",\n \"county\": \"Cook\",\n \"state\": \"IL\"\n },\n \"timestamp\": \"2026-03-12T18:24:00.000Z\"\n}" }, "description": "Submit a public verification request from an existing workflow. The response should include verification signals, a signed verification receipt, and public-safe verifiable provenance metadata for later verification.", "url": { @@ -110,7 +117,34 @@ "response": [] }, { - "name": "3. Run Later Verification", + "name": "3. Download Receipt PDF", + "request": { + "method": "GET", + "header": [ + { + "key": "x-api-key", + "value": "{{api_key}}" + } + ], + "description": "Download the PDF rendering of the stored verification receipt when the evaluator wants the receipt-ready artifact.", + "url": { + "raw": "{{base_url}}/api/v1/receipt/{{receipt_id}}/pdf", + "host": [ + "{{base_url}}" + ], + "path": [ + "api", + "v1", + "receipt", + "{{receipt_id}}", + "pdf" + ] + } + }, + "response": [] + }, + { + "name": "4. Run Later Verification", "request": { "method": "POST", "header": [ @@ -137,7 +171,7 @@ "response": [] }, { - "name": "4. Revoke Receipt", + "name": "5. Revoke Receipt", "request": { "method": "POST", "header": [ @@ -176,7 +210,7 @@ "response": [] }, { - "name": "5. Review Provenance State", + "name": "6. Review Provenance State", "request": { "method": "POST", "header": [ diff --git a/scripts/security-readiness.ts b/scripts/security-readiness.ts new file mode 100644 index 0000000..1870e29 --- /dev/null +++ b/scripts/security-readiness.ts @@ -0,0 +1,101 @@ +import fs from "node:fs"; +import path from "node:path"; + +type CheckResult = { + status: "present" | "partial" | "missing"; + details: string; +}; + +const repoRoot = path.resolve(__dirname, ".."); + +function exists(relativePath: string): boolean { + return fs.existsSync(path.join(repoRoot, relativePath)); +} + +function evaluate(): Record { + const workflowsPresent = + exists(".github/workflows/dependency-review.yml") && + exists(".github/workflows/trivy.yml") && + exists(".github/workflows/scorecard.yml") && + exists(".github/workflows/zizmor.yml"); + + const dependencyScanning = + exists(".github/dependabot.yml") && exists(".github/workflows/dependency-review.yml"); + + const branchProtectionIndicators = + exists("docs/github-settings-checklist.md") && + (exists("scripts/apply-github-branch-protection.sh") || exists(".github/pull_request_template.md")); + + const ciSecurityTools = + exists(".github/workflows/trivy.yml") && + exists(".github/workflows/zizmor.yml") && + exists(".github/workflows/scorecard.yml"); + + return { + "GitHub workflows present": { + status: workflowsPresent ? "present" : "missing", + details: workflowsPresent + ? "Dependency Review, Trivy, Scorecard, and zizmor workflow files exist." + : "One or more expected workflow files are missing.", + }, + "Dependency scanning enabled": { + status: dependencyScanning ? "present" : "partial", + details: dependencyScanning + ? "Dependabot configuration and dependency review workflow are present in-repo." + : "Repository automation is incomplete or depends on manual GitHub feature enablement.", + }, + "Branch protection indicators": { + status: branchProtectionIndicators ? "partial" : "missing", + details: branchProtectionIndicators + ? "Repository contains documentation or helper automation for branch protection, but actual GitHub rules must be verified manually." + : "No branch protection guidance or helper indicators were found.", + }, + "CI security tools present": { + status: ciSecurityTools ? "present" : "partial", + details: ciSecurityTools + ? "Repository-level CI security tooling is present for vulnerabilities, Scorecard, and workflow linting." + : "Only part of the expected CI security tooling is present.", + }, + }; +} + +function renderMarkdown(results: Record): string { + const rows = Object.entries(results) + .map(([name, result]) => `| ${name} | ${result.status} | ${result.details} |`) + .join("\n"); + + return `# TrustSignal Security Posture Snapshot + +Generated: ${new Date().toISOString()} + +> This report summarizes repository-visible security governance indicators. It is a posture snapshot, not proof that all related GitHub or infrastructure settings are enabled in production. + +## Checks + +| Check | Status | Details | +| --- | --- | --- | +${rows} + +## Interpretation + +- \`present\` means the expected repository file or automation exists. +- \`partial\` means repository indicators exist, but the control still depends on manual GitHub or infrastructure verification. +- \`missing\` means the repository does not currently provide the expected indicator. + +## Manual Follow-Up + +- Verify branch protection or rulesets directly in GitHub. +- Verify Dependency Graph, Dependabot alerts, and code scanning are enabled in repository settings. +- Capture dated screenshots or exports if the result will be used as audit evidence. +`; +} + +function main(): void { + const results = evaluate(); + const outputPath = path.join(repoRoot, "docs/compliance/security-posture.md"); + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, renderMarkdown(results)); + console.log(JSON.stringify(results, null, 2)); +} + +main(); diff --git a/scripts/soc2-readiness.ts b/scripts/soc2-readiness.ts new file mode 100644 index 0000000..9c5f1c8 --- /dev/null +++ b/scripts/soc2-readiness.ts @@ -0,0 +1,184 @@ +import fs from "node:fs"; +import path from "node:path"; + +type CategoryName = + | "Access Control" + | "Infrastructure Security" + | "Secure Development" + | "Monitoring" + | "Secrets Management" + | "Incident Response" + | "Data Protection" + | "Compliance Documentation"; + +type CategoryResult = { + score: 0 | 1 | 2 | 3; + rationale: string; + remediation: string[]; +}; + +const repoRoot = path.resolve(__dirname, ".."); + +function exists(relativePath: string): boolean { + return fs.existsSync(path.join(repoRoot, relativePath)); +} + +function categoryResults(): Record { + return { + "Access Control": { + score: exists("docs/github-settings-checklist.md") ? 2 : 1, + rationale: + "Repository documentation covers branch protection and review expectations, but in-repo evidence does not prove completed access reviews or enforced GitHub settings.", + remediation: [ + "Capture recurring access review evidence for GitHub and production systems.", + "Enable and verify branch protection or rulesets with required reviews on main.", + ], + }, + "Infrastructure Security": { + score: exists(".github/workflows/trivy.yml") && exists(".github/workflows/scorecard.yml") ? 2 : 1, + rationale: + "Repository-level security workflows exist for dependency review, Trivy, and Scorecard, but infrastructure controls still require manual verification outside the repo.", + remediation: [ + "Document environment hardening baselines and infrastructure ownership.", + "Capture operational evidence for backup, recovery, and hosted-service security settings.", + ], + }, + "Secure Development": { + score: + exists(".github/pull_request_template.md") && + exists(".github/workflows/dependency-review.yml") && + exists(".github/workflows/zizmor.yml") + ? 3 + : 1, + rationale: + "Pull request review guidance and security-focused CI checks provide strong repository-level secure development coverage for a readiness baseline.", + remediation: [ + "Add documented secure code review ownership and periodic retrospective review of findings.", + ], + }, + Monitoring: { + score: exists("docs/compliance/security-posture.md") ? 2 : 1, + rationale: + "The repository can generate a security posture snapshot and retain CI scan outputs, but ongoing production monitoring evidence is not proven by repository files alone.", + remediation: [ + "Document log review cadence, alert routing, and monitored systems.", + "Attach monitoring exports or screenshots for operational environments.", + ], + }, + "Secrets Management": { + score: exists("SECURITY_CHECKLIST.md") || exists("apps/api/.env.example") ? 2 : 1, + rationale: + "TrustSignal guidance prohibits hardcoded secrets and uses environment-based configuration, but rotation cadence and vault evidence are not yet captured in this framework.", + remediation: [ + "Track secret rotation ownership and review cadence.", + "Collect evidence that production secrets are stored and rotated using approved mechanisms.", + ], + }, + "Incident Response": { + score: exists("docs/compliance/policies/incident-response-policy.md") ? 2 : 0, + rationale: + "A formal policy template exists, but exercised incident records, communication drills, and post-incident evidence are not yet included.", + remediation: [ + "Run a tabletop exercise and retain the output.", + "Define severity levels, contact paths, and evidence preservation procedures in operating records.", + ], + }, + "Data Protection": { + score: + exists("docs/compliance/policies/data-retention-policy.md") && exists("docs/security-summary.md") ? 2 : 1, + rationale: + "Data handling and retention guidance now exists, but applied retention schedules and production evidence still need to be collected.", + remediation: [ + "Define retention windows by evidence and operational data category.", + "Capture proof of encryption, access controls, and disposal procedures where applicable.", + ], + }, + "Compliance Documentation": { + score: + exists("docs/compliance/soc2/controls.md") && + exists("docs/compliance/soc2/readiness-checklist.md") && + exists("docs/compliance/soc2/readiness-report.md") + ? 3 + : 2, + rationale: + "The repository contains a structured readiness framework, policy templates, and generated reporting suitable for a mock-audit baseline.", + remediation: [ + "Assign document owners and refresh cadence for each policy and evidence tracker.", + ], + }, + }; +} + +function buildReport(): { markdown: string; percentage: number; results: Record } { + const results = categoryResults(); + const categoryEntries = Object.entries(results) as Array<[CategoryName, CategoryResult]>; + const totalScore = categoryEntries.reduce((sum, [, item]) => sum + item.score, 0); + const maxScore = categoryEntries.length * 3; + const percentage = Math.round((totalScore / maxScore) * 100); + + const remediationItems = categoryEntries + .flatMap(([name, item]) => + item.score < 3 ? item.remediation.map((entry) => `- ${name}: ${entry}`) : [], + ) + .join("\n"); + + const tableRows = categoryEntries + .map( + ([name, item]) => + `| ${name} | ${item.score} / 3 | ${item.rationale} |`, + ) + .join("\n"); + + const markdown = `# TrustSignal SOC 2 Readiness Report + +Generated: ${new Date().toISOString()} + +> This report is an internal readiness snapshot aligned to SOC 2 Security criteria. It is intended for planning and gap remediation. It is not an audit opinion and does not imply SOC 2 certification. + +## Overall Readiness Score + +${percentage}% + +## Category Scores + +| Category | Score | Notes | +| --- | --- | --- | +${tableRows} + +## Recommended Remediation Items + +${remediationItems} + +## Scoring Model + +- 0 = missing +- 1 = partial +- 2 = implemented +- 3 = strong + +## Notes + +- Scores are based on repository-visible controls and documentation only. +- GitHub UI configuration, infrastructure operations, access reviews, and restore testing still require manual verification. +- This report should be refreshed when major security workflows, policies, or governance controls change. +`; + + return { markdown, percentage, results }; +} + +function main(): void { + const outputPath = path.join(repoRoot, "docs/compliance/soc2/readiness-report.md"); + const { markdown, percentage, results } = buildReport(); + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, markdown); + + const summary = { + generatedAt: new Date().toISOString(), + scorePercentage: percentage, + categories: results, + }; + + console.log(JSON.stringify(summary, null, 2)); +} + +main(); diff --git a/vercel.api.json b/vercel.api.json deleted file mode 100644 index 332dc69..0000000 --- a/vercel.api.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "https://openapi.vercel.sh/vercel.json", - "version": 2, - "buildCommand": "npm --workspace packages/core run build && npm --workspace apps/api run build", - "builds": [ - { - "src": "api/[...path].ts", - "use": "@vercel/node" - } - ], - "routes": [ - { - "src": "/api/(.*)", - "dest": "api/[...path].ts" - } - ], - "headers": [ - { - "source": "/(.*)", - "headers": [ - { - "key": "X-Frame-Options", - "value": "DENY" - }, - { - "key": "X-Content-Type-Options", - "value": "nosniff" - }, - { - "key": "Referrer-Policy", - "value": "strict-origin-when-cross-origin" - }, - { - "key": "Permissions-Policy", - "value": "camera=(), microphone=(), geolocation=()" - }, - { - "key": "Strict-Transport-Security", - "value": "max-age=31536000; includeSubDomains; preload" - } - ] - } - ] -} diff --git a/vercel.json b/vercel.json index 14c7c36..72ec604 100644 --- a/vercel.json +++ b/vercel.json @@ -16,6 +16,18 @@ { "src": "/api/(.*)", "dest": "api/[...path].ts" + }, + { + "src": "/v1/(.*)", + "dest": "api/[...path].ts" + }, + { + "src": "/version", + "dest": "api/[...path].ts" + }, + { + "src": "/health", + "dest": "api/[...path].ts" } ], "headers": [ diff --git a/wiki/API-Overview.md b/wiki/API-Overview.md index 18fb696..f8f4629 100644 --- a/wiki/API-Overview.md +++ b/wiki/API-Overview.md @@ -1,33 +1,51 @@ **Navigation** -- [Home](Home) -- [What is TrustSignal](What-is-TrustSignal) -- [Architecture](Evidence-Integrity-Architecture) -- [Verification Receipts](Verification-Receipts) -- [API Overview](API-Overview) -- [Claims Boundary](Claims-Boundary) -- [Quick Verification Example](Quick-Verification-Example) -- [Vanta Integration Example](Vanta-Integration-Example) +- [Home](Home.md) +- [What is TrustSignal](What-is-TrustSignal.md) +- [Architecture](Evidence-Integrity-Architecture.md) +- [Verification Receipts](Verification-Receipts.md) +- [API Overview](API-Overview.md) +- [Claims Boundary](Claims-Boundary.md) +- [Quick Verification Example](Quick-Verification-Example.md) +- [Vanta Integration Example](Vanta-Integration-Example.md) # API Overview -## Problem +Short description: +This page summarizes the TrustSignal public API surface for signed verification receipts, verification signals, verifiable provenance, later verification, and existing workflow integration. + +Audience: +- integration engineers +- evaluators +- developers + +## Problem / Context Partners need a stable public contract that explains how TrustSignal fits into an existing workflow without requiring them to understand internal implementation details. The relevant attack surface includes evidence tampering after collection, artifact substitution attacks, provenance loss in compliance workflows, stale evidence during audit review, and documentation chains that cannot be verified later. -## Verification Lifecycle +## Integrity Model The canonical lifecycle diagram is documented in [docs/verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md). TrustSignal exposes a public verification lifecycle centered on signed verification receipts, verification signals, verifiable provenance metadata, and later verification. +## How It Works + +The public lifecycle is centered on: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification +- existing workflow integration + ## Demo Start with the local developer trial for the fastest lifecycle walkthrough: - [5-minute developer trial](/Users/christopher/Projects/trustsignal/demo/README.md) -## Integration Model +## API And Examples Start here to try the public lifecycle: @@ -63,6 +81,11 @@ Golden path: - `x-signature-timestamp` - `x-issuer-signature` +## Production Considerations + +> [!IMPORTANT] +> Production considerations: the public API overview is an integration reference, not a substitute for deployment-specific authentication, signing configuration, infrastructure review, or operational controls. + ## Integration Fit The integration-facing `/api/v1/*` surface is the main public partner API in this repository. It uses `x-api-key` authentication with scoped access such as `verify`, `read`, `anchor`, and `revoke`. @@ -92,6 +115,11 @@ Local development defaults are intentionally constrained and fail closed where p | `GET` | `/v1/status/:bundleId` | bearer JWT | Check bundle status | | `POST` | `/v1/revoke` | bearer JWT with admin authorization | Revoke a bundle | +## Security And Claims Boundary + +> [!NOTE] +> Claims boundary: this page documents the public API contract and existing workflow integration surface only. It does not expose proof internals, signer infrastructure specifics, internal topology, or unsupported performance/security claims. + ### Error Semantics Integrators should expect these broad patterns: @@ -104,3 +132,10 @@ Integrators should expect these broad patterns: - `503` when a required dependency is unavailable The canonical public contract for the verification lifecycle is [openapi.yaml](/Users/christopher/Projects/trustsignal/openapi.yaml). + +## Related Documentation + +- [docs/partner-eval/try-the-api.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/try-the-api.md) +- [docs/verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md) +- [docs/partner-eval/overview.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [wiki/Claims-Boundary.md](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md) diff --git a/wiki/Claims-Boundary.md b/wiki/Claims-Boundary.md index 9f33d66..47b34ed 100644 --- a/wiki/Claims-Boundary.md +++ b/wiki/Claims-Boundary.md @@ -1,17 +1,25 @@ **Navigation** -- [Home](Home) -- [What is TrustSignal](What-is-TrustSignal) -- [Architecture](Evidence-Integrity-Architecture) -- [Verification Receipts](Verification-Receipts) -- [API Overview](API-Overview) -- [Claims Boundary](Claims-Boundary) -- [Quick Verification Example](Quick-Verification-Example) -- [Vanta Integration Example](Vanta-Integration-Example) +- [Home](Home.md) +- [What is TrustSignal](What-is-TrustSignal.md) +- [Architecture](Evidence-Integrity-Architecture.md) +- [Verification Receipts](Verification-Receipts.md) +- [API Overview](API-Overview.md) +- [Claims Boundary](Claims-Boundary.md) +- [Quick Verification Example](Quick-Verification-Example.md) +- [Vanta Integration Example](Vanta-Integration-Example.md) # Claims Boundary -## Problem +Short description: +This page defines what TrustSignal public materials do and do not claim across signed verification receipts, verification signals, verifiable provenance, later verification, and existing workflow integration. + +Audience: +- evaluators +- partner reviewers +- documentation authors + +## Problem / Context Public integrations need a clear technical boundary so partner engineers and reviewers know what the TrustSignal response means and what it does not mean. @@ -25,10 +33,25 @@ TrustSignal is evidence integrity infrastructure. It acts as an integrity layer - later verification capability - API-accessible receipt lifecycle state +## How It Works + +The public TrustSignal position should be read through the integrity layer: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification +- existing workflow integration + ## Integration Fit TrustSignal is designed to sit behind an upstream workflow that remains the system of record. The partner or workflow owner keeps control of collection, review, and business decisions. +## Security And Claims Boundary + +> [!NOTE] +> Claims boundary: public TrustSignal documents describe technical verification artifacts and the integrity layer. They do not create legal determinations, compliance certifications, or environment-specific infrastructure guarantees. + ## Technical Detail TrustSignal does not provide: @@ -40,3 +63,10 @@ TrustSignal does not provide: - guarantees that depend on environment-specific infrastructure evidence outside this repository The TrustSignal response should be treated as a technical verification artifact that supports audit-ready evidence and later verification. + +## Related Documentation + +- [README.md](/Users/christopher/Projects/trustsignal/README.md) +- [docs/security-summary.md](/Users/christopher/Projects/trustsignal/docs/security-summary.md) +- [docs/partner-eval/overview.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [wiki/What-is-TrustSignal.md](/Users/christopher/Projects/trustsignal/wiki/What-is-TrustSignal.md) diff --git a/wiki/Home.md b/wiki/Home.md index 0ff9b5b..e70e6b8 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -1,43 +1,60 @@ **Navigation** - [Home](Home) -- [What is TrustSignal](What-is-TrustSignal) -- [Architecture](Evidence-Integrity-Architecture) -- [Verification Receipts](Verification-Receipts) -- [API Overview](API-Overview) -- [Claims Boundary](Claims-Boundary) -- [Quick Verification Example](Quick-Verification-Example) -- [Vanta Integration Example](Vanta-Integration-Example) +- [What is TrustSignal](What-is-TrustSignal.md) +- [Architecture](Evidence-Integrity-Architecture.md) +- [Verification Receipts](Verification-Receipts.md) +- [API Overview](API-Overview.md) +- [Claims Boundary](Claims-Boundary.md) +- [Quick Verification Example](Quick-Verification-Example.md) +- [Vanta Integration Example](Vanta-Integration-Example.md) # TrustSignal Wiki +Short description: +This wiki is the lightweight TrustSignal knowledge map for core concepts, API orientation, claims boundary, and quick evaluator references. + +Audience: +- evaluators +- developers +- partner reviewers + TrustSignal is evidence integrity infrastructure for existing workflows. It acts as an integrity layer that provides signed verification receipts, verification signals, verifiable provenance metadata, and later verification capability. -## Problem +## Start Here + +- [What is TrustSignal](What-is-TrustSignal.md) +- [API Overview](API-Overview.md) +- [Quick Verification Example](Quick-Verification-Example.md) +- [Claims Boundary](Claims-Boundary.md) + +## Problem / Context TrustSignal is built for workflows where evidence can be challenged after collection. The relevant attack surface includes evidence tampering after collection, artifact substitution attacks, provenance loss across compliance workflows, stale evidence during audit review, and documentation chains that cannot be verified later. High-loss environments create incentives for these attack paths because downstream reviewers often must rely on artifacts long after the original collection event. -## Verification Lifecycle +## Integrity Model The canonical lifecycle diagram is documented in [docs/verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md). TrustSignal provides signed verification receipts, verification signals, verifiable provenance metadata, and later verification capability as an integrity layer for an existing system of record. -## Start Here +## How It Works -- [What is TrustSignal](What-is-TrustSignal) -- [API Overview](API-Overview) -- [Verification Receipts](Verification-Receipts) -- [Claims Boundary](Claims-Boundary) -- [Quick Verification Example](Quick-Verification-Example) +TrustSignal provides: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification +- existing workflow integration ## Demo - [5-minute developer trial](/Users/christopher/Projects/trustsignal/demo/README.md) -## Integration Model +## API And Examples Use the evaluator docs when you want to see the verification lifecycle before production integration detail: @@ -45,7 +62,7 @@ Use the evaluator docs when you want to see the verification lifecycle before pr - [API playground](/Users/christopher/Projects/trustsignal/docs/partner-eval/api-playground.md) - [OpenAPI contract](/Users/christopher/Projects/trustsignal/openapi.yaml) -## Technical Details +## Verification Lifecycle The public verification lifecycle is: @@ -55,10 +72,22 @@ The public verification lifecycle is: 4. run later verification before downstream reliance 5. use authorized lifecycle actions when receipt state changes +## Production Considerations + +> [!IMPORTANT] +> Production considerations: the wiki mirrors the public evaluation surface. It does not replace deployment-specific authentication, signing configuration, infrastructure controls, or operational review. + ## Production Deployment Requirements Local development defaults are intentionally constrained and fail closed where production trust assumptions are not satisfied. Production deployment requires explicit authentication, signing configuration, and environment setup. -## Current Boundary +## Security And Claims Boundary TrustSignal provides technical verification artifacts. It does not provide legal determinations, compliance certification, fraud adjudication, or a replacement for the upstream system of record. + +## Related Documentation + +- [docs/README.md](/Users/christopher/Projects/trustsignal/docs/README.md) +- [docs/partner-eval/overview.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [docs/verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md) +- [wiki/Claims-Boundary.md](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md) diff --git a/wiki/Quick-Verification-Example.md b/wiki/Quick-Verification-Example.md index 1283e73..468ddc9 100644 --- a/wiki/Quick-Verification-Example.md +++ b/wiki/Quick-Verification-Example.md @@ -1,26 +1,44 @@ **Navigation** -- [Home](Home) -- [What is TrustSignal](What-is-TrustSignal) -- [Architecture](Evidence-Integrity-Architecture) -- [Verification Receipts](Verification-Receipts) -- [API Overview](API-Overview) -- [Claims Boundary](Claims-Boundary) -- [Quick Verification Example](Quick-Verification-Example) -- [Vanta Integration Example](Vanta-Integration-Example) +- [Home](Home.md) +- [What is TrustSignal](What-is-TrustSignal.md) +- [Architecture](Evidence-Integrity-Architecture.md) +- [Verification Receipts](Verification-Receipts.md) +- [API Overview](API-Overview.md) +- [Claims Boundary](Claims-Boundary.md) +- [Quick Verification Example](Quick-Verification-Example.md) +- [Vanta Integration Example](Vanta-Integration-Example.md) # Quick Verification Example -## Problem +Short description: +This page walks through a minimal TrustSignal evaluator flow for verification signals, signed verification receipts, verifiable provenance, and later verification. + +Audience: +- partner evaluators +- integration engineers +- developers + +## Problem / Context This example is for partner engineers who want the smallest realistic TrustSignal flow that shows what goes in, what comes back, and how later verification works. It is intended for workflows where tampered evidence, provenance loss, artifact substitution, and stale evidence matter after collection. -## Verification Lifecycle +## Integrity Model The canonical lifecycle diagram is documented in [docs/verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md). This example uses the current integration-facing lifecycle to create a verification, return verification signals plus a signed verification receipt, store the receipt with the workflow record, and later verify stored receipt state during audit review. +## How It Works + +This example shows: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification +- existing workflow integration through the public lifecycle + ## Demo Start here for the full evaluator path: @@ -30,15 +48,20 @@ Start here for the full evaluator path: - [OpenAPI contract](/Users/christopher/Projects/trustsignal/openapi.yaml) - [Postman collection](/Users/christopher/Projects/trustsignal/postman/TrustSignal.postman_collection.json) -## Integration Model +## API And Examples This example is a deliberate evaluator path. It is designed to show the verification lifecycle before production authentication, signing, and environment requirements are fully configured. +## Production Considerations + +> [!IMPORTANT] +> Production considerations: this is a compact evaluator example. Production deployment still requires explicit authentication, signing configuration, infrastructure controls, and operational review. + ## Production Deployment Requirements Local development defaults are intentionally constrained and fail closed where production trust assumptions are not satisfied. Production deployment requires explicit authentication, signing configuration, and environment setup. -## Technical Details +## Example Or Diagram ```mermaid sequenceDiagram @@ -119,6 +142,23 @@ curl -X POST -H "x-api-key: $TRUSTSIGNAL_API_KEY" \ https://api.trustsignal.example/api/v1/receipt/2c17d2f5-4de6-48c3-b22c-0b7ea9eb5c0a/verify ``` +### Recent Verification Timing + +Recent local benchmark snapshot from [bench/results/latest.md](/Users/christopher/Projects/trustsignal/bench/results/latest.md) at `2026-03-12T22:30:04.260Z`: + +- clean verification request latency: mean `5.24 ms`, median `4.11 ms`, p95 `21.65 ms` +- signed receipt generation latency: mean `0.34 ms`, median `0.32 ms`, p95 `0.63 ms` +- receipt lookup latency: mean `0.57 ms`, median `0.56 ms`, p95 `0.63 ms` +- later verification latency: mean `0.77 ms`, median `0.71 ms`, p95 `1.08 ms` +- tampered artifact detection latency: mean `7.76 ms`, median `5.13 ms`, p95 `42.82 ms` + +This is a recent local evaluator benchmark snapshot, not a production guarantee. The tampered path is most useful as a behavior check for mismatch handling rather than a parser-completeness claim. + +## Security And Claims Boundary + +> [!NOTE] +> Claims boundary: this example documents the public evaluation flow only. It does not expose proof internals, circuit identifiers, model outputs, signing infrastructure specifics, or internal service topology. + ### What This Does Not Expose This public example does not expose: @@ -130,3 +170,10 @@ This public example does not expose: - internal service topology - witness or prover details - registry scoring algorithms + +## Related Documentation + +- [docs/partner-eval/try-the-api.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/try-the-api.md) +- [docs/partner-eval/benchmark-summary.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/benchmark-summary.md) +- [docs/verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md) +- [wiki/Claims-Boundary.md](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md) diff --git a/wiki/What-is-TrustSignal.md b/wiki/What-is-TrustSignal.md index 583fa0e..619e861 100644 --- a/wiki/What-is-TrustSignal.md +++ b/wiki/What-is-TrustSignal.md @@ -1,17 +1,25 @@ **Navigation** -- [Home](Home) -- [What is TrustSignal](What-is-TrustSignal) -- [Architecture](Evidence-Integrity-Architecture) -- [Verification Receipts](Verification-Receipts) -- [API Overview](API-Overview) -- [Claims Boundary](Claims-Boundary) -- [Quick Verification Example](Quick-Verification-Example) -- [Vanta Integration Example](Vanta-Integration-Example) +- [Home](Home.md) +- [What is TrustSignal](What-is-TrustSignal.md) +- [Architecture](Evidence-Integrity-Architecture.md) +- [Verification Receipts](Verification-Receipts.md) +- [API Overview](API-Overview.md) +- [Claims Boundary](Claims-Boundary.md) +- [Quick Verification Example](Quick-Verification-Example.md) +- [Vanta Integration Example](Vanta-Integration-Example.md) # What Is TrustSignal -## Problem +Short description: +This page defines TrustSignal as evidence integrity infrastructure and explains how the integrity layer fits into existing workflow integration. + +Audience: +- evaluators +- partner reviewers +- developers + +## Problem / Context Many workflow systems can show that an artifact was collected or reviewed. Fewer can later verify that the same artifact is still the one tied to the recorded decision. In high-stakes workflows, that creates attack surfaces around evidence tampering after collection, artifact substitution attacks, provenance loss in compliance workflows, stale evidence during audit review, and unverifiable documentation chains. @@ -21,12 +29,32 @@ High-loss environments create incentives for those attack paths because the chal TrustSignal is evidence integrity infrastructure. It provides signed verification receipts, verification signals, verifiable provenance metadata, and later verification for existing workflows. +## How It Works + +TrustSignal provides: + +- signed verification receipts +- verification signals +- verifiable provenance +- later verification +- an integrity layer for existing workflow integration + ## Demo The fastest local evaluator path is the 5-minute developer trial: - [5-minute developer trial](/Users/christopher/Projects/trustsignal/demo/README.md) +## Verification Lifecycle + +At a high level, the public verification lifecycle is: + +1. An upstream system submits a verification request. +2. TrustSignal evaluates the request against configured checks. +3. TrustSignal returns verification signals and a signed verification receipt. +4. Downstream systems store the receipt with the workflow record. +5. Later verification confirms receipt integrity, status, and provenance state when needed. + ## Integration The evaluator and demo path in this repository is a deliberate evaluator path. It is designed to show the verification lifecycle safely before production integration requirements are fully configured. @@ -46,17 +74,19 @@ The upstream platform remains the system of record. TrustSignal adds an integrit Local development defaults are intentionally constrained and fail closed where production trust assumptions are not satisfied. Production deployment requires explicit authentication, signing configuration, and environment setup. -## Technical Details +## API And Examples -At a high level, the public verification lifecycle is: +In the current codebase, the integration-facing `/api/v1/*` routes implement that lifecycle. The legacy `/v1/*` surface remains present for the current SDK. -1. An upstream system submits a verification request. -2. TrustSignal evaluates the request against configured checks. -3. TrustSignal returns verification signals and a signed verification receipt. -4. Downstream systems store the receipt with the workflow record. -5. Later verification confirms receipt integrity, status, and provenance state when needed. +## Production Considerations -In the current codebase, the integration-facing `/api/v1/*` routes implement that lifecycle. The legacy `/v1/*` surface remains present for the current SDK. +> [!IMPORTANT] +> Production considerations: evaluator and demo materials show the TrustSignal integrity layer clearly, but production deployment still requires explicit authentication, signing configuration, infrastructure controls, and operational review. + +## Security And Claims Boundary + +> [!NOTE] +> Claims boundary: this page explains the public product position. It does not expose proof internals, signer infrastructure specifics, internal topology, or unsupported legal/compliance claims. ## What TrustSignal Is Not @@ -67,3 +97,10 @@ TrustSignal is not: - a compliance certification service - a fraud adjudication service - a substitute for environment-specific security evidence + +## Related Documentation + +- [wiki/API-Overview.md](/Users/christopher/Projects/trustsignal/wiki/API-Overview.md) +- [docs/verification-lifecycle.md](/Users/christopher/Projects/trustsignal/docs/verification-lifecycle.md) +- [docs/partner-eval/overview.md](/Users/christopher/Projects/trustsignal/docs/partner-eval/overview.md) +- [wiki/Claims-Boundary.md](/Users/christopher/Projects/trustsignal/wiki/Claims-Boundary.md)