Skip to content

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:

  1. The JWKS at https://edge.sill.so/.well-known/jwks.json.
  2. An RFC 8785 (JCS) canonicalizer.
  3. 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.

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):

  1. Fetch the JWKS. GET https://edge.sill.so/.well-known/jwks.json. It returns an application/jwk-set+json document with one or more ed25519 public keys.
  2. 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.
  3. Decode the protected header (base64url → JSON). Read its kid; pick the matching JWK from the JWKS where kid matches and kty is OKP, crv is Ed25519, alg is EdDSA. The header’s typ will be ard-catalog+jcs for catalog trust manifests.
  4. Recompute the signing input. Take the trust manifest object, remove its signature field, canonicalize the remainder with RFC 8785 (JCS), and base64url-encode the resulting UTF-8 bytes. The signing input is then BASE64URL(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).
  5. 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.

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 lib
import * 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);
}

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.

  • ARD ai-catalog.json — each trustManifest.signature attests the identity + provenance of that one trust manifest (host or entry). The per-entry url, capabilities, description, and other content fields are NOT inside the signed payload; their integrity rests on TLS to edge.sill.so plus the did:web resolution of the merchant identity, per the upstream ARD specification.
  • Agent card — the canonical card payload, including protocol_version, skills[], and capabilities (as A2A transport flags). The card’s signature is delivered in a signing.envelope_signature field rather than as a compact JWS string; the same JWKS key signs it.