EconomyCredentials
Agent Passports
Reference for Agent Passport schemas, credential projections, OpenID flows, and offline verification.
did:chio identifiers
Every Agent Passport subject is a did:chio identifier:
did:chio:{64-lowercase-hex-Ed25519-public-key}The method-specific identifier is the lowercase hex form of an Ed25519 public key. Because the identifier embeds the key, a verifier can check signatures without a registry lookup, DID resolver, or network call. Parsing rules in chio-did:
- The string must start with
did:chio:. - The suffix must be exactly 64 hexadecimal characters.
- The decoded public key must be Ed25519. P-256 and other algorithms are rejected with
DidError::UnsupportedKeyAlgorithm.
Resolution returns a W3C-compliant DID document with the embedded Ed25519 verification method (in multibase encoding) plus optional service endpoints:
pub const RECEIPT_LOG_SERVICE_TYPE: &str = "ChioReceiptLogService";
pub const PASSPORT_STATUS_SERVICE_TYPE: &str = "ChioPassportStatusService";Service endpoints are attached by the resolving environment, not the identifier itself. Two operators resolving the same did:chio can hand back different service URLs while sharing the same verification method.
Passport schema
The native Agent Passport (schema tag chio.agent-passport.v1) is an unsigned bundle of independently verifiable credentials:
pub struct AgentPassport {
pub schema: String,
pub subject: String,
pub credentials: Vec<ReputationCredential>,
pub merkle_roots: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub enterprise_identity_provenance: Vec<EnterpriseIdentityProvenance>,
pub issued_at: String,
pub valid_until: String,
/// Trust tier synthesized from the operator's compliance score and
/// behavioral-anomaly signal. Optional for wire back-compat: passports
/// without the field omit it entirely.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trust_tier: Option<TrustTier>,
}Two serde attributes sit immediately above the struct at crates/trust/chio-credentials/src/passport.rs:1-2, and they set the rules every sample on this page obeys. rename_all = "camelCase" means the wire keys are merkleRoots, enterpriseIdentityProvenance, issuedAt and validUntil, never the Rust field names. deny_unknown_fields means a key the type does not declare fails deserialization rather than being ignored. issued_at and valid_until are RFC 3339 strings, not epoch integers.
The Agent Passport itself carries no signature. Signature verification applies to the credentials inside it: each ReputationCredential is independently signed by an issuer, and verification walks the bundle credential by credential.
The four field groups serve different verification needs:
| Field | Purpose |
|---|---|
subject | Binds the passport to a single Ed25519 keypair via did:chio |
credentials | Issuer-signed reputation attestations carrying scorecards |
merkle_roots | Receipt-log checkpoint roots so a verifier can spot-check receipt evidence |
enterprise_identity_provenance | Federation evidence: which IdP authenticated the principal that issued each credential |
The V2 variant and the id that carries it
A second passport shape, AgentPassportV2, carries signed, individually verifiable, selectively disclosable financial credentials from the chio.fincred.* families alongside the reputation credentials above. There is no chio.agent-passport.v2 on the wire. The id that reaches the V2 decoder is chio.financial-agent-passport.v1, the value of FINANCIAL_AGENT_PASSPORT_SCHEMA_V1.
decode_versioned_agent_passport reads the schema string off the object and branches on it. The identity id chio.agent-passport.v1 yields VersionedAgentPassport::V1. The financial id yields VersionedAgentPassport::V2, after validate_agent_passport_v2_contract accepts the body. Any other string is refused as unsupported versioned passport schema: {0}, so a producer emitting a literal chio.agent-passport.v2 gets that refusal rather than a V2 passport.
fn decode_versioned_agent_passport_value(
value: serde_json::Value,
) -> Result<VersionedAgentPassport, CredentialError> {
let schema = value
.as_object()
.and_then(|object| object.get("schema"))
.and_then(serde_json::Value::as_str)
.ok_or_else(|| {
CredentialError::InvalidVersionedPassport("passport schema is missing".to_string())
})?;
match schema {
PASSPORT_SCHEMA => serde_json::from_value(value)
.map(VersionedAgentPassport::V1)
.map_err(|error| CredentialError::InvalidVersionedPassport(error.to_string())),
FINANCIAL_AGENT_PASSPORT_SCHEMA_V1 => {
let passport = serde_json::from_value(value)
.map_err(|error| CredentialError::InvalidVersionedPassport(error.to_string()))?;
validate_agent_passport_v2_contract(&passport)
.map_err(|error| CredentialError::InvalidVersionedPassport(error.to_string()))?;
Ok(VersionedAgentPassport::V2(Box::new(passport)))
}
other => Err(CredentialError::UnsupportedVersionedPassportSchema(
other.to_string(),
)),
}
}The version-two name survives in the schema registry. The JSON Schema for the financial passport carries $id: https://chio.world/schemas/chio/agent-passport/v2.schema.json, at spec/schemas/chio-trust/v1/financial-agent-passport.schema.json. That is a schema document identifier, not the value a passport puts in its own schema field. The credential families themselves are documented at Financial Credentials.
Verifiable credential projections
Chio supports three wire formats for a passport. The native format is the source of truth; the other two are projections derived from it for interop with W3C Verifiable Credentials tooling.
| Format | Where it lives | Use case |
|---|---|---|
| Native CHIO JSON | chio-credentials/src/passport.rs | Source of truth. All other formats are derived from this. |
| SD-JWT VC | portable_sd_jwt.rs | IETF SD-JWT-VC. Holders selectively disclose individual claims. |
| JWT VC JSON | portable_jwt_vc.rs | W3C VC 2.0 in JWT-encoded JSON for VP-style flows. |
The SD-JWT VC projection partitions claims into two groups: always-disclosed claims that travel in cleartext, and selectively disclosable claims that the holder reveals on demand.
| Always disclosed | Why |
|---|---|
iss | The credential issuer (the issuing operator) |
sub | Holder thumbprint (binds to the holder's key) |
vct | Verifiable Credential type (the chio-passport SD-JWT VC type) |
cnf | Confirmation key (holder JWK for DPoP-style binding) |
chio_passport_id | Stable passport identifier |
chio_subject_did | did:chio of the agent the passport binds to |
chio_credential_count | Number of credentials inside the bundled passport |
| Selectively disclosable | Why |
|---|---|
chio_issuer_dids | List of issuer did:chios for the credentials in the bundle |
chio_merkle_roots | Receipt-log checkpoint roots referenced by the credentials |
chio_enterprise_identity_provenance | Federation evidence (IdP, principal, tenant, groups, roles) |
Selective disclosure is a presentation concern
OID4VCI and OID4VP protocols
The SD-JWT VC projection supports two OpenID-family protocols: OID4VCI for issuance and a limited OID4VP path for presentation. Both operate over the same projected credential, so a holder issued through OID4VCI can present through OID4VP without a second encoding step.
OID4VCI issuance
An Agent Passport is issuable over OpenID for Verifiable Credential Issuance. The credential configuration id is chio_agent_passport, the credential format is chio-agent-passport+json, and issuer metadata is served at /.well-known/openid-credential-issuer. The pre-authorized-code grant drives the offer, token, and credential exchange, exposed through the chio passport issuance metadata|offer|token|credential CLI verbs.
pub const OID4VCI_PRE_AUTHORIZED_GRANT_TYPE: &str =
"urn:ietf:params:oauth:grant-type:pre-authorized_code";
pub const CHIO_PASSPORT_OID4VCI_CREDENTIAL_CONFIGURATION_ID: &str = "chio_agent_passport";
pub const CHIO_PASSPORT_OID4VCI_FORMAT: &str = "chio-agent-passport+json";
pub const OID4VCI_ISSUER_METADATA_PATH: &str = "/.well-known/openid-credential-issuer";OID4VP presentation
Presentation supports a limited OID4VP verifier flow over the projected application/dc+sd-jwt Agent Passport presentation endpoint. It does not implement generic OID4VP. A request carries a DCQL query for exactly one credential, and the verifier rejects any credential whose format is not application/dc+sd-jwt or whose type is not the chio-passport SD-JWT VC type. Verifier metadata is served at /.well-known/chio-oid4vp-verifier, and the chio passport oid4vp verbs drive the exchange.
impl Oid4vpDcqlQuery {
pub fn validate(&self) -> Result<(), CredentialError> {
if self.credentials.len() != 1 {
return Err(CredentialError::InvalidOid4vpRequest(
"Chio OID4VP currently supports exactly one requested credential".to_string(),
));
}
self.credentials[0].validate()
}
}HTTP flow
Both protocols use a fixed external HTTP flow that a raw client can drive without the Chio CLI: issuance covers the first four calls, presentation the rest. A holder issued a credential in steps one through four presents it back through either the OID4VP lane (steps five through seven) or the Chio-native challenge lane (steps eight and nine).
| Step | Endpoint | Purpose |
|---|---|---|
| 1 | GET /.well-known/openid-credential-issuer | Fetch issuer metadata. |
| 2 | GET /.well-known/jwks.json, GET /.well-known/chio-passport-sd-jwt-vc or .../chio-passport-jwt-vc-json | Optional. Portable issuer key material and projected-profile type metadata. Omitted from issuer metadata and served as 404 when no portable signing key is configured. |
| 3 | POST /v1/passport/issuance/token | Redeem a pre-authorized code for an access token. |
| 4 | POST /v1/passport/issuance/credential | Redeem a native AgentPassport or a projected application/dc+sd-jwt or jwt_vc_json credential. |
| 5 | GET /.well-known/chio-oid4vp-verifier | Fetch verifier metadata. |
| 6 | GET /v1/public/passport/oid4vp/requests/{request_id} | Fetch the signed OID4VP request object, or resolve the same transaction through the HTTPS cross-device launch URL the verifier returns. |
| 7 | POST /v1/public/passport/oid4vp/direct-post | Submit the signed direct_post.jwt holder response. |
| 8 | GET /v1/public/passport/challenges/{challenge_id} | Fetch a stored Chio-native verifier challenge. |
| 9 | POST /v1/public/passport/challenges/verify | Submit the signed Chio-native holder response. |
The OID4VP request object pins client_id_scheme=redirect_uri, response_type=vp_token, and response_mode=direct_post.jwt. Admin operations (issuance offers, challenge creation, verifier-policy CRUD, lifecycle publish and revoke) stay on separate routes so public transport never widens verifier admin authority.
Multi-issuer composition
An Agent Passport can carry credentials from any number of issuers. Two organizations can each independently sign a credential for the same agent, and the agent bundles both into a single Agent Passport without either issuer needing to coordinate with the other.
This works because the Agent Passport is a container: each credential carries its own Ed25519 signature, and verification walks the credentials individually. The PassportVerification result returned by verify_agent_passport exposes both lists:
pub struct PassportVerification {
pub passport_id: String,
pub subject: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub issuer: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub issuers: Vec<String>,
pub issuer_count: usize,
pub credential_count: usize,
pub merkle_root_count: usize,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub enterprise_identity_provenance: Vec<EnterpriseIdentityProvenance>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub passport_lifecycle: Option<PassportLifecycleResolution>,
pub verified_at: u64,
pub valid_until: String,
}issuer is the single-issuer back-compat field; issuers holds the unique issuer did:chio identifiers found in the bundle.
A verifier policy (see PassportVerifierPolicy in chio-credentials/src/passport.rs) constrains each credential independently. It can pin issuers to an issuer_allowlist, set a min_receipt_count or min_composite_score per credential, and require an active Agent Passport lifecycle before accepting the Agent Passport.
Offline verification
The portable kernel core ships an offline-capable Agent Passport verifier at chio-kernel-core/src/passport_verify.rs. It is no_std + alloc, so the same verifier compiles into native sidecars, browser WASM, mobile runtimes, and edge proxies.
The portable verifier consumes a thin PortablePassportEnvelope (schema tag chio.portable-agent-passport.v1) that wraps canonical-JSON bytes of any passport projection:
pub struct PortablePassportBody {
/// Schema identifier; must equal [`PORTABLE_PASSPORT_SCHEMA`].
pub schema: String,
/// Subject identifier (typically the agent DID) the passport binds to.
pub subject: String,
/// Issuer public key that signed this envelope.
pub issuer: PublicKey,
/// Unix timestamp (seconds) the envelope was issued at.
pub issued_at: u64,
/// Unix timestamp (seconds) the envelope expires at.
pub expires_at: u64,
/// Canonical-JSON bytes of the authenticated payload.
#[serde(with = "payload_bytes_hex")]
pub payload_canonical_bytes: Vec<u8>,
}payload_canonical_bytes travels hex-encoded, which is what the payload_bytes_hex serde module does. The signed envelope wraps that body:
/// Signed portable passport envelope.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct PortablePassportEnvelope {
pub body: PortablePassportBody,
pub signature: Signature,
}verify_passport performs four checks:
- The bytes parse as a
PortablePassportEnvelopeand the schema tag matches. - The issuer key is in the relying party's authority key set.
- The Ed25519 signature is valid over the canonical JSON of the body.
- The clock value sits in
[issued_at, expires_at).
On success it returns a VerifiedPassport with the subject, issuer, validity bounds, evaluation time, and the canonical payload bytes. There is no revocation lookup, no issuer-chain validation, and no payload decoding in the portable path. Those richer checks remain in the native chio-credentials / chio-kernel code; the portable core is the verification component that browsers and edge adapters can run with the same Ed25519 path as the sidecar.
Holder binding
A passport binds to runtime key material through the holder's confirmation key. In the SD-JWT VC projection the cnf claim carries a JWK for the holder's Ed25519 key (the same key embedded in chio_subject_did). The holder's thumbprint is the SD-JWT's sub, so the issuer signature attests that this passport may only be presented by the holder of the bound key.
At runtime the same key signs DPoP proofs on the request side, which gives a relying party two bindings that meet. The passport says the scorecard belongs to the holder of one did:chio. The DPoP proof says this request was signed by that same key. Neither claim is worth much alone; together they say the party making the request is the party the issuer attested to.
Worked example: a credential on the wire
The Chio source carries a signed passport in the internet-of-agents example bundle. It holds one credential from one issuer, and it is the shape a second issuer's credential is appended to. Both blocks below are extracted from that file at the line ranges their headers name, and re-verified by digest on every run. What sits between them, lines 17 to 114, is credentialSubject: the subject id and the LocalReputationScorecard under metrics, whose fields the Reputation Scoring page covers.
"schema": "chio.agent-passport.v1",
"subject": "did:chio:bff663535a5cf58658cc38595d20dc4501f6dfc076ddf07e20571d413a3ceb5a",
"credentials": [
{
"@context": [
"https://www.w3.org/2018/credentials/v1",
"https://chio.world/credentials/v1"
],
"type": [
"VerifiableCredential",
"ChioReputationAttestation"
],
"issuer": "did:chio:2f4ea6948f4c70eb9a8cbbfacc88944a238fc3c1410221403e0833ac00f4ea66",
"issuanceDate": "2026-04-24T01:52:59Z",
"expirationDate": "2026-05-24T01:52:59Z",Both subject and issuer are 64 lowercase hex characters after the did:chio: prefix, so the verifier gets both public keys out of the identifiers. The four credential fields @context, type, issuanceDate and expirationDate are required by ReputationCredentialWire and a credential missing any of them fails to deserialize.
"evidence": {
"query": {
"until": 1776995579
},
"receiptCount": 5,
"receiptIds": [
"rcpt-history-job-001",
"rcpt-history-job-002",
"rcpt-history-job-003",
"rcpt-history-job-004",
"rcpt-history-job-005"
],
"checkpointRoots": [],
"receiptLogUrls": [
"https://trust.proofworks.local/receipts"
],
"lineageRecords": 1,
"uncheckpointedReceipts": 5
},
"proof": {
"type": "Ed25519Signature2020",
"created": "2026-04-24T01:52:59Z",
"proofPurpose": "assertionMethod",
"verificationMethod": "did:chio:2f4ea6948f4c70eb9a8cbbfacc88944a238fc3c1410221403e0833ac00f4ea66#key-1",
"proofValue": "b21469ea2aa5bb5212fb89828003498ba658b70717e771d63a7350209d75d311d515fdd4684879c9325b8e28e77d853198221792561bcf61eca93c8841e0d904"
}
}
],
"merkleRoots": [],
"issuedAt": "2026-04-24T01:52:59Z",
"validUntil": "2026-05-24T01:52:59Z"evidence is a ChioCredentialEvidence, which requires query, receiptCount, receiptIds, checkpointRoots, lineageRecords and uncheckpointedReceipts. Here all five receipts are uncheckpointed and checkpointRoots is empty, so a verifier that sets require_checkpoint_coverage rejects this credential. proof is a CredentialProof and all five of its fields are required, including created and verificationMethod. The passport's own trailing keys are merkleRoots, issuedAt and validUntil, camelCase and RFC 3339, and enterpriseIdentityProvenance is absent because it serializes only when non-empty.
What a second issuer adds
A second organization signing its own attestation for the same subject appends one more object to credentials, carrying its own issuer, its own evidence over the receipts it observed, and its own proof whose verificationMethod names that issuer's key. Nothing else in the passport changes and neither issuer coordinates with the other. A verifier then walks the array:
- Confirm each credential's
credentialSubject.idequals the passport'ssubject. - Verify each Ed25519 proof against the public key carried in that credential's own issuer identifier, with no registry lookup.
- Apply
PassportVerifierPolicyper credential. It can pinissuer_allowlist, setmin_receipt_count,min_lineage_records,min_composite_scoreormax_attestation_age_days, and demandrequire_checkpoint_coverage,require_receipt_log_urls,require_enterprise_identity_provenanceorrequire_active_lifecycle.
Verification returns a PassportVerification whose issuers lists both did:chios and whose issuer_count is two, reached without contacting either issuer. Each credential is also reported on its own: a CredentialPolicyEvaluation carries the credential's index, issuer, accepted and the reasons behind a rejection, so a passport with one failing credential is not simply refused whole.
See also
- Agent Passport guide for creating a passport, presenting it, and writing a verifier policy. This page carries the schemas, the projections, the selective-disclosure partition, the composition rules, and the portable offline verifier instead.
- Reputation Scoring for the
LocalReputationScorecardthat goes inside each credential. - Financial Credentials for the
chio.fincred.*families the V2 variant carries. - Federation Overview for bilateral policies and import attenuation across operators.
- Compliance Certificates for the per-session counterpart that anchors receipt-side evidence.