Chio/Docs
LOGIN · JOIN

Agent Passports

Carry independently signed credentials between operators, while each receiver decides which evidence it trusts.

Concept chio@5f63e6e

On this page

An agent can bring its record to an operator it has never worked for. An Agent Passport bundles credentials that different issuers signed about one did:chio subject, and the receiving operator checks each one under its own policy before it issues any authority.

The running example is the signed passport in the internet-of-agents example bundle: one subject, one issuer, one reputation credential backed by five receipts that no checkpoint covers.

Each issuer signs its own credential about the same subject

A passport is a container. Each credential inside it carries its own Ed25519 signature, so a second organization adds an attestation by appending one more credential, and neither issuer coordinates with the other.

Agent PassportChoose a case.
issuing operatorsreceiving operatorsignssignsacceptedacceptedaσ1σ2c1c2Ksubjectdid:chio:…issuerissuercredentialcredentialreceiving kernelissuing operatorsreceiving operatoracceptedacceptedaσ1σ2c1c2Ksubjectdid:chio:…issuerissuercredentialcredentialreceiving kernel

Each issuer signs its own credential about the same subject

A second issuer appends one credential with its own issuer, evidence, and proof. The receiver checks each Ed25519 proof against the key inside that issuer’s did:chio and reports both issuers, with issuer_count 2. Agent Passport guide.

One Agent Passport holding two issuer-signed credentials about one did:chio subject, checked by the receiving operator’s kernel. The second issuer is illustrative; the example bundle carries one. Lifecycle status, the projected formats, and the grant the receiver issues are left out.

The appended credential carries its own issuer, its own evidence over the receipts that issuer observed, and its own proof whose verificationMethod names the issuer’s key. Nothing else in the passport changes. The verifier walks the credentials array, checks that each credentialSubject.id equals the passport’s subject, and verifies each proof against the public key inside that issuer’s identifier, with no registry lookup.

Verification returns a PassportVerification whose issuers lists both identifiers and whose issuer_count is two, reached without contacting either issuer. A credential naming another subject refuses the whole passport with PassportSubjectMismatch.

A valid signature can still fail the receiver’s policy

The example passport authenticates, and a receiver that requires checkpoint coverage still rejects its credential. The receiver reads four things separately:

  1. The issuer. The signature verifies, and the receiver decides whether it trusts that issuer for the claim presented.
  2. The evidence. All five receipts are uncheckpointed and checkpointRoots is empty, so a policy with require_checkpoint_coverage rejects the credential.
  3. The presenter. The holder-binding checks of the chosen projection and presentation protocol tie the request to the subject’s key.
  4. The receiving policy. Validity and lifecycle requirements apply before the receiver decides what authority to issue.

PassportVerifierPolicy constrains each credential on its own. It can pin an issuer_allowlist, set min_receipt_count, min_lineage_records, min_composite_score, or max_attestation_age_days, and demand require_checkpoint_coverage, require_receipt_log_urls, require_enterprise_identity_provenance, or require_active_lifecycle. Each credential’s CredentialPolicyEvaluation carries its index, issuer, accepted, and the reasons behind a rejection, so one failing credential leaves the others readable.

Inspect the example credential on the wire

Both blocks are extracted from the example bundle and re-verified by digest on every run. Between them, lines 17 to 114, sits credentialSubject: the subject id and the LocalReputationScorecard under metrics, whose fields Reputation Scoring covers.

examples/internet-of-agents-web3-network/app/tests/fixtures/good-bundle/identity/passports/proofworks-provider-passport.json2-16json
  "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 reads both public keys out of the identifiers. ReputationCredentialWire requires @context, type, issuanceDate, and expirationDate; a credential missing any of them fails to deserialize.

examples/internet-of-agents-web3-network/app/tests/fixtures/good-bundle/identity/passports/proofworks-provider-passport.json115-145json
      "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. proof is a CredentialProof whose five fields are all required, created and verificationMethod among them. The passport’s own trailing keys are merkleRoots, issuedAt, and validUntil, camelCase and RFC 3339; enterpriseIdentityProvenance serializes only when non-empty, so it is absent here.

