Sign Your First Trust Record¶
Generate an Ed25519 signing key and produce a signed TRACE Trust Record that any verifier can check offline.
What you'll learn¶
- How to generate a signing key and export its public JWK
- Which fields a minimal valid TrustRecord requires
- How
sign_record()constructs the signature and embedscnf.jwk - Why RFC 8785 JCS canonical form matters and how the library handles it
- How to verify the signed record with
verify_record()
Prerequisites¶
Generate a Key¶
generate_key() returns an Ed25519PrivateKey from the cryptography library. Keep the private key secret. Distribute only the public half.
from agentrust_trace import generate_key, key_to_jwk
key = generate_key()
jwk = key_to_jwk(key)
print(jwk)
# {'kty': 'OKP', 'crv': 'Ed25519', 'x': '<base64url-encoded public key>'}
key_to_jwk() returns the public JWK dict in OKP format (RFC 8037). This is the value that will appear in cnf.jwk on every record you sign with this key.
For production use, persist the private key and load it via the TRACE_PRIVATE_KEY_PEM environment variable:
from agentrust_trace import load_signing_key
# Reads TRACE_PRIVATE_KEY_PEM if set, otherwise generates an ephemeral key
# (ephemeral keys emit a warning and cannot be re-verified after the process exits)
key = load_signing_key()
Construct a Minimal TrustRecord¶
Every TRACE Trust Record requires these top-level fields. There are no optional shortcuts for a conformant record.
import time
record = {
"eat_profile": "tag:agentrust.io,2026:trace-v0.1",
"iat": int(time.time()),
"subject": "spiffe://trust.example.org/agent/my-agent",
"model": {
"provider": "anthropic",
"model_id": "claude-sonnet-4-6",
"version": "20251001",
},
"runtime": {
"platform": "software-only",
"measurement": "sha256:" + "0" * 64,
},
"policy": {
"bundle_hash": "sha256:b2c3d4e5f6a7b8c9" + "0" * 48,
"enforcement_mode": "enforce",
},
"data_class": "internal",
"build_provenance": {
"slsa_level": 1,
"digest": "sha256:e5f6a7b8c9d0e1f2" + "0" * 48,
},
"appraisal": {
"status": "none",
"verifier": "https://verifier.example.org",
},
"transparency": "https://registry.agentrust.io/claim/placeholder",
}
A few constraints to keep in mind:
subjectmust be a SPIFFE URI (spiffe://...) or a DID URI (did:...).measurementmust be asha256:orsha384:digest string. Forsoftware-onlydevelopment records, all-zero digests are conventional.enforcement_modemust be"enforce","advisory", or"silent". Omitting the field is not valid.appraisal.statusof"none"is correct for software-only Level 0 records. Use"affirming"for hardware-attested records.
Sign the Record¶
Pass the record dict and the private key to sign_record(). It returns a new dict with two additional fields: cnf.jwk (populated from the key) and signature.
from agentrust_trace import sign_record
signed = sign_record(record, key)
print(signed["cnf"]["jwk"]) # {'kty': 'OKP', 'crv': 'Ed25519', 'x': '...'}
print(signed["signature"]) # base64url string, no padding
The signature covers every field in the record except signature itself. cnf.jwk is included in the signed payload, which binds the public key to the record content.
What the Signature Covers¶
The library signs the canonical byte representation of the record with the signature field removed. Canonicalization follows RFC 8785 JSON Canonicalization Scheme (JCS):
- Object keys sorted in Unicode code-point order (ascending)
- No whitespace between tokens
- Numbers serialized in IEEE 754 double-precision shortest form
json.dumps(record, sort_keys=True) produces a different byte sequence than JCS for Unicode keys whose sort order differs between Python and Unicode code-point order. The library uses _canonical_bytes() internally, which calls json.dumps(..., sort_keys=True, separators=(",", ":"), ensure_ascii=True). For plain ASCII keys this matches JCS. Records with non-ASCII keys require a dedicated JCS library; use ASCII-only field names to stay portable.
The spec (section 3.2.2) requires JCS canonical form. Do not reimplement this by hand.
Verify the Signed Record¶
verify_record() extracts cnf.jwk, recomputes the canonical bytes, and checks the Ed25519 signature. It raises cryptography.exceptions.InvalidSignature if the record was tampered with, and returns None on success.
from agentrust_trace import verify_record
from cryptography.exceptions import InvalidSignature
try:
verify_record(signed)
print("signature valid")
except InvalidSignature:
print("tampered — do not trust this record")
To confirm that tampered records are rejected:
import copy
tampered = copy.deepcopy(signed)
tampered["data_class"] = "public" # change a field after signing
try:
verify_record(tampered)
except InvalidSignature:
print("correctly rejected") # this branch runs
Validate the Schema¶
Signature verification and schema validation are separate steps. A record can have a valid signature but still violate the JSON Schema (for example, a malformed digest string). Call validate_json() to check conformance:
from agentrust_trace import validate_json
import jsonschema
try:
validate_json(signed)
print("schema valid")
except jsonschema.ValidationError as e:
print(e.message)
For all violations at once instead of failing on the first:
from agentrust_trace import iter_errors
errors = iter_errors(signed)
for e in errors:
print(e.message)
Summary¶
You generated an Ed25519 key, built a minimal TRACE Trust Record, signed it with sign_record(), and verified it with verify_record(). The signature covers all fields except signature itself, canonicalized via RFC 8785 JCS. The cnf.jwk field embeds the public key so any verifier can check the record offline without a separate key distribution step.
Next steps: