ans-sdk-rust
Health Uyari
- License — License: MIT
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 6 GitHub stars
Code Gecti
- Code scan — Scanned 12 files during light audit, no dangerous patterns found
Permissions Gecti
- Permissions — No dangerous permissions requested
Bu listing icin henuz AI raporu yok.
Agent Name Service SDK written in rust.
ANS Rust Libraries
Rust libraries for the Agent Name Service (ANS) ecosystem.
Crates
| 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 |
Overview
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.
Installation
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"] }
API Client Quick Start
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(())
}
Registration Flow
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(())
}
Authentication Methods
// 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()?;
Verification Quick Start
Server Verification (Client verifying Server)
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(())
}
Client Verification (Server verifying mTLS Client)
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 Verification (Optional)
SCITT (Supply Chain Integrity, Transparency, and Trust) provides offline-capable verification using cryptographically signed artifacts from the transparency log. Enable with features = ["scitt"].
How It Works
| 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 |
SCITT-Enhanced Server Verification
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(())
}
Tier Policies
| 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 |
Verification Tiers
| 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.
Inspect SCITT Artifacts
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-0313e5529694
DPoP / Method B (A2A without mTLS)
Enable 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_dpop
Configuration
Verifier Builder Options
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),
..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?;
Failure Policies
| Policy | Behavior | Use Case |
|---|---|---|
FailClosed |
Reject on any error | High security (default) |
FailOpenWithCache |
Use cached badge if fresh enough | Balance availability/security |
DANE/TLSA Policies
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 |
DNS Resolver Configuration
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 |
Verification Outcomes
| 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 |
Badge Status Values
| 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 |
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ 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) ││
│ └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘
Testing
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/scitt
Logging
The 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 details
TLS Integration (rustls)
The ans-verify crate provides optional rustls integration for verifying certificates during TLS handshakes.
Enable the feature:
[dependencies]
ans-verify = { ..., features = ["rustls"] }
Server Certificate Verification (Client-side)
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();
Client Certificate Verification (Server-side mTLS)
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);
}
}
Examples
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 |
Generate Test Certificates
cargo run -p ans-verify --example gen_test_certs -- --output-dir ./test-certs
Run Local mTLS Demo
This 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"
Connect to Real MCP Server
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 rustls
SCITT Inspector
Fetch 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-0313e5529694
Basic Verification Examples
RUST_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
Yorumlar (0)
Yorum birakmak icin giris yap.
Yorum birakSonuc bulunamadi