Skip to content

dcp-ai — Rust SDK

Rust release (26/10/2026). Rust dcp-ai 2.11.0 is published on crates.io, including verifier fixes, admission and experimental opt-in APIs. dcp-jcs-v1 remains the normative default. See Rust 2.11.0.

Official Rust SDK for the Digital Citizenship Protocol (DCP v2.0). Serde-based types, hybrid post-quantum cryptography (Ed25519 + ML-DSA-65 + SLH-DSA-192f + ML-KEM-768), composite signatures with pq_over_classical binding, dual hash chains, OpenTelemetry observability, DCP-04..09 behavior, and optional WebAssembly support.

Verification checks integrity and signatures; it does not establish principal trust, admission or authorization to act. The V2 verifier checks the primary audit chain and, from 2.11.1, the optional secondary SHA3-256 links, intent hashes and Merkle root that are present (absent ones are not required); 2.11.0 does not check the secondary fields. See the documented verification limit and use the admission APIs below when trust is required.

Installation

Rust SDK. Since 2.11.0 this SDK includes V2 bundle building and signing, admission with explicit principal trust and strict-v1, delegation chains, verifier corrections and experimental opt-in dcp-jcs-v2 APIs. dcp-jcs-v1 remains the normative default. Invalid evidence accepted by older versions may now be rejected. Rust A2A session encryption and its replay window are not implemented; discovery and handshake helpers remain available.

Rust SDK 2.11.1. This version also verifies the secondary SHA3-256 fields of VERIFICATION.md V2-11 when they are present: audit_merkle_root_secondary, each prev_hash_secondary and each intent_hash_secondary. Absent fields are not required, so a verified bundle may lack a complete secondary chain; a present field that is null, not a string or inconsistent is rejected. APIs, result fields, dcp-jcs-v1 and hash_alg handling are unchanged. 2.11.0 does not check these fields. The installation commands below name this version; for its registry availability, see Published versions.

[dependencies]
dcp-ai = "2.11.1"

With OpenTelemetry/OTLP export:

[dependencies]
dcp-ai = { version = "2.11.1", features = ["otlp"] }

For WebAssembly support:

[dependencies]
dcp-ai = { version = "2.11.1", features = ["wasm"] }

Features

