Trust Passport · v1
How Unfragile signs trust passports
Every passport returned by /api/v1/passport/[slug] is cryptographically signed with Ed25519. Anyone can verify the signature offline using only the published public key — no roundtrip to Unfragile required.
Why it matters
Agents and enterprises that route capability invocations through Unfragile need a non-repudiable record of what was attested, when, and by whom. Informational trust data is easy to fake; cryptographic attestation is not.
The signed envelope makes Unfragile the certificate authority of the agent economy. Compliance, procurement, and insurance workflows can require passport verification the same way they require SSL — and verify it themselves without trusting us.
The signature envelope
Four fields are appended to every signed passport:
| Field | Format | Meaning |
|---|---|---|
signature | base64 | Ed25519 signature over the canonical-JSON-encoded bare payload |
signedAt | ISO 8601 | When this snapshot was signed |
signedBy | string | Issuer identity. Always unfragile.ai |
version | integer | Signing-envelope version. Currently 1 |
Canonicalization
Before signing, the unsigned payload is serialised with sorted keys (deterministic JSON via safe-stable-stringify), then UTF-8 encoded to bytes. Verifiers must reproduce the exact same byte sequence — strip the envelope fields, sort all keys recursively, and use the standard JSON serialiser. Whitespace and key order matter.
Public key
The 32-byte raw Ed25519 public key is published at /api/v1/trust-passport-public-key. The endpoint is CORS-permissive and cached aggressively (24h public, 7d stale-while-revalidate). Pin it locally — it's stable across passport versions until a planned key rotation, which will be announced here.
Verify a passport — JavaScript
The full offline verifier, ~25 lines:
// Verify an Unfragile trust passport, offline.
// Requires only the published public key + the passport JSON.
//
// npm install @noble/ed25519 safe-stable-stringify
import * as ed from "@noble/ed25519";
import { sha512 } from "@noble/hashes/sha512";
import stringify from "safe-stable-stringify";
ed.hashes.sha512 = sha512;
const PUBLIC_KEY_URL = "https://unfragile.ai/api/v1/trust-passport-public-key";
async function verifyUnfragilePassport(passport) {
// 1. Fetch the public key (cache locally — it's stable)
const { publicKey } = await fetch(PUBLIC_KEY_URL).then((r) => r.json());
// 2. Detach envelope fields and canonicalize the remaining payload
const { signature, signedAt, signedBy, version, ...payload } = passport;
const canonical = new TextEncoder().encode(stringify(payload));
// 3. base64-decode both signature and key
const sigBytes = Uint8Array.from(Buffer.from(signature, "base64"));
const keyBytes = Uint8Array.from(Buffer.from(publicKey, "base64"));
return ed.verifyAsync(sigBytes, canonical, keyBytes);
}
// Example — the endpoint returns { passport, _links }.
// Verify the inner signed passport object, not the envelope.
const { passport } = await fetch("https://unfragile.ai/api/v1/passport/cursor")
.then((r) => r.json());
const ok = await verifyUnfragilePassport(passport);
console.log(ok ? "✓ verified" : "✗ tampered or unsigned");Conformance test vector
Any third party can confirm their Ed25519 implementation interoperates with ours. There are two ways to do it — a runnable live check (authoritative, always current, verifies a real signature) and a frozen static vector (a fixed payload for offline regression tests).
1. Runnable check (authoritative)
Fetch a live signed passport and verify response.passport against the published public key. Expected result: true.
// Conformance check — confirm your Ed25519 verifier interops with Unfragile.
// Authoritative because it verifies a REAL, live signature.
//
// npm install @noble/ed25519 safe-stable-stringify
import * as ed from "@noble/ed25519";
import { sha512 } from "@noble/hashes/sha512";
import stringify from "safe-stable-stringify";
ed.hashes.sha512 = sha512;
// 1. Pin the published public key (base64, 32-byte raw Ed25519).
const { publicKey } = await fetch(
"https://unfragile.ai/api/v1/trust-passport-public-key"
).then((r) => r.json());
// 2. Fetch a live signed passport. The endpoint returns { passport, _links } —
// verify the INNER passport object, never the envelope.
const { passport } = await fetch(
"https://unfragile.ai/api/v1/passport/cursor"
).then((r) => r.json());
// 3. Reproduce the canonical bytes: strip the 4 envelope fields,
// sort keys recursively, UTF-8 encode.
const { signature, signedAt, signedBy, version, ...payload } = passport;
const canonical = new TextEncoder().encode(stringify(payload));
// 4. Verify. Expected result: true.
const ok = await ed.verifyAsync(
Uint8Array.from(Buffer.from(signature, "base64")),
canonical,
Uint8Array.from(Buffer.from(publicKey, "base64"))
);
console.assert(ok === true, "CONFORMANCE FAILED — your verifier does not interop");
console.log(ok ? "✓ conformant" : "✗ NOT conformant");2. Frozen static vector
TEMPLATE — signature not yet filled
The payload below is frozen: its canonical bytes (envelope fields stripped, keys sorted recursively, UTF-8 encoded) never change. A maintainer signs these exact bytes with the production private key and pastes the base64 signature into the slot. We do not ship a placeholder signature — inventing one would be a fabrication, and a fabricated signature would fail the very verification it claims to demonstrate. Until the slot is filled, use the runnable check above.
Frozen payload (the bytes that get signed):
{
"unfragile": {
"@version": "1.0",
"version": "2026-05",
"artifact": {
"id": "conformance-vector",
"slug": "conformance-vector",
"name": "Conformance Vector",
"type": "mcp",
"url": "https://example.com/conformance-vector",
"page_url": "https://unfragile.ai/conformance-vector",
"categories": ["tool-use-integration"],
"tags": ["conformance", "test-vector"],
"status": "active",
"verified": false
},
"capabilities": [],
"trust": {
"score": 1,
"verified": false,
"data_access_risk": "low",
"permissions": [],
"failure_modes": ["fixed conformance vector — not a real artifact"]
}
}
}| Slot | Value |
|---|---|
algorithm | ed25519 |
publicKey (base64) | pin the live value from /api/v1/trust-passport-public-key (the key in effect when the vector was signed) |
signature (base64) | null — TODO(maintainer): sign canonical(payload) with the production private key and paste the base64 result here |
expectedVerification | true |
This vector is also published machine-readably under /schema.json at conformance.frozenVector, alongside the frozen canonicalization recipe at conformance.canonicalization.
License
The trust passport format and the broader capability protocol schemas are licensed Creative Commons Attribution 4.0 International (SPDX: CC-BY-4.0). Verify, fork, and build on this trust layer freely with attribution to Unfragile. Schema + license: /schema.json. Full license text: creativecommons.org/licenses/by/4.0.
FAQ
Why Ed25519 instead of RSA or ECDSA?
Ed25519 signatures are deterministic (no nonce reuse risk), fast to verify on commodity hardware, small (64 bytes), and supported by every modern language runtime. It's the right primitive for high-volume agent-side verification.
How fresh is a signature?
The passport endpoint re-signs the current artifact state on every read, so signatures reflect data as fresh as the underlying graph. The signedAt timestamp gives you the exact freshness window for the values you received.
What if Unfragile's private key is compromised?
We'll rotate the keypair, publish the new public key at the same endpoint, and announce the rotation here. Old signatures created before rotation will fail verification — that's the intended behaviour. Pin signature timestamps in any compliance workflow that needs to know the exact key that signed.
Can I cache the public key?
Yes — the endpoint sets Cache-Control: public, s-maxage=86400, stale-while-revalidate=604800. Caching locally is encouraged for high-volume verifiers.
Try it
Fetch a passport and verify it — start with a verified artifact:
curl https://unfragile.ai/api/v1/passport/cursor | jq .