Verify a signature
Every signed surface Sill publishes — the per-site agent card, the ARD ai-catalog.json trust manifest — is signed with an ed25519 key whose public half is published at a JWKS endpoint anyone can fetch. A third party can verify any signature end-to-end using only standard tools:
- The JWKS at
https://edge.sill.so/.well-known/jwks.json. - An RFC 8785 (JCS) canonicalizer.
- An off-the-shelf ed25519 verifier (
@noble/ed25519,pynacl,tweetnacl, OpenSSL — any reputable ed25519 library).
No Sill SDK, no Sill code, no Sill account is involved in the verifier path.
The recipe (ARD trust manifest)
Section titled “The recipe (ARD trust manifest)”Each ARD trustManifest.signature is a compact detached JWS per RFC 7515 §3.7, signed with alg: EdDSA (ed25519) per RFC 8037. The string shape is:
BASE64URL(protected) + ".." + BASE64URL(signature)Note the double dot: the middle payload segment is empty, which is what “detached” means — the payload is not transported inside the JWS string; the verifier reconstructs it from the trust manifest itself.
To verify a single trust manifest (the host’s, or any entry’s):
- Fetch the JWKS.
GET https://edge.sill.so/.well-known/jwks.json. It returns anapplication/jwk-set+jsondocument with one or more ed25519 public keys. - Split the signature string on
.into three parts. Confirm the middle part is empty. The first part is the base64url-encoded protected header; the third is the base64url-encoded ed25519 signature. - Decode the protected header (base64url → JSON). Read its
kid; pick the matching JWK from the JWKS wherekidmatches andktyisOKP,crvisEd25519,algisEdDSA. The header’stypwill beard-catalog+jcsfor catalog trust manifests. - Recompute the signing input. Take the trust manifest object, remove its
signaturefield, canonicalize the remainder with RFC 8785 (JCS), and base64url-encode the resulting UTF-8 bytes. The signing input is thenBASE64URL(protected) + "." + BASE64URL(jcs_canonical_stripped_manifest)(a single dot here, since we are reconstructing the input that was signed — not the wire form of the JWS). - Verify the ed25519 signature over the signing input bytes using the public key from step 3.
If the signature verifies, the trust manifest’s identity + provenance was published by the holder of the corresponding Sill ed25519 private key. If it does not verify, treat the manifest as untrusted.
The host trust manifest and each entry trust manifest carry independent signatures. Verify each one separately; there is no single root signature over the catalog body.
Minimal verifier sketch (JavaScript)
Section titled “Minimal verifier sketch (JavaScript)”This is illustrative — any reputable ed25519 library works. Sill publishes no SDK; the verifier path is intentionally standard-only.
import { canonicalize } from 'json-canonicalize'; // any RFC-8785 JCS libimport * as ed from '@noble/ed25519';
function base64urlDecode(s) { const pad = '='.repeat((4 - (s.length % 4)) % 4); return Uint8Array.from( atob(s.replace(/-/g, '+').replace(/_/g, '/') + pad), (c) => c.charCodeAt(0), );}
async function verifyTrustManifest({ trustManifest, jwks }) { // 1. Split the detached compact JWS: "protected..sig" const parts = trustManifest.signature.split('.'); if (parts.length !== 3 || parts[1] !== '') return false; const [headerB64, , sigB64] = parts;
// 2. Decode the protected header, pick the JWK by `kid` const header = JSON.parse(new TextDecoder().decode(base64urlDecode(headerB64))); if (header.alg !== 'EdDSA') return false;
const jwk = jwks.keys.find( (k) => k.kid === header.kid && k.kty === 'OKP' && k.crv === 'Ed25519', ); if (!jwk) return false; const publicKey = base64urlDecode(jwk.x); // raw ed25519 public key (32 bytes)
// 3. Recompute the signing input: JCS over the manifest with `signature` removed const { signature: _omit, ...stripped } = trustManifest; const canonical = new TextEncoder().encode(canonicalize(stripped)); const payloadB64 = btoa(String.fromCharCode(...canonical)) .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, '');
const signingInput = new TextEncoder().encode(`${headerB64}.${payloadB64}`); const signature = base64urlDecode(sigB64); if (signature.length !== 64) return false;
return ed.verify(signature, signingInput, publicKey);}Negative control
Section titled “Negative control”A working verifier MUST also reject tampered input. As a smoke test, change a single byte of the trust manifest (for example flip one character of the identity value) and re-run the recipe. The signature must fail to verify. If it still verifies, the verifier is not actually checking the payload — fix the canonicalization step before relying on it.
What the signature attests
Section titled “What the signature attests”- ARD
ai-catalog.json— eachtrustManifest.signatureattests the identity + provenance of that one trust manifest (host or entry). The per-entryurl,capabilities,description, and other content fields are NOT inside the signed payload; their integrity rests on TLS toedge.sill.soplus thedid:webresolution of the merchant identity, per the upstream ARD specification. - Agent card — the canonical card payload, including
protocol_version,skills[], andcapabilities(as A2A transport flags). The card’s signature is delivered in asigning.envelope_signaturefield rather than as a compact JWS string; the same JWKS key signs it.