Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,32 @@ consumers must preserve unknown fields.
## Packages
| Package | Status | Purpose |
|---|---|---|
| `@verifiable-okf/canon` | M0 | Canonicalization (§5) + JCS (§6) + signing-payload builder. The shared, version-pinned core. |
| `@verifiable-okf/signer` | M0 (S2) | `vokf sign` — attaches `provenance` without mutating any other byte. |
| `@verifiable-okf/verifier` | M0 (S3) | `vokf verify` — implements the §10 verification algorithm. |
| `@verifiable-okf/visualizer` | M1 | Bundle → single offline HTML with per-concept badges. |

## Quickstart
> Filled in once `signer` / `verifier` land (S2/S3). The <5-minute sign → verify walkthrough
> will reproduce `fixtures/signed` from `fixtures/unsigned` and verify it.
| `@verifiable-okf/canon` | ✅ M0 | Canonicalization (§5) + JCS (§6) + signing-payload builder. The shared, version-pinned core. |
| `@verifiable-okf/signer` | ✅ M0 | `vokf-sign` — attaches `provenance` without mutating any other byte. |
| `@verifiable-okf/verifier` | ✅ M0 | `vokf-verify` — the §10 verification algorithm (status + reason codes), schema validation, did:web/did:key. |
| `@verifiable-okf/visualizer` | ⏳ M1 | Bundle → single offline HTML with per-concept badges. |

## Quickstart (< 5 min)

```bash
pnpm install && pnpm build

# A test-only Ed25519 seed (32 bytes hex). NEVER use a real key on the CLI.
SEED=0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20

# 1) Sign an unsigned concept → attaches a `provenance` block (only).
node packages/signer/dist/cli.js fixtures/unsigned/ga4-event.md \
--key "$SEED" --valid-from 2026-01-01T00:00:00Z --valid-until 2027-01-01T00:00:00Z \
-o /tmp/signed.md

# 2) Verify it → machine-readable JSON, exit 0 when verified.
node packages/verifier/dist/cli.js /tmp/signed.md
# {"status":"verified","reasons":[],"issuer":"did:key:z6Mkne..."}
```

The signed output is byte-identical to `fixtures/signed/ga4-event.md`, and tampering with the body,
signature, schema, or issuer yields the corresponding `failed` reason code (see `fixtures/tampered/`).
Verification is fully offline for `did:key`; `did:web` resolves the issuer over HTTPS.

