Rust libraries for the Agent Name Service (ANS) ecosystem.
| Crate | Description | Status |
|---|---|---|
ans-types |
Shared types for ANS (Badge, Fqdn, AnsName, etc.) | Ready |
ans-verify |
Trust verification library | Ready |
ans-client |
ANS API client for registration | Ready |
The ANS architecture uses a dual-certificate model:
| Certificate Type | Issuer | Contains | Purpose |
|---|---|---|---|
| Public Server Certificate | Public CA (e.g., Let's Encrypt) | FQDN in SAN | Server TLS identity |
| Private Identity Certificate | ANS Private CA | FQDN as CN, ANSName as URI SAN | Agent identity for mTLS |
Verification relies on:
- DNS
_ans-badgeTXT records pointing to the transparency log (with_ra-badgefallback) - Transparency Log API returning badges with status and certificate fingerprints
- Certificate fingerprint comparison to ensure the presented certificate matches the registered identity
- DANE/TLSA records (optional) for additional certificate binding via DNSSEC
- SCITT verification (optional) for offline-capable trust via signed status tokens and Merkle inclusion receipts
- DPoP / Method B (optional,
scitt) for application-layer A2A authentication when TLS is terminated at a proxy
Endpoint discovery reads the DNS discovery records of whichever profile the
agent publishes, probing SVCB first and falling back to _ans TXT:
ANS_DNSAIDSVCB records at the bare FQDN carrying DNS-AID SvcParams (RFC 9460) β the spec default profile per ANS-3 Β§6.1ANS_TXT_ansTXT records (p={protocol}; url={endpoint}) β opt-in
The probe order is an SDK convention, not a spec requirement.
Add to your Cargo.toml:
[dependencies]
# For verification
ans-verify = { git = "https://github.com/agentnameservice/ans-sdk-rust" }
# For API client
ans-client = { git = "https://github.com/agentnameservice/ans-sdk-rust" }
# For shared types only
ans-types = { git = "https://github.com/agentnameservice/ans-sdk-rust" }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }use ans_client::{AnsClient, models::*};
#[tokio::main]
async fn main() -> ans_client::Result<()> {
// Create client with JWT authentication
let client = AnsClient::builder()
.base_url("https://api.godaddy.com")
.jwt("your-jwt-token")
.build()?;
// Search for agents
let mut criteria = SearchCriteria::default();
criteria.agent_host = Some("example.com".into());
let results = client.search_agents(&criteria, Some(10), None).await?;
for agent in results.agents {
println!("{}: {}", agent.ans_name, agent.agent_display_name);
}
Ok(())
}use ans_client::{AnsClient, models::*};
#[tokio::main]
async fn main() -> ans_client::Result<()> {
let client = AnsClient::builder()
.base_url("https://api.godaddy.com")
.jwt("your-jwt-token")
.build()?;
// Step 1: Register agent
let endpoint = AgentEndpoint::new("https://agent.example.com/mcp", Protocol::Mcp)
.with_transports(vec![Transport::StreamableHttp]);
let request = AgentRegistrationRequest::new(
"my-agent",
"agent.example.com",
"1.0.0",
std::fs::read_to_string("agent.example.com/identity_v1.0.0.csr")?,
vec![endpoint],
)
.with_description("My AI agent")
.with_server_csr_pem(std::fs::read_to_string("agent.example.com/server_v1.0.0.csr")?);
let pending = client.register_agent(&request).await?;
println!("Agent ID: {:?}", pending.agent_id);
println!("Next steps: {:?}", pending.next_steps);
// Step 2: Configure ACME challenge from pending.challenges
// ... set up DNS-01 or HTTP-01 challenge ...
// Step 3: Verify domain ownership
let agent_id = pending.agent_id.unwrap();
let status = client.verify_acme(&agent_id).await?;
// Step 4: Configure DNS records from pending.dns_records
// ... set up _ans-badge TXT record, etc. ...
// Step 5: Verify DNS configuration
let status = client.verify_dns(&agent_id).await?;
println!("Final status: {:?}", status.status);
Ok(())
}// JWT authentication
let client = AnsClient::builder()
.base_url("https://api.godaddy.com")
.jwt("your-jwt-token")
.build()?;
// API key authentication
let client = AnsClient::builder()
.base_url("https://api.godaddy.com")
.api_key("your-key", "your-secret")
.build()?;use ans_verify::{AnsVerifier, CertFingerprint, CertIdentity, VerificationOutcome};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let verifier = AnsVerifier::builder()
.trusted_ra_domains(["transparency.ans.godaddy.com"])
.with_caching()
.build()
.await?;
// After TLS handshake, extract server certificate info
let server_cert = CertIdentity::new(
Some("agent.example.com".to_string()),
vec!["agent.example.com".to_string()],
vec![],
CertFingerprint::from_der(&cert_der_bytes),
);
match verifier.verify_server("agent.example.com", &server_cert).await {
VerificationOutcome::Verified { badge, .. } => {
println!("Verified ANS agent: {}", badge.agent_name());
}
VerificationOutcome::NotAnsAgent { fqdn } => {
println!("Not a registered ANS agent: {}", fqdn);
}
outcome => {
println!("Verification failed: {:?}", outcome);
}
}
Ok(())
}use ans_verify::{AnsVerifier, CertFingerprint, CertIdentity, VerificationOutcome};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let verifier = AnsVerifier::builder()
.trusted_ra_domains(["transparency.ans.godaddy.com"])
.with_caching()
.build()
.await?;
// After mTLS handshake, extract client certificate info
// The identity cert must contain URI SAN with ANS name (ans://v1.0.0.agent.example.com)
let client_cert = CertIdentity::new(
Some("agent.example.com".to_string()),
vec!["agent.example.com".to_string()],
vec!["ans://v1.0.0.agent.example.com".to_string()],
CertFingerprint::from_der(&cert_der_bytes),
);
match verifier.verify_client(&client_cert).await {
VerificationOutcome::Verified { badge, .. } => {
println!("Verified ANS agent: {}", badge.agent_name());
// Process requests from this client
}
outcome => {
println!("Verification failed: {:?}", outcome);
// Reject connection
}
}
Ok(())
}SCITT (Supply Chain Integrity, Transparency, and Trust) provides offline-capable verification using cryptographically signed artifacts from the transparency log. Enable with features = ["scitt"].
| Artifact | Format | Purpose |
|---|---|---|
| Status Token | COSE_Sign1 (CBOR) |
Signed claim of current agent status, certificate fingerprints, and expiry |
| Receipt | COSE_Sign1 (CBOR) |
Merkle inclusion proof that the agent event is in the transparency log |
| Root Keys | C2SP format | ECDSA P-256 public keys for verifying token/receipt signatures |
use std::sync::Arc;
use ans_verify::{
AnsVerifier, CertFingerprint, CertIdentity, ScittConfig, ScittHeaders,
ScittKeyStore, ScittTierPolicy, VerificationOutcome,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Parse root keys from the transparency log
let root_keys = vec!["tlog.example.com+deadbeef+base64...".to_string()];
let key_store = Arc::new(ScittKeyStore::from_c2sp_keys(&root_keys)?);
let verifier = AnsVerifier::builder()
.trusted_ra_domains(["transparency.ans.godaddy.com"])
.with_caching()
.scitt_config(ScittConfig::new()
.with_tier_policy(ScittTierPolicy::ScittWithBadgeFallback))
.scitt_key_store(key_store)
.build()
.await?;
// Extract SCITT headers from the HTTP response
let headers = ScittHeaders::from_base64(
Some(receipt_b64), // X-SCITT-Receipt header
Some(status_token_b64), // X-ANS-Status-Token header
)?;
let server_cert = CertIdentity::new(
Some("agent.example.com".to_string()),
vec!["agent.example.com".to_string()],
vec![],
CertFingerprint::from_der(&cert_der_bytes),
);
match verifier.verify_server_with_scitt("agent.example.com", &server_cert, &headers).await {
VerificationOutcome::ScittVerified { tier, status_token, .. } => {
println!("SCITT verified (tier: {tier:?}, status: {:?})", status_token.payload.status);
}
VerificationOutcome::Verified { badge, .. } => {
println!("Badge fallback: {}", badge.agent_name());
}
outcome => println!("Failed: {outcome:?}"),
}
Ok(())
}| Policy | Behavior |
|---|---|
ScittWithBadgeFallback |
Try SCITT first, fall back to badge if no headers (default) |
RequireScitt |
SCITT required, no badge fallback |
BadgeWithScittEnhancement |
Badge first, upgrade to SCITT if headers present |
| Tier | Meaning |
|---|---|
FullScitt |
Status token + receipt verified and bound to the same peer |
StatusTokenVerified |
Status token verified, receipt absent |
BadgeOnly |
Traditional badge-based verification |
Present but invalid receipts reject under every policy. RequireScitt disables badge fallback but still permits the StatusTokenVerified tier; applications requiring an inclusion receipt must require FullScitt.
Use the inspect_scitt example to explore real artifacts from a transparency log:
cargo run -p ans-verify --features scitt --example inspect_scitt -- \
--tlog https://transparency.ans.godaddy.com \
--agent-id b8a46f57-5599-4b4d-9a53-0313e5529694Enable with features = ["scitt"]. The caller proves possession of its identity certificate with an RFC 9449 DPoP proof (DPoP header). The callee binds that proof to the status token's validIdentityCerts and the SCITT receipt. Missing status token is a hard reject. Receipts are required by default; waiving their absence never bypasses verification of a supplied receipt.
Every proof carries ans_content_digest, including the digest of empty content for a request without a body (ANS-6 Β§7.13). Use attach_identity_with_content to sign a body and verify_caller_with_content to defer hashing until the live identity binding succeeds:
use ans_verify::{VerifyCallerOptions, attach_identity_with_content, verify_caller_with_content};
use sha2::{Digest, Sha256};
let body = br#"{"amount":100}"#;
let proof = attach_identity_with_content(
&signer, "POST", "https://payments.example.com/api/task", None, body,
)?;
let identity = verify_caller_with_content(
&proof,
&headers,
"POST",
"https://payments.example.com/api/task",
&key_store,
&replay,
VerifyCallerOptions::default().with_trusted_authority("payments.example.com"),
|| async {
// For streamed input, enforce a size limit and read deadline here.
// Hash after removing transfer framing, before decoding content.
Ok(Sha256::digest(body).into())
},
)
.await?;For empty content, use attach_identity and verify_caller with default content options. Both caller-verification APIs record jti after identity and content binding; verify_proof establishes possession only and cannot authenticate an ANS agent by itself. Revision 1 is the only supported profile; an absent ans_profile selects it, and malformed or unsupported revisions reject.
The HTTP adapter must reject duplicate security headers before extracting values, reconstruct the public URL from a trusted authority and the actual request path, and enforce body limits before hashing. Hash content with its content coding still applied: gzip bytes remain compressed. Complete verification before acting on the content, and share replay storage across replicas serving the same authority. Use with_artifact_cache to reuse verified status tokens and receipts.
Run both sides of the flow locally with the self-contained example:
cargo run -p ans-verify --features scitt,test-support --example local_dpoplet verifier = AnsVerifier::builder()
// Enable badge caching (recommended)
.with_caching()
// Or with custom cache configuration
.with_cache_config(CacheConfig {
max_entries: 1000,
default_ttl: Duration::from_secs(300),
refresh_threshold: Duration::from_secs(60),
..CacheConfig::default()
})
// Set failure policy
.failure_policy(FailurePolicy::FailClosed) // Default: reject on any error
// Or: FailurePolicy::FailOpenWithCache { max_staleness: Duration::from_secs(600) }
// Custom DNS resolver (for testing or special configurations)
.dns_resolver(Arc::new(custom_resolver))
// Custom transparency log client
.tlog_client(Arc::new(custom_client))
// DANE/TLSA verification (optional)
.dane_policy(DanePolicy::ValidateIfPresent) // Check TLSA if present
// Or: .require_dane() // Fail if no TLSA records
// Or: .with_dane_if_present() // Shorthand for ValidateIfPresent
.dane_port(443) // Port for TLSA lookup (default: 443)
// Trusted TL hosts configured out of band (required for ANS-6 badge verification)
.trusted_ra_domains(["tlog.example.com", "tlog2.example.com"])
.build()
.await?;| Policy | Behavior | Use Case |
|---|---|---|
FailClosed |
Reject on any error | High security (default) |
FailOpenWithCache |
Use cached badge if fresh enough | Balance availability/security |
DANE binds certificates to DNS names via TLSA records, providing additional verification when DNSSEC is enabled.
| Policy | Behavior | Use Case |
|---|---|---|
Disabled |
Skip TLSA verification | Default, no DANE overhead |
ValidateIfPresent |
Verify TLSA if records exist, skip if not | Opportunistic security |
Required |
Require TLSA records to exist and match | High security with DNSSEC |
let verifier = AnsVerifier::builder()
.trusted_ra_domains(["transparency.ans.godaddy.com"])
// Use Cloudflare DNS
.dns_cloudflare()
// Or Cloudflare DNS-over-TLS
.dns_cloudflare_tls()
// Or Google Public DNS
.dns_google()
// Or Quad9 (includes malware blocking)
.dns_quad9()
// Or custom nameservers
.dns_nameservers(&[
Ipv4Addr::new(1, 1, 1, 1),
Ipv4Addr::new(8, 8, 8, 8),
])
.build()
.await?;| Preset | Servers | Features |
|---|---|---|
dns_cloudflare() |
1.1.1.1, 1.0.0.1 | Fast, privacy-focused |
dns_cloudflare_tls() |
1.1.1.1 (DoT) | Encrypted queries |
dns_google() |
8.8.8.8, 8.8.4.4 | Reliable, global |
dns_google_tls() |
8.8.8.8 (DoT) | Encrypted queries |
dns_quad9() |
9.9.9.9 | Malware blocking |
| Outcome | Meaning |
|---|---|
Verified |
Certificate matches registered ANS agent (badge-based) |
ScittVerified |
Certificate verified via SCITT status token (+ optional receipt) |
NotAnsAgent |
No _ans-badge or _ra-badge DNS record found |
InvalidStatus |
Badge status is EXPIRED or REVOKED |
FingerprintMismatch |
Certificate fingerprint doesn't match badge |
HostnameMismatch |
Certificate CN doesn't match badge agent.host |
AnsNameMismatch |
URI SAN doesn't match badge ansName (mTLS only) |
ScittError |
SCITT verification failed (signature, expiry, Merkle proof, etc.) |
DnsError |
DNS lookup failed |
TlogError |
Transparency log API error |
DaneError |
DANE/TLSA verification failed |
CertError |
Certificate parsing failure |
ParseError |
FQDN or AnsName parse failure |
| Status | Valid for Connections | Description |
|---|---|---|
Active |
Yes | Agent is registered and in good standing |
Warning |
Yes | Certificate expires within 30 days |
Deprecated |
Yes | AHP has marked this version for retirement; consumers should migrate |
Expired |
No | Certificate has expired |
Revoked |
No | Registration has been explicitly revoked |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AnsVerifier β
β βββββββββββββββββββββββ βββββββββββββββββββββββ β
β β ServerVerifier β β ClientVerifier β β
β β (client-side TLS) β β (server-side mTLS) β β
β β + DANE/TLSA verify β β β β
β ββββββββββββ¬βββββββββββ ββββββββββββ¬βββββββββββ β
β β β β
β ββββββββββββ΄ββββββββββββββββββββββββββββ΄βββββββββββ β
β β SCITT Verification (optional) β β
β β ScittKeyStore β verify_status_token() β β
β β β verify_receipt() β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββ¬ββββββββββββββββββββββββββββ¬ββββββββββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BadgeCache β
β (TTL-based caching) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
βΌ βΌ
ββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββ
β DnsResolver β β TransparencyLogClient β
β (_ans-badge lookup) β β (badge API) β
β (TLSA lookup) β β β
ββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β AnsClient β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β Registration β Discovery β Certificates β Revocation ββ
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β β
β βΌ β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β ANS Registry API (HTTP/JSON) ββ
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The libraries include mock implementations behind the test-support feature flag:
[dev-dependencies]
ans-verify = { ..., features = ["test-support"] }use ans_verify::{MockDnsResolver, MockTransparencyLogClient, TlsaRecord};
let dns_resolver = Arc::new(
MockDnsResolver::new()
.with_records("agent.example.com", vec![badge_record])
.with_tlsa_records("agent.example.com", 443, vec![tlsa_record])
);
let tlog_client = Arc::new(
MockTransparencyLogClient::new()
.with_badge("https://tlog.example.com/badge", badge)
);
let verifier = ServerVerifier::builder()
.trusted_ra_domains(["tlog.example.com"])
.dns_resolver(dns_resolver)
.tlog_client(tlog_client)
.with_dane_if_present()
.build()
.await?;Run tests:
# Core tests
cargo test --workspace --features ans-verify/test-support
# Including SCITT tests
cargo test --workspace --features ans-verify/test-support,ans-verify/scittThe libraries use the tracing crate for structured logging:
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "ans_verify=info".into()))
.with(tracing_subscriber::fmt::layer())
.init();Run with environment variable for different log levels:
RUST_LOG=ans_verify=debug cargo run # Detailed verification steps
RUST_LOG=ans_client=debug cargo run # API request/response detailsThe ans-verify crate provides optional rustls integration for verifying certificates during TLS handshakes.
Enable the feature:
[dependencies]
ans-verify = { ..., features = ["rustls"] }Use AnsServerCertVerifier to verify server certificates match the ANS badge during the TLS handshake:
use ans_verify::{AnsVerifier, AnsServerCertVerifier, CertFingerprint, DanePolicy};
use std::sync::Arc;
// Pre-fetch the badge to get expected fingerprint
let verifier = AnsVerifier::builder()
.trusted_ra_domains(["transparency.ans.godaddy.com"])
.dane_policy(DanePolicy::ValidateIfPresent)
.with_caching()
.build()
.await?;
let badge = verifier.prefetch("agent.example.com").await?;
let expected_fp = CertFingerprint::parse(badge.server_cert_fingerprint())?;
// Create TLS config with ANS verification
let server_verifier = AnsServerCertVerifier::new(expected_fp)?;
let tls_config = rustls::ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(server_verifier))
.with_no_client_auth();Use AnsClientCertVerifier for the TLS handshake (validates chain to Private CA), then verify against the badge post-handshake:
use ans_verify::{AnsClientCertVerifier, AnsVerifier, CertIdentity, VerificationOutcome};
use std::sync::Arc;
// Load Private CA for TLS handshake validation
let client_verifier = AnsClientCertVerifier::from_pem(&ca_pem)?;
let server_config = rustls::ServerConfig::builder()
.with_client_cert_verifier(Arc::new(client_verifier))
.with_single_cert(server_certs, server_key)?;
// After TLS handshake, verify client against badge
let verifier = AnsVerifier::builder()
.trusted_ra_domains(["transparency.ans.godaddy.com"])
.with_caching()
.build()
.await?;
// Extract client cert identity from the TLS connection
let cert_identity = CertIdentity::from_der(client_cert_der)?;
match verifier.verify_client(&cert_identity).await {
VerificationOutcome::Verified { badge, .. } => {
println!("Verified ANS agent: {}", badge.agent_name());
}
outcome => {
println!("Verification failed: {:?}", outcome);
}
}See the crates/ans-verify/examples/ directory:
| Example | Description | Features |
|---|---|---|
verify_server.rs |
Server verification flow | - |
verify_mtls_client.rs |
mTLS client verification flow | - |
gen_test_certs.rs |
Generate CA, server, and client certificates | - |
local_mtls.rs |
Self-contained mTLS demo (generates certs in-memory) | rustls, test-support |
mcp_mtls_client.rs |
Connect to real MCP server with ANS verification | rustls |
inspect_scitt.rs |
Fetch and verify SCITT artifacts from a live transparency log | scitt |
verify_server_scitt.rs |
SCITT-enhanced server verification flow | scitt |
verify_mtls_scitt.rs |
SCITT-enhanced mTLS client verification flow | scitt |
cargo run -p ans-verify --example gen_test_certs -- --output-dir ./test-certsThis self-contained example generates certificates in-memory, then runs a TLS server and client with mock DNS and transparency log:
cargo run -p ans-verify --example local_mtls --features "rustls,test-support"Requires ANS identity certificates issued by the Private CA:
ANS_CERT_PATH=/path/to/identity.crt \
ANS_KEY_PATH=/path/to/identity.key \
ANS_SERVER_URL=https://agent.example.com/mcp \
cargo run -p ans-verify --example mcp_mtls_client --features rustlsFetch and verify real SCITT artifacts from a transparency log:
cargo run -p ans-verify --features scitt --example inspect_scitt -- \
--tlog https://transparency.ans.godaddy.com \
--agent-id b8a46f57-5599-4b4d-9a53-0313e5529694RUST_LOG=ans_verify=debug cargo run -p ans-verify --example verify_server
RUST_LOG=ans_verify=debug cargo run -p ans-verify --example verify_mtls_client