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
Add to your Cargo.toml:
[dependencies]
# For verification
ans-verify = { git = "https://github.com/godaddy/ans-sdk-rust" }
# For API client
ans-client = { git = "https://github.com/godaddy/ans-sdk-rust" }
# For shared types only
ans-types = { git = "https://github.com/godaddy/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()
.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()
.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(())
}let 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),
})
// 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 RA domains (optional, defense-in-depth)
.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()
// 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 |
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) |
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 β β β β
β ββββββββββββ¬βββββββββββ ββββββββββββ¬βββββββββββ β
βββββββββββββββΌββββββββββββββββββββββββββββΌββββββββββββββββββββββββ
β β
βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β 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()
.dns_resolver(dns_resolver)
.tlog_client(tlog_client)
.with_dane_if_present()
.build()
.await?;Run tests:
cargo test --workspace --features ans-verify/test-supportThe 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()
.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().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 |
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 rustlsRUST_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