@ixo/udid-verify
v0.1.1
Published
Standalone verification of IXO Evals Engine UDID receipts: Ed25519/EdDSA compact JWS (algorithm-pinned, kid-resolved via the public GET /v1/issuer-keys), canonical-JSON byte binding, payload schema validation, and CID content proofs. No engine deployment
Readme
@ixo/udid-verify
Standalone verification of UDID receipts — the signed determinations
issued by the IXO Evals Engine.
Any third party can check that a presented receipt is authentic, addressed to
them, unexpired, byte-canonical, and schema-valid, using nothing but the
issuing deployment's public GET /v1/issuer-keys route. No engine deployment,
auth token, or secret is required.
This is the same verification code the engine itself runs before it acts on a
UDID — extracted from the engine's internal udid-validator package so
external verifiers do not have to re-implement it.
Why
A UDID is a compact JWS (Ed25519 / EdDSA, algorithm-pinned) over the canonical
JSON encoding of a determination payload (iss, aud, sub, jti, act,
res, optional out). Verifying one correctly means more than checking the
signature:
- kid resolution — the verification key is selected by the JWS header
kidfrom the issuer's published keyring, so receipts keep verifying across signing-key rotations (unknown kids fail closed). - Audience binding — the receipt must be addressed to the
audyou expected, or it proves nothing to you. - Canonical bytes — the signed bytes must be the canonical (deep key-sorted) JSON encoding of the payload.
- Payload schema — the payload must validate against the normative UDID
schemas (bundled), including
out-narrative consistency (outis descriptive only;resis the decision surface). - Expiry —
expis honored with optional clock skew.
Install
npm install @ixo/udid-verifyPublished from this repo's publish-udid-verify workflow via npm Trusted
Publishing (GitHub Actions OIDC) on every udid-verify-v* tag.
Usage
One call, given the issuing engine's base URL:
import { verifyUdidReceipt } from "@ixo/udid-verify";
const { valid, failures, report } = await verifyUdidReceipt(compactJws, {
expectedAud: "did:ixo:my-oracle", // the audience the receipt must be issued for
issuerKeys: "https://evals.example.org", // base URL; keys come from GET /v1/issuer-keys
});
if (!valid) throw new Error(`receipt rejected: ${failures.join(", ")}`);
// report.payload.res is the trusted decision surface (outcome, patch, reason)Or bring the keys yourself (e.g. pinned, cached, or resolved out-of-band):
import { VerifierKeyring, verifyUdidJws } from "@ixo/udid-verify";
const keyring = VerifierKeyring.fromPublishedKeys(publishedKeys); // the `keys` array
const report = await verifyUdidJws(compactJws, keyring, {
expectedAud: "did:ixo:my-oracle",
enforceCanonicalBytes: true,
});Only trust report.payload after the full acceptance rule passes — use
verificationReportFailures(report) (empty array = trustworthy), which mirrors
the engine's own gate before on-chain submission.
Also included
canonicalJsonString/sortKeysDeep— the signing canonicalization.cidV1RawSha256[Utf8]— the engine's content-proof convention (CIDv1, raw codec, sha2-256, base32) for traces, evidence, and rubric bindings.validateUdidPayloadSchema— standalone payload schema validation.checkOutConsistencyand friends —outnarrative binding checks.fetchIssuerKeys/parseIssuerKeysResponse— issuer-key discovery.
Trust model
Receipts are third-party verifiable: the issuer signs with Ed25519 and publishes only public keys. Your trust root is the issuer-keys endpoint you fetch from (TLS to the deployment you already trust to have evaluated the claim) — pin the keys if you need to remove that runtime dependency. Published kids are recomputed from the key bytes and rejected on mismatch.
License
Apache-2.0