## Development
```bash
Expand Down
30 changes: 30 additions & 0 deletions fixtures/signed/with-attestation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
title: GA4 Purchase Event
resource: https://example.com/okf/ga4/purchase
description: Server-side GA4 purchase event mapping for the storefront.
tags:
- analytics
- ga4
provenance:
spec_version: verifiable-okf/0.3
issuer: did:key:z6MkneMkZqwqRiU5mJzSG3kDwzt9P8C59N4NGTfBLfSGE7c7
content_hash: sha256:94ea40b78f963160edd233a2ad2aa10c5da2d945eb40e4059dd80399d4ccbd00
valid_from: 2026-01-01T00:00:00Z
valid_until: 2027-01-01T00:00:00Z
attestations:
- claim: contains_no_pii
scheme: zk-groth16
circuit: pii-absence
proof: AAAA
signature:
scheme: ed25519
value: ARAqELugqKSMPQtGun5BjNHX3usUJSb+exKgam476xjuqyRnjPLXnCBUiVgMUkz0uJkmSSX2JajYDix78N2IAQ==
---
# GA4 Purchase Event

Send a `purchase` event when an order is confirmed.

| param | source |
|---|---|
| value | order.total |
| currency | order.currency |
25 changes: 25 additions & 0 deletions fixtures/tampered/bad-signature.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: GA4 Purchase Event
resource: https://example.com/okf/ga4/purchase
description: Server-side GA4 purchase event mapping for the storefront.
tags:
- analytics
- ga4
provenance:
spec_version: verifiable-okf/0.3
issuer: did:key:z6MkneMkZqwqRiU5mJzSG3kDwzt9P8C59N4NGTfBLfSGE7c7
content_hash: sha256:94ea40b78f963160edd233a2ad2aa10c5da2d945eb40e4059dd80399d4ccbd00
valid_from: 2026-01-01T00:00:00Z
valid_until: 2027-01-01T00:00:00Z
signature:
scheme: ed25519
value: AefDAKAw+bF+3MJLZJGsy3rcSjDMVJXy4w6ElKOuYyUpq6PjP6OnRl6vVBdNikjO9CjqPBMPsVRDrMr+jyISBQ==
---
# GA4 Purchase Event

Send a `purchase` event when an order is confirmed.

| param | source |
|---|---|
| value | order.total |
| currency | order.currency |
25 changes: 25 additions & 0 deletions fixtures/tampered/body-modified.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: GA4 Purchase Event
resource: https://example.com/okf/ga4/purchase
description: Server-side GA4 purchase event mapping for the storefront.
tags:
- analytics
- ga4
provenance:
spec_version: verifiable-okf/0.3
issuer: did:key:z6MkneMkZqwqRiU5mJzSG3kDwzt9P8C59N4NGTfBLfSGE7c7
content_hash: sha256:94ea40b78f963160edd233a2ad2aa10c5da2d945eb40e4059dd80399d4ccbd00
valid_from: 2026-01-01T00:00:00Z
valid_until: 2027-01-01T00:00:00Z
signature:
scheme: ed25519
value: 6efDAKAw+bF+3MJLZJGsy3rcSjDMVJXy4w6ElKOuYyUpq6PjP6OnRl6vVBdNikjO9CjqPBMPsVRDrMr+jyISBQ==
---
# GA4 Purchase Event

Send a `purchase` event when an order is confirmed.

| param | source |
|---|---|
| value | order.TOTAL |
| currency | order.currency |
25 changes: 25 additions & 0 deletions fixtures/tampered/schema-invalid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: GA4 Purchase Event
resource: https://example.com/okf/ga4/purchase
description: Server-side GA4 purchase event mapping for the storefront.
tags:
- analytics
- ga4
provenance:
spec_version: "0.3"
issuer: did:key:z6MkneMkZqwqRiU5mJzSG3kDwzt9P8C59N4NGTfBLfSGE7c7
content_hash: sha256:94ea40b78f963160edd233a2ad2aa10c5da2d945eb40e4059dd80399d4ccbd00
valid_from: 2026-01-01T00:00:00Z
valid_until: 2027-01-01T00:00:00Z
signature:
scheme: ed25519
value: 6efDAKAw+bF+3MJLZJGsy3rcSjDMVJXy4w6ElKOuYyUpq6PjP6OnRl6vVBdNikjO9CjqPBMPsVRDrMr+jyISBQ==
---
# GA4 Purchase Event

Send a `purchase` event when an order is confirmed.

| param | source |
|---|---|
| value | order.total |
| currency | order.currency |
25 changes: 25 additions & 0 deletions fixtures/tampered/unresolvable-issuer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
title: GA4 Purchase Event
resource: https://example.com/okf/ga4/purchase
description: Server-side GA4 purchase event mapping for the storefront.
tags:
- analytics
- ga4
provenance:
spec_version: verifiable-okf/0.3
issuer: did:example:unresolvable
content_hash: sha256:94ea40b78f963160edd233a2ad2aa10c5da2d945eb40e4059dd80399d4ccbd00
valid_from: 2026-01-01T00:00:00Z
valid_until: 2027-01-01T00:00:00Z
signature:
scheme: ed25519
value: 6efDAKAw+bF+3MJLZJGsy3rcSjDMVJXy4w6ElKOuYyUpq6PjP6OnRl6vVBdNikjO9CjqPBMPsVRDrMr+jyISBQ==
---
# GA4 Purchase Event

Send a `purchase` event when an order is confirmed.

| param | source |
|---|---|
| value | order.total |
| currency | order.currency |
39 changes: 39 additions & 0 deletions packages/verifier/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@verifiable-okf/verifier",
"version": "0.1.0",
"description": "Verifiable OKF verifier: the SPEC §10 algorithm (status + reason codes), JSON Schema validation, and did:web/did:key resolution. CLI: vokf-verify.",
"license": "Apache-2.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"bin": {
"vokf-verify": "dist/cli.js"
},
"files": [
"dist"
],
"scripts": {
"build": "tsc -p tsconfig.json",
"test": "vitest run",
"lint": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@noble/curves": "^1.6.0",
"@scure/base": "^1.1.9",
"@verifiable-okf/canon": "workspace:*",
"ajv": "^8.17.1",
"ajv-formats": "^3.0.1"
},
"devDependencies": {
"@types/node": "^20.14.0",
"@verifiable-okf/signer": "workspace:*",
"typescript": "^5.6.0",
"vitest": "^2.1.0"
}
}
34 changes: 34 additions & 0 deletions packages/verifier/src/cli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env node
/**
* `vokf-verify` — verify a concept and print machine-readable JSON.
*
* Usage:
* vokf-verify <concept.md> [--require <claim>]...
*
* Exit code: 0 = verified, 1 = failed, 2 = unverified / usage error.
*/
import { readFileSync } from "node:fs";
import { verifyConcept } from "./verify.js";

function main(): void {
const path = process.argv[2];
if (!path || path.startsWith("-")) {
console.error("usage: vokf-verify <concept.md> [--require <claim>]...");
process.exit(2);
}
const requiredClaims: string[] = [];
for (let i = 3; i < process.argv.length - 1; i++) {
if (process.argv[i] === "--require") requiredClaims.push(process.argv[i + 1]!);
}
verifyConcept(readFileSync(path), { policy: { requiredClaims } })
.then((result) => {
process.stdout.write(JSON.stringify(result) + "\n");
process.exit(result.status === "verified" ? 0 : result.status === "failed" ? 1 : 2);
})
.catch((err: unknown) => {
console.error(String(err));
process.exit(2);
});
}

main();
77 changes: 77 additions & 0 deletions packages/verifier/src/did.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* DID resolution → Ed25519 public key (SPEC §7).
*
* - `did:key`: fully offline (multibase base58btc + multicodec ed25519-pub).
* - `did:web`: resolved over HTTPS (TLS REQUIRED); `fetch` is injectable for tests.
*
* Returns the 32-byte Ed25519 public key, or `null` if it cannot be resolved.
*/
import { base58 } from "@scure/base";

const ED25519_PUB_MULTICODEC = [0xed, 0x01] as const;

/** Decode an Ed25519 `did:key` to its 32-byte public key. */
export function resolveDidKey(did: string): Uint8Array | null {
const mb = did.slice("did:key:".length);
if (!mb.startsWith("z")) return null;
let bytes: Uint8Array;
try {
bytes = base58.decode(mb.slice(1));
} catch {
return null;
}
if (bytes[0] !== ED25519_PUB_MULTICODEC[0] || bytes[1] !== ED25519_PUB_MULTICODEC[1]) return null;
const pub = bytes.slice(2);
return pub.length === 32 ? pub : null;
}

/** did:web → HTTPS URL of the DID Document. */
export function didWebToUrl(did: string): string | null {
const rest = did.slice("did:web:".length);
if (rest === "") return null;
const parts = rest.split(":").map((p) => decodeURIComponent(p));
const host = parts[0]!;
if (parts.length === 1) return `https://${host}/.well-known/did.json`;
return `https://${host}/${parts.slice(1).join("/")}/did.json`;
}