Area Status
Ed25519 / ML-DSA-65 / SLH-DSA-192f / ML-KEM-768 providers Yes
Composite signatures (pq_over_classical) + verification Yes
Canonical JSON v2 + domain separation Yes
Dual hash (SHA-256 + SHA3-256) + Merkle roots Yes
Bundle verification (V1 + V2; V2 under a verifier policy and the security tier, verify_signed_bundle_v2_with_policy) Yes
V2 bundle building & signing (dcp_ai::v2::bundle: build_bundle_v2, sign_bundle_v2; the wasm feature's wasm_build_bundle / wasm_sign_bundle call them; no PQ checkpoints yet) Yes
DCP-05 agent lifecycle (commissioning / vitality / decommissioning) Yes
DCP-06 digital succession (testament, memory transfer, ceremony) Yes
DCP-07 dispute resolution + arbitration + jurisprudence Yes
DCP-08 rights + obligations + compliance Yes
DCP-09 delegation + verifiable delegation chains (create_sub_mandate, verify_mandate_chain, §3.5) + awareness threshold + principal mirror Yes
DCP-04 A2A discovery + handshake + session id derivation Yes
DCP-04 A2A AES-256-GCM session encryption Deferred to a follow-up release
Session nonce helpers, security tier engine (compute_security_tier(&intent, Some(&policy_decision)), spec §14.2; compute_security_tier_with_floor adds a delegation chain's tier), emergency revocation Yes
Lazy PQ checkpoints + PQCheckpointManager Yes
Blinded RPR, multi-party authorization, algorithm advisory helpers Yes
Canonical error codes (38 shared across all SDKs) + detect_wire_format Yes
OpenTelemetry / OTLP exporter (optional otlp feature) Yes
Admission: trusted principals and the strict-v1 profile Yes
Canonicalization candidate dcp-jcs-v2 (ingress, router, bytes verifier, producers) Experimental and opt-in; not normative

Quickstart

use dcp_ai::crypto::{generate_keypair, sign_object, verify_object, hash_object};
use serde_json::json;

fn main() {
    // 1. Generate Ed25519 keypair
    let (public_key_b64, secret_key_b64) = generate_keypair();
    println!("Public Key: {}", public_key_b64);

    // 2. Sign an object
    let obj = json!({
        "agent_id": "agent-001",
        "action": "api_call"
    });
    let signature = sign_object(&obj, &secret_key_b64).unwrap();

    // 3. Verify signature
    let valid = verify_object(&obj, &signature, &public_key_b64).unwrap();
    println!("Verified: {}", valid); // true

    // 4. SHA-256 hash
    let hash = hash_object(&obj);
    println!("SHA-256: {}", hash);
}

Verify a Signed Bundle

// Needs citizenship_bundle.signed.json in the working directory and the signer's public key in place of BASE64_PUBLIC_KEY.
use dcp_ai::verify::verify_signed_bundle;
use serde_json;
use std::fs;

fn main() {
    let data = fs::read_to_string("citizenship_bundle.signed.json").unwrap();
    let signed_bundle: serde_json::Value = serde_json::from_str(&data).unwrap();

    let result = verify_signed_bundle(&signed_bundle, Some("BASE64_PUBLIC_KEY"));
    println!("Verified: {}", result.verified);
    if let Some(errors) = &result.errors {
        println!("Errors: {:?}", errors);
    }
}

API Reference

Crypto (dcp_ai::crypto)

Function Signature Description
generate_keypair() () -> (String, String) Returns (public_key_b64, secret_key_b64)
sign_object(obj, secret_key_b64) (&Value, &str) -> Result<String, String> Signs, returns base64
verify_object(obj, sig_b64, pub_b64) (&Value, &str, &str) -> Result<bool, String> Verifies signature
canonicalize(obj) (&Value) -> String Deterministic JSON
hash_object(obj) (&Value) -> String SHA-256 hex
merkle_root_from_hex_leaves(leaves) (&[String]) -> Option<String> Merkle root

Verification (dcp_ai::verify)

// Signature (not runnable)
fn verify_signed_bundle(
    signed_bundle: &Value,
    public_key_b64: Option<&str>,
) -> VerificationResult

Verifies: Ed25519 signature, bundle_hash, merkle_root, intent_hash chain, prev_hash chain.

Types (dcp_ai::types)

// Declarations, fields elided (not runnable)
pub struct ResponsiblePrincipalRecord { ... }
pub struct AgentPassport { ... }
pub struct Intent { ... }
pub struct IntentTarget { ... }
pub struct PolicyDecision { ... }
pub struct AuditEntry { ... }
pub struct AuditEvidence { ... }
pub struct CitizenshipBundle { ... }
pub struct SignedBundle { ... }
pub struct BundleSignature { ... }
pub struct Signer { ... }
pub struct VerificationResult {
    pub verified: bool,
    pub errors: Vec<String>,
}

All structs implement Serialize + Deserialize (serde).

V2 Types (dcp_ai::v2::types)

V2 includes Serde-derived structs for all DCP-05 through DCP-09 artifacts:

Spec Types
DCP-05 LifecycleState, CommissioningCertificate, VitalityReport, VitalityMetrics, DecommissioningRecord, TerminationMode, DataDisposition
DCP-06 DigitalTestament, SuccessionRecord, MemoryTransferManifest, MemoryTransferEntry, SuccessorPreference, MemoryClassification, TransitionType, MemoryDisposition
DCP-07 DisputeRecord, ArbitrationResolution, JurisprudenceBundle, ObjectionRecord, DisputeType, EscalationLevel, DisputeStatus, ObjectionType, AuthorityLevel
DCP-08 RightsDeclaration, RightEntry, ObligationRecord, RightsViolationReport, RightType, ComplianceStatus
DCP-09 DelegationMandate, AdvisoryDeclaration, PrincipalMirror, InteractionRecord, AwarenessThreshold, ThresholdRule, AuthorityScopeEntry

Domain separation constants: CTX_LIFECYCLE, CTX_SUCCESSION, CTX_DISPUTE, CTX_RIGHTS, CTX_DELEGATION, CTX_AWARENESS

All structs derive Debug, Clone, Serialize, Deserialize. Enums use #[serde(rename_all = "snake_case")]. Optional fields use Option<T> with #[serde(skip_serializing_if = "Option::is_none")].

Received audit entries (prepared for 2.12.0, not published). AuditEventV2 and AuditEvidenceV2 are for producing entries. Decoding a received entry into them and encoding it again loses an explicit null (in evidence, prev_hash_secondary and intent_hash_secondary) and any unknown member, and turns an absent pq_checkpoint_ref into null. That changes the entry's canonical bytes and may break the prev_hash chain or the audit Merkle root. To keep a received entry, use RawAuditEventV2: it holds the decoded serde_json::Value, so every member at every depth keeps its presence and null-ness, unknown members included, and it serializes that value as it is. It refuses only what is not a JSON object (RawAuditEventError). It keeps what serde_json decoded, not the original text, never repairs or re-signs, and keeps invalid evidence as it is, so a verifier rejects it for the same reason. typed() is a lossy view for reading fields. To keep a received bundle, keep it as received and verify that.

Bundles (dcp_ai::v2::bundle)

Function Description
build_bundle_v2(&rpr, &passport, &intent, &policy, &audit_entries, session_nonce) Builds the V2 bundle from the four SignedPayloads and the audit trail. It checks each payload_hash and the session_nonce of every artifact and audit entry, and writes the manifest with both audit Merkle roots; an empty trail gets roots of 64 zeros
sign_bundle_v2(bundle, &classical, &pq) Composite-signs the manifest under CTX_BUNDLE and returns {bundle, signature}, with signer.id the RPR's human_id. Put session_expires_at or intended_verifier in the manifest before signing; with session_expires_at and no session_issued_at, it sets session_issued_at to the signing time

Schema validation (V2-2; on main, not in a published version). verify_signed_bundle_v2, …_with_policy and …_at first validate the signed bundle envelope and the four payloads against schemas/v2, after the policy and clock checks and before the verifier checks the manifest, payload hashes, keys and signatures; admit_signed_bundle_v2, which verifies first, and the experimental candidate verifier go through the same gate, while evaluate_admission_v2 takes an earlier result and does not run it. A bundle that fails returns verified: false with schema: <JSON pointer> <keyword>[ <member>] errors (at most 16, then schema: further errors not listed) and no cryptographic details. Unknown members of closed objects are rejected; in open objects, such as the intent target, the declared members keep their constraints and only additional members are not inspected. The manifest's session_issued_at and session_expires_at must be strings when present, not null. date-time follows the ajv-formats 3.0.1 check of the reference verifier, which is not strict RFC 3339. They take a parsed serde_json::Value: what serde_json refuses to parse (a lone surrogate, 1e400) is the caller's parse error. Acceptance change: schema-invalid bundles that verified before are now rejected. There is no schema-only API. See the coverage map.

Delegation Chains (dcp_ai::v2::delegation, DCP-09 §3.5)

Function Description
create_delegation_mandate_with_options(ed, pq, human_classical, human_pq, params, &MandateChainOptions) Root mandate signed by the human, with optional sub_delegation and minimum_security_tier
create_sub_mandate(ed, pq, classical, pq_key, &parent, SubMandateParams { .. }) Sub-mandate signed by the parent's agent under the DCP-AI.v2.SubMandate context
verify_mandate_chain(&chain, &revoked_ids, &MandateChainKeys { human_keys, agent_keys }, now_millis) Verifies the chain root first and offline (signatures back to the human, links and depth, sub_delegation, nested validity, attenuation of actions, data_classes and limits, tier floor, revocation); returns MandateChainResult { valid, errors, depth, effective_tier }

Admission (dcp_ai::v2, spec/profiles/admission)

Whether a verified V2 bundle is admitted as evidence from a principal the operator already trusts, optionally under the strict-v1 profile. The trust set, the profile and the revocation source come from the operator, never from the bundle or the request.

Function Description
verify_signed_bundle_v2_at(&b, &policy, now_ms) verify_signed_bundle_v2_with_policy at now_ms (milliseconds since the Unix epoch), which every time check uses; the other two verify functions use the system clock. It returns V2VerificationDetails: the same V2VerificationResult as result, and signer_keys, which lists, for each composite signature checked, the key resolved for each component and whether it verified. V2VerificationResult retains its ten-field shape and the other two functions retain their signatures
PrincipalTrustStore::from_json(&json) Loads a trust set (schemas/v2/principal_trust_set.schema.json)
resolve_strict_v1_profile(&json) Loads a strict-v1 profile (schemas/v2/admission_profile.schema.json); policy(), verifier_id(), max_session_duration_seconds() and max_revocation_age_seconds() are read-only
revocation_subjects(&b, &verification) The passport agent_id and the sorted kids of the verified signer keys, for the operator's revocation source
evaluate_admission_v2(&b, &verification, &AdmissionOptions { .. }) Admission from a verification result, with no cryptography and no I/O
admit_signed_bundle_v2(&b, &AdmitOptions { .. }) Verifies at now_ms (the system clock when None) under the profile's policy, or policy without a profile, then evaluates

A trust set or profile that cannot be loaded, a profile without a trust set and a profile with a caller policy return AdmissionConfigError { code, message }. AdmissionResult serializes with the keys of § 5.2 (verified, verification, principal_trust, profile, profile_compliance, admitted, action_authorized, recorded_decision, trust_set_version, errors); action_authorized is always false. A profile's verifier_policy may narrow accepted_classical_algs and accepted_pq_algs, which the verifier enforces; it fixes min_classical, min_pq, accepted_hash_algs and max_key_age_days at their defaults.

// Fragment, inside a function returning a Result: profile_json, trust_set_json, signed_bundle, now_ms and revocation_source come from your application.
use dcp_ai::v2::{
    evaluate_admission_v2, resolve_strict_v1_profile, revocation_subjects, verify_signed_bundle_v2_at,
    AdmissionOptions, PrincipalTrustStore,
};

let profile = resolve_strict_v1_profile(&profile_json)?;
let trust = PrincipalTrustStore::from_json(&trust_set_json)?;
let verification = verify_signed_bundle_v2_at(&signed_bundle, profile.policy(), now_ms);
// Ask the operator's revocation source about these; None when it cannot answer.
let snapshot: Option<serde_json::Value> = revocation_source(&revocation_subjects(&signed_bundle, &verification));
let result = evaluate_admission_v2(
    &signed_bundle,
    &verification,
    &AdmissionOptions { profile: Some(&profile), trust: Some(&trust), revocation: snapshot.as_ref(), now_ms },
)?;
if result.admitted {
    // Authentic evidence from a trusted principal; not an authorization to act.
}

Experimental: Canonicalization Candidate dcp-jcs-v2 (opt-in)

Not normative and not active: dcp-jcs-v1 stays the only canonicalization profile, and no default, builder, signer or verifier changes. These modules are available in 2.11.0 only to callers who explicitly opt in. A verified candidate bundle grants no trust or admission. Design and contract: docs/proposals/canonicalization-candidate (§15 ingress, §16 router, §17 verifier, §18 producers).

Module API
dcp_ai::v2::experimental_candidate_ingress experimental_candidate_ingress(&[u8]): CandidateIngressOutcome::Accept { value, canonical, canonical_sha256 } or Reject { class, offset }
dcp_ai::v2::experimental_candidate_router CandidateRouter::new(CandidateRouterConfig { transport, accepted_profiles, historical }); decoder HistoricalAdapter::serde_json_from_slice(), none a default
dcp_ai::v2::experimental_candidate_verify CandidateVerifier::new(config) and verify_at(input, policy, now_ms)
dcp_ai::v2::experimental_candidate_produce sign_payload(context, &CandidateInput, &CandidateKeys) and sign_bundle(&BundleParts, &BundleSigner, &BundleOptions); ProduceError::Reject for the inputs' domain or form (before any signer runs), Usage, Provider
use dcp_ai::providers::{ed25519::Ed25519Provider, ml_dsa_65::MlDsa65Provider};
use dcp_ai::v2::composite_ops::CompositeKeyInfo;
use dcp_ai::v2::crypto_provider::CryptoProvider;
use dcp_ai::v2::experimental_candidate_ingress::{experimental_candidate_ingress, CandidateIngressOutcome};
use dcp_ai::v2::experimental_candidate_produce::{sign_payload, CandidateInput, CandidateKeys};

fn main() {
    // The ingress decides on the original bytes: 1.0 is the integer 1, members are sorted.
    if let Ok(CandidateIngressOutcome::Accept { canonical, .. }) = experimental_candidate_ingress(br#"{"b":1.0,"a":[]}"#) {
        println!("{}", String::from_utf8(canonical).unwrap()); // {"a":[],"b":1}
    }
    if let Ok(CandidateIngressOutcome::Reject { class, offset }) = experimental_candidate_ingress(br#"{"a":1,"a":2}"#) {
        println!("{} {}", class.as_str(), offset); // DUPLICATE_KEY 7
    }

    // A producer signs an artifact from those bytes (hybrid keys, today's composite signature).
    let key = |p: &dyn CryptoProvider| {
        let k = p.generate_keypair().unwrap();
        CompositeKeyInfo { kid: k.kid, alg: p.alg().to_string(), secret_key_b64: k.secret_key_b64, public_key_b64: k.public_key_b64 }
    };
    let keys = CandidateKeys { classical: key(&Ed25519Provider), pq: Some(key(&MlDsa65Provider)) };
    let input = CandidateInput::Bytes(br#"{"intent_id":"intent:1","risk":1.0}"#.to_vec());
    let artifact = sign_payload("DCP-AI.v2.Intent", &input, &keys).unwrap();
    println!("{}", artifact.signed_payload["payload_hash"]); // "sha256:..." of {"intent_id":"intent:1","risk":1}
}

Feature wasm

When the wasm feature is enabled, the crate exposes WebAssembly bindings:

// Declarations of the wasm-bindgen exports, bodies elided (not runnable)
// Available via wasm-bindgen
fn wasm_verify_signed_bundle(json: &str, pub_key: Option<String>) -> String;
fn wasm_verify_signed_bundle_v2(json: &str, policy_json: Option<String>) -> String; // default policy when None; V1 bundles are rejected under a policy
fn wasm_hash_object(json: &str) -> String;
fn wasm_generate_keypair() -> String;
fn wasm_build_bundle(rpr_json: &str, passport_json: &str, intent_json: &str, policy_json: &str, audit_entries_json: &str, session_nonce: &str) -> String;
fn wasm_sign_bundle(bundle_json: &str, classical_sk_b64: &str, classical_kid: &str, pq_sk_b64: &str, pq_kid: &str) -> String;

Compile for WASM:

cargo build --target wasm32-unknown-unknown --features wasm

Development

# Build
cargo build

# Tests
cargo test

# Build with WASM
cargo build --features wasm

# Build for WASM target
cargo build --target wasm32-unknown-unknown --features wasm

Dependencies

  • serde + serde_json — Serialization
  • sha2 — SHA-256
  • sha3 — SHA3-256 (dual hash chains)
  • ed25519-dalek — Ed25519 cryptography
  • fips203 / fips204 / fips205 — ML-KEM-768, ML-DSA-65 and SLH-DSA-192f (FIPS 203 / 204 / 205)
  • rand — Random number generation
  • base64 — Base64 encoding
  • hex — Hex encoding
  • wasm-bindgen (optional) — WASM bindings

License

Apache-2.0