The holder’s key binds the passport to whoever presents it

A passport names a subject key, and the request names a signer. Holder binding makes them the same key.

In the SD-JWT VC projection, the cnf claim carries a JWK for the holder’s Ed25519 key, the key embedded in chio_subject_did. The holder’s thumbprint is the SD-JWT’s sub, so the issuer signature attests that only the holder of that key may present the passport.

At runtime the same key signs DPoP proofs on each request. The passport says the scorecard belongs to the holder of one did:chio; the DPoP proof says this request was signed by that key. Together they show that the party making the request is the party the issuer attested to.

Read more in Proof of possession, which follows a DPoP proof through the receiving kernel’s checks.

Four checks verify a portable passport on any runtime

The portable kernel core ships an Agent Passport verifier that is no_std + alloc, so the same Ed25519 path compiles into native sidecars, browser WASM, mobile runtimes, and edge proxies. It reads a thin PortablePassportEnvelope (schema tag chio.portable-agent-passport.v1) that wraps the canonical JSON bytes of any passport projection. verify_passport runs four checks:

  1. Shape. The bytes parse as a PortablePassportEnvelope and the schema tag matches.
  2. Issuer. The issuer key is in the relying party’s authority key set.
  3. Signature. The Ed25519 signature is valid over the canonical JSON of the body.
  4. Time. 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. Revocation lookup, issuer-chain validation, and payload decoding run in the native chio-credentials and chio-kernel verifiers, which a sidecar adds on top of the same Ed25519 check.

One native passport projects into two credential formats

The native Chio JSON passport is the source of truth. Two projections derive from it for W3C Verifiable Credentials tooling: SD-JWT VC, the IETF format in which the holder discloses individual claims, and JWT VC JSON, W3C VC 2.0 encoded as a JWT for presentation flows.

In the SD-JWT VC projection, selectively disclosable claims travel as salted hashes, and the holder reveals individual disclosures at presentation. A verifier can learn which issuers signed the bundle without learning the federation provenance, or the reverse. Issuance runs over OID4VCI and presentation over an OID4VP verifier profile, both on the same projected credential, so a holder issued through one presents through the other with no second encoding step.

Read more in the Agent Passport guide, which creates a passport, presents it, and writes a verifier policy.

Passport fields, projections, and endpoints

Subject identifier and schema

did:chio identifiers

Every Agent Passport subject is a did:chio identifier whose method-specific part is the lowercase hex form of an Ed25519 public key. Because the identifier embeds the key, a verifier checks signatures without a registry lookup, DID resolver, or network call.

did:chio syntaxtext
did:chio:{64-lowercase-hex-Ed25519-public-key}

Parsing rules in chio-did:

  • The string starts with did:chio:.
  • The suffix is exactly 64 hexadecimal characters.
  • The decoded public key is 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:

crates/trust/chio-did/src/lib.rs26-27rust
pub const RECEIPT_LOG_SERVICE_TYPE: &str = "ChioReceiptLogService";
pub const PASSPORT_STATUS_SERVICE_TYPE: &str = "ChioPassportStatusService";

The resolving environment attaches service endpoints. Two operators resolving the same did:chio can return different service URLs while sharing one verification method.

Passport schema

The native Agent Passport (schema tag chio.agent-passport.v1) is an unsigned bundle of independently verifiable credentials. Each ReputationCredential inside it is signed by its issuer, and verification walks the bundle credential by credential.

crates/trust/chio-credentials/src/passport.rs3-17rust
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 above the struct at crates/trust/chio-credentials/src/passport.rs:1-2. rename_all = "camelCase" makes the wire keys merkleRoots, enterpriseIdentityProvenance, issuedAt, and validUntil. deny_unknown_fields fails deserialization on any key the type does not declare. issued_at and valid_until are RFC 3339 strings.

  • subject binds the passport to a single Ed25519 keypair through did:chio.
  • credentials holds the issuer-signed reputation attestations carrying scorecards.
  • merkle_roots holds receipt-log checkpoint roots, so a verifier can spot-check receipt evidence.
  • enterprise_identity_provenance records which identity provider authenticated the principal that issued each credential.