type FetchLike = (url: string) => Promise<{ ok: boolean; json: () => Promise<unknown> }>;

function pubKeyFromDidDocument(doc: unknown): Uint8Array | null {
if (typeof doc !== "object" || doc === null) return null;
const vms = (doc as { verificationMethod?: unknown }).verificationMethod;
if (!Array.isArray(vms)) return null;
for (const vm of vms) {
const mb = (vm as { publicKeyMultibase?: unknown }).publicKeyMultibase;
if (typeof mb === "string" && mb.startsWith("z")) {
const key = resolveDidKey("did:key:" + mb);
if (key) return key;
}
}
return null;
}

/** Resolve a did:web over HTTPS. `fetchImpl` defaults to global fetch. */
export async function resolveDidWeb(
did: string,
fetchImpl: FetchLike = fetch as unknown as FetchLike,
): Promise<Uint8Array | null> {
const url = didWebToUrl(did);
if (!url || !url.startsWith("https://")) return null; // TLS required
try {
const res = await fetchImpl(url);
if (!res.ok) return null;
return pubKeyFromDidDocument(await res.json());
} catch {
return null;
}
}

export type DidResolver = (did: string) => Promise<Uint8Array | null>;

/** Default resolver: did:key offline, did:web over HTTPS, others unresolved. */
export const defaultResolveDid: DidResolver = async (did) => {
if (did.startsWith("did:key:")) return resolveDidKey(did);
if (did.startsWith("did:web:")) return resolveDidWeb(did);
return null;
};
20 changes: 20 additions & 0 deletions packages/verifier/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* @verifiable-okf/verifier — the SPEC §10 verification algorithm
* (status + reason codes), JSON Schema validation (§11), and DID resolution (§7).
*/
export {
verifyConcept,
type VerifyResult,
type VerifyOptions,
type TrustStatus,
type ReasonCode,
type AttestationVerifier,
} from "./verify.js";
export { isSchemaValid, PROVENANCE_SCHEMA } from "./schema.js";
export {
resolveDidKey,
resolveDidWeb,
didWebToUrl,
defaultResolveDid,
type DidResolver,
} from "./did.js";
Loading
Loading