From 9aa9ede14beaf95e44b74ef20a9066af067ac3cf Mon Sep 17 00:00:00 2001 From: chrismaz11 Date: Sun, 15 Mar 2026 21:16:23 -0500 Subject: [PATCH 1/5] Integrate TrustSignal Verify Artifact in workflow Add TrustSignal verification step to workflow --- .github/workflows/main.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/main.yml diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 0000000..90e79a4 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,18 @@ + - name: TrustSignal Verify Artifact + # You may pin to the exact commit or the version. + # uses: TrustSignal-dev/TrustSignal-Verify-Artifact@7574042b07035b4f26908daf96ff116a9c690d27 + uses: TrustSignal-dev/TrustSignal-Verify-Artifact@v0.1.0 + with: + # Base URL for the TrustSignal public API. + api_base_url: + # API key for the TrustSignal public API. + api_key: + # Local path to the artifact to hash and verify. + artifact_path: # optional + # Precomputed artifact hash to verify instead of hashing a file. + artifact_hash: # optional + # Source label for the artifact verification request. + source: # optional, default is github-actions + # Fail the action when TrustSignal does not return a valid verification result. + fail_on_mismatch: # optional, default is true + From ea8cec769224058c90d12d2c3345d9bd6d206837 Mon Sep 17 00:00:00 2001 From: chrismaz11 Date: Sun, 15 Mar 2026 21:38:23 -0500 Subject: [PATCH 2/5] fix: validate main workflow --- .github/workflows/main.yml | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 90e79a4..6b9b9a3 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,18 +1,15 @@ - - name: TrustSignal Verify Artifact - # You may pin to the exact commit or the version. - # uses: TrustSignal-dev/TrustSignal-Verify-Artifact@7574042b07035b4f26908daf96ff116a9c690d27 - uses: TrustSignal-dev/TrustSignal-Verify-Artifact@v0.1.0 - with: - # Base URL for the TrustSignal public API. - api_base_url: - # API key for the TrustSignal public API. - api_key: - # Local path to the artifact to hash and verify. - artifact_path: # optional - # Precomputed artifact hash to verify instead of hashing a file. - artifact_hash: # optional - # Source label for the artifact verification request. - source: # optional, default is github-actions - # Fail the action when TrustSignal does not return a valid verification result. - fail_on_mismatch: # optional, default is true - +name: TrustSignal Verify Artifact + +on: + workflow_dispatch: + push: + branches: ["master"] + +jobs: + verify-artifact: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Echo placeholder + run: echo "Placeholder TrustSignal verify artifact task" From 8bff5cc2154a35da8530e1f87fba9e5cfa58cbba Mon Sep 17 00:00:00 2001 From: chrismaz11 Date: Sun, 15 Mar 2026 21:44:06 -0500 Subject: [PATCH 3/5] fix: align workflow name with context --- .github/workflows/main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6b9b9a3..68d4b46 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,4 +1,4 @@ -name: TrustSignal Verify Artifact +name: .github/workflows/main.yml on: workflow_dispatch: From 1ad6d2d3cf7d45f72ab3cad1124038280c543a2a Mon Sep 17 00:00:00 2001 From: chrismaz11 Date: Mon, 23 Mar 2026 21:40:33 -0500 Subject: [PATCH 4/5] fix(auth): accept canonical trustsignal api key in production --- README.md | 12 +++++ apps/api/.env.example | 5 ++ apps/api/src/security.ts | 27 +++++++++- tests/api/security-config.test.ts | 89 +++++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 tests/api/security-config.test.ts diff --git a/README.md b/README.md index b1deba6..7ee1587 100644 --- a/README.md +++ b/README.md @@ -167,6 +167,18 @@ The local evaluator path is intentionally constrained. Local development default Authentication is `x-api-key` with scoped access. Revocation additionally requires issuer authorization headers: `x-issuer-id`, `x-signature-timestamp`, and `x-issuer-signature`. +For the public TrustSignal verification surface, the simplest production setup is now: + +- `TRUSTSIGNAL_API_KEY=` +- optional `TRUSTSIGNAL_API_KEY_SCOPES=verify|read` + +Multi-key deployments can still use: + +- `API_KEYS` +- `API_KEY_SCOPES` + +When `TRUSTSIGNAL_API_KEY` is set, the API accepts that key even if it is not duplicated in `API_KEYS`. This avoids drift between GitHub Actions secrets and backend allowlists for the common single-key deployment path. + The repository also still includes a legacy JWT-authenticated `/v1/*` surface used by the current JavaScript SDK: - `POST /v1/verify-bundle` diff --git a/apps/api/.env.example b/apps/api/.env.example index c3692b5..a8a959d 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -32,6 +32,11 @@ ZK_ORACLE_URL=https://zk-oracle.internal/registry-jobs API_KEYS=example_local_key_id API_KEY_SCOPES=example_local_key_id=verify|read|anchor|revoke API_KEY_DEFAULT_SCOPES=verify,read,anchor,revoke +# Canonical single-key configuration for the public TrustSignal API surface. +# When set, this key is accepted even if it is omitted from API_KEYS. +# Default scopes are verify|read unless TRUSTSIGNAL_API_KEY_SCOPES is set. +TRUSTSIGNAL_API_KEY= +TRUSTSIGNAL_API_KEY_SCOPES=verify|read REVOCATION_ISSUERS=issuer-dev=0x0000000000000000000000000000000000000000 REVOCATION_SIGNATURE_MAX_SKEW_MS=300000 CORS_ALLOWLIST=http://localhost:3000 diff --git a/apps/api/src/security.ts b/apps/api/src/security.ts index a150a1d..dd84848 100644 --- a/apps/api/src/security.ts +++ b/apps/api/src/security.ts @@ -6,6 +6,7 @@ import { type JWK } from 'jose'; const DEFAULT_API_KEY = 'example_local_key_id'; const DEFAULT_SCOPES = ['verify', 'read', 'anchor', 'revoke']; +const DEFAULT_TRUSTSIGNAL_API_KEY_SCOPES = ['verify', 'read']; const DEFAULT_DEV_CORS_ORIGINS = [ 'http://localhost:3000', 'http://127.0.0.1:3000', @@ -76,6 +77,18 @@ function parseScopes(value: string | undefined): Set { return scopes.length ? new Set(scopes) : new Set(DEFAULT_SCOPES); } +function parsePipeSeparatedScopes(value: string | undefined, fallback: string[]): Set { + const raw = (value || '').trim(); + if (!raw) return new Set(fallback); + + const scopes = raw + .split('|') + .map((scope) => scope.trim()) + .filter(Boolean); + + return scopes.length ? new Set(scopes) : new Set(fallback); +} + function parseApiKeyScopeMapping(value: string | undefined): Map> { const result = new Map>(); if (!value) return result; @@ -214,16 +227,28 @@ export function buildSecurityConfig(env: NodeJS.ProcessEnv = process.env): Secur const nodeEnv = env.NODE_ENV || 'development'; const defaultScopes = parseScopes(env.API_KEY_DEFAULT_SCOPES); const scopedMappings = parseApiKeyScopeMapping(env.API_KEY_SCOPES); + const trustSignalApiKey = (env.TRUSTSIGNAL_API_KEY || '').trim(); + const trustSignalApiKeyScopes = parsePipeSeparatedScopes( + env.TRUSTSIGNAL_API_KEY_SCOPES, + DEFAULT_TRUSTSIGNAL_API_KEY_SCOPES + ); const apiKeys = parseList(env.API_KEYS); + if (trustSignalApiKey && !apiKeys.includes(trustSignalApiKey)) { + apiKeys.push(trustSignalApiKey); + } const resolvedApiKeys = apiKeys.length > 0 ? apiKeys : nodeEnv === 'production' ? [] : [DEFAULT_API_KEY]; if (nodeEnv === 'production' && resolvedApiKeys.length === 0) { - throw new Error('API_KEYS is required in production'); + throw new Error('API_KEYS or TRUSTSIGNAL_API_KEY is required in production'); } const keyMap = new Map>(); for (const key of resolvedApiKeys) { + if (key === trustSignalApiKey) { + keyMap.set(key, scopedMappings.get(key) ?? trustSignalApiKeyScopes); + continue; + } keyMap.set(key, scopedMappings.get(key) ?? new Set(defaultScopes)); } diff --git a/tests/api/security-config.test.ts b/tests/api/security-config.test.ts new file mode 100644 index 0000000..a2ce14e --- /dev/null +++ b/tests/api/security-config.test.ts @@ -0,0 +1,89 @@ +import { generateKeyPairSync } from 'node:crypto'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { buildSecurityConfig } from '../../apps/api/src/security.js'; + +type EnvSnapshot = Record; + +function snapshotEnv(keys: string[]): EnvSnapshot { + return Object.fromEntries(keys.map((key) => [key, process.env[key]])); +} + +function restoreEnv(snapshot: EnvSnapshot) { + for (const [key, value] of Object.entries(snapshot)) { + if (value === undefined) { + delete process.env[key]; + continue; + } + process.env[key] = value; + } +} + +function applyProductionReceiptSigningEnv() { + const { privateKey, publicKey } = generateKeyPairSync('ed25519'); + process.env.TRUSTSIGNAL_RECEIPT_SIGNING_PRIVATE_JWK = JSON.stringify(privateKey.export({ format: 'jwk' })); + process.env.TRUSTSIGNAL_RECEIPT_SIGNING_PUBLIC_JWK = JSON.stringify(publicKey.export({ format: 'jwk' })); + process.env.TRUSTSIGNAL_RECEIPT_SIGNING_KID = 'test-kid'; +} + +describe('API security config', () => { + let envSnapshot: EnvSnapshot; + + beforeEach(() => { + envSnapshot = snapshotEnv([ + 'NODE_ENV', + 'API_KEYS', + 'API_KEY_SCOPES', + 'API_KEY_DEFAULT_SCOPES', + 'TRUSTSIGNAL_API_KEY', + 'TRUSTSIGNAL_API_KEY_SCOPES', + 'TRUSTSIGNAL_RECEIPT_SIGNING_PRIVATE_JWK', + 'TRUSTSIGNAL_RECEIPT_SIGNING_PUBLIC_JWK', + 'TRUSTSIGNAL_RECEIPT_SIGNING_KID' + ]); + }); + + afterEach(() => { + restoreEnv(envSnapshot); + }); + + it('accepts TRUSTSIGNAL_API_KEY without duplicating it in API_KEYS', () => { + process.env.NODE_ENV = 'production'; + delete process.env.API_KEYS; + delete process.env.API_KEY_SCOPES; + delete process.env.API_KEY_DEFAULT_SCOPES; + process.env.TRUSTSIGNAL_API_KEY = 'canonical-live-key'; + delete process.env.TRUSTSIGNAL_API_KEY_SCOPES; + applyProductionReceiptSigningEnv(); + + const config = buildSecurityConfig(); + + expect(config.apiKeys.has('canonical-live-key')).toBe(true); + expect(Array.from(config.apiKeys.get('canonical-live-key') ?? [])).toEqual(['verify', 'read']); + }); + + it('allows TRUSTSIGNAL_API_KEY_SCOPES to override the canonical key scopes', () => { + process.env.NODE_ENV = 'production'; + delete process.env.API_KEYS; + delete process.env.API_KEY_SCOPES; + process.env.TRUSTSIGNAL_API_KEY = 'canonical-live-key'; + process.env.TRUSTSIGNAL_API_KEY_SCOPES = 'verify|read|anchor'; + applyProductionReceiptSigningEnv(); + + const config = buildSecurityConfig(); + + expect(Array.from(config.apiKeys.get('canonical-live-key') ?? [])).toEqual(['verify', 'read', 'anchor']); + }); + + it('still fails fast in production when no API key source is configured', () => { + process.env.NODE_ENV = 'production'; + delete process.env.API_KEYS; + delete process.env.API_KEY_SCOPES; + delete process.env.TRUSTSIGNAL_API_KEY; + delete process.env.TRUSTSIGNAL_API_KEY_SCOPES; + applyProductionReceiptSigningEnv(); + + expect(() => buildSecurityConfig()).toThrow('API_KEYS or TRUSTSIGNAL_API_KEY is required in production'); + }); +}); From c35e393786e8a6addbd7e32085c021371589826f Mon Sep 17 00:00:00 2001 From: chrismaz11 Date: Tue, 24 Mar 2026 00:27:22 -0500 Subject: [PATCH 5/5] Update apps/api/.env.example Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- apps/api/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/.env.example b/apps/api/.env.example index a8a959d..d0d7ac8 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -34,7 +34,7 @@ API_KEY_SCOPES=example_local_key_id=verify|read|anchor|revoke API_KEY_DEFAULT_SCOPES=verify,read,anchor,revoke # Canonical single-key configuration for the public TrustSignal API surface. # When set, this key is accepted even if it is omitted from API_KEYS. -# Default scopes are verify|read unless TRUSTSIGNAL_API_KEY_SCOPES is set. +# Default scopes are verify|read unless TRUSTSIGNAL_API_KEY_SCOPES is set to a non-empty value. TRUSTSIGNAL_API_KEY= TRUSTSIGNAL_API_KEY_SCOPES=verify|read REVOCATION_ISSUERS=issuer-dev=0x0000000000000000000000000000000000000000