The financial passport and its schema id

AgentPassportV2 carries signed, individually verifiable, selectively disclosable financial credentials from the chio.fincred.* families beside the reputation credentials. Its wire id is chio.financial-agent-passport.v1, the value of FINANCIAL_AGENT_PASSPORT_SCHEMA_V1; the string chio.agent-passport.v2 never appears on the wire.

decode_versioned_agent_passport reads the schema string and branches on it. 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.

crates/trust/chio-credentials/src/financial.rs630-655rust
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 lives 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, separate from the value a passport puts in its own schema field. The credential families are documented at Financial Credentials.

PassportVerification

verify_agent_passport returns a PassportVerification that exposes both the single issuer field and the issuers list of unique issuer did:chio identifiers found in the bundle:

crates/trust/chio-credentials/src/passport.rs401-417rust
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,
}

PassportVerifierPolicy is defined in chio-credentials/src/passport.rs.

Projections and presentation

Verifiable credential projections

FormatSourceUse
Native Chio JSON
chio-credentials/src/passport.rs
Source of truth; the others derive from it
SD-JWT VC
portable_sd_jwt.rs
IETF SD-JWT-VC with selective disclosure
JWT VC JSON
portable_jwt_vc.rs
W3C VC 2.0 as a JWT for VP-style flows

The SD-JWT VC projection partitions claims into those that always travel in cleartext and those the holder reveals on demand.

ClaimDisclosureCarries
iss
Always
The issuing operator
sub
Always
Holder thumbprint, bound to the holder’s key
vct
Always
The chio-passport SD-JWT VC type
cnf
Always
Holder JWK for DPoP-style binding
chio_passport_id
Always
Stable passport identifier
chio_subject_did
Always
The agent’s did:chio
chio_credential_count
Always
Credentials in the bundled passport
chio_issuer_dids
Selective
Issuer identifiers of the bundled credentials
chio_merkle_roots
Selective
Receipt-log checkpoint roots the credentials reference
chio_enterprise_identity_provenance
Selective
IdP, principal, tenant, groups, roles

OID4VCI and OID4VP protocols

The SD-JWT VC projection supports two OpenID-family protocols over the same projected credential: OID4VCI for issuance and an OID4VP verifier profile for presentation.

OID4VCI 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.

crates/trust/chio-credentials/src/oid4vci.rs1-5rust
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 implements an OID4VP verifier flow for the projected application/dc+sd-jwt Agent Passport, scoped to that one credential type. A request carries a DCQL query for exactly one credential, and the verifier rejects any credential whose format is other than application/dc+sd-jwt or whose type differs from 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.

crates/trust/chio-credentials/src/oid4vp.rs16-25rust
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

A raw client can drive both protocols without the Chio CLI. Steps one to four issue a credential; the holder presents it back through the OID4VP lane (steps five to seven) or the Chio-native challenge lane (steps eight and nine).

StepEndpointPurpose
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 GET /.well-known/chio-passport-jwt-vc-json
Optional portable issuer keys and projected-profile type metadata; 404, and absent from issuer metadata, 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 request object, or resolve it through the HTTPS cross-device launch URL.
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) run on separate routes, so the public transport carries no verifier admin authority.

Portable envelope

The portable verifier lives at chio-kernel-core/src/passport_verify.rs. payload_canonical_bytes travels hex-encoded through the payload_bytes_hex serde module:

crates/kernel/chio-kernel-core/src/passport_verify.rs58-72rust
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>,
}

The signed envelope wraps that body:

crates/kernel/chio-kernel-core/src/passport_verify.rs74-80rust
/// 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,
}

Next: score the history a credential carries