@opena2a/atx-verify
v0.2.0
Published
Spec-compliant offline verifier for ATX (Agent Trust eXtension) credentials — Ed25519 signature verification over the canonical payload (v1.0 pipe / v1.1 JCS RFC 8785), expiry, revocation, issuer-trust, and issuer-chain checks. Byte-for-byte interoperable
Readme
@opena2a/atx-verify
Spec-compliant offline verifier for ATX (Agent Trust eXtension) credentials — the signed, portable credential that states what an agent is.
This is the single shared TypeScript verifier for the OpenA2A ecosystem. It is
byte-for-byte interoperable with the Go (opena2a-registry/pkg/atcverify)
and Python (atx-conformance) reference verifiers; canonicalization agreement
across Go == Python == TS is pinned by atx-conformance/jcs-vectors.
What it does
LocalAtxVerifier.verify(atx) runs the full local check with no network call:
- schema version (
atcVersion1.0or1.1) - expiry
- revocation (the credential's
revokedfield + a cached/federated CRL) - issuer trust (issuer DID against injected trust anchors)
- Ed25519 signature over the canonical payload
Trust anchors (trusted issuers, public keys, CRL, clock) are injected — the
library does no I/O. A consumer wires the live anchors (and, in production, the
post-quantum half) via the AtxVerifier seam.
Signature coverage depends on atcVersion
- v1.0 (
canonicalPayload) signs an 11-field pipe-delimited string covering identity, issuer, trustLevel, trustScore, contentHash, buildAttestation, and the validity window. It does not covercapabilities,scanSummary,issuerChain, orpublisher— a holder can edit those without breaking the signature, so they MUST NOT be trusted for authorization. - v1.1 (
canonicalPayloadV11) signsJCS(TBS)(RFC 8785), which does covercapabilities,scanSummary,issuerChain,publisher, andbehavioralProfile.
The verified context exposes signedCapabilities (true iff v1.1) so callers can
gate capability-based authorization on whether those fields are signed.
Scope
Ed25519 is verified fully via Node's crypto. ML-DSA-65 presence is recorded
(mldsaPresent) but verification is delegated — Node's stdlib has no ML-DSA,
matching the Python reference verifier. Wire the PQC half via the AtxVerifier
seam in production.
Usage
import { LocalAtxVerifier, type AtxTrustAnchors } from "@opena2a/atx-verify";
const anchors: AtxTrustAnchors = {
trustedIssuers: ["did:opena2a:authority:opena2a.org"],
publicKeys: [
{
algorithm: "Ed25519",
publicKeyHex: "<32-byte hex>",
// Recommended: a DID-URL keyId binds the key to its controller so it can
// only verify credentials issued by that DID. Required to be safe with a
// MULTI-issuer anchor set (see "Key-to-issuer binding" below).
keyId: "did:opena2a:authority:opena2a.org#key-1",
},
],
crl: { entries: [] },
};
const result = new LocalAtxVerifier(anchors).verify(atx);
if (result.valid) {
// result.context — backend-free; only authorize on capabilities when
// result.context.signedCapabilities is true (v1.1).
} else {
// result.rejectCategory: UNSUPPORTED_VERSION | EXPIRED | REVOKED
// | UNTRUSTED_ISSUER | SIGNATURE_INVALID | MALFORMED
}Key-to-issuer binding
A signature is only accepted from a key controlled by the credential's issuer.
A configured key whose keyId is a DID-URL (contains #) is bound to its
controller DID and may only verify credentials issued by that DID — or, for
v1.1 (where issuerChain is signed), by an authority named in the chain. This
prevents one trusted issuer's key from satisfying a credential issued under a
different issuer's DID.
A key with no keyId, or a keyId without a # fragment, is treated as
unbound and stays eligible for any issuer — safe for a single-issuer anchor
set, but supply DID-URL keyIds whenever the anchor set holds keys for more
than one issuer.
Conformance
src/conformance.test.ts runs the verifier against the OpenA2A ATX conformance
fixtures (with their pinned signatures), and src/atx.test.ts pins the v1.1
JCS baseline canonical bytes from atx-conformance/jcs-vectors. Any drift from
the cross-language contract fails the package's own CI.
License
Apache-2.0
