https://github.com/godaddy/ans-sdk-rust
Agent Name Service SDK written in rust.
https://github.com/godaddy/ans-sdk-rust
Last synced: 2 months ago
JSON representation
Agent Name Service SDK written in rust.
- Host: GitHub
- URL: https://github.com/godaddy/ans-sdk-rust
- Owner: godaddy
- License: mit
- Created: 2026-03-02T21:38:05.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-03-25T20:30:57.000Z (3 months ago)
- Last Synced: 2026-03-26T18:39:12.599Z (3 months ago)
- Language: Rust
- Size: 507 KB
- Stars: 4
- Watchers: 0
- Forks: 0
- Open Issues: 6
-
Metadata Files:
- Readme: README.md
- Changelog: CHANGELOG.md
- Contributing: CONTRIBUTING.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
- Security: SECURITY.md
Awesome Lists containing this project
README
# ANS Rust Libraries
Rust libraries for the Agent Name Service (ANS) ecosystem.
## Crates
| Crate | Description | Status |
|-------|-------------|--------|
| [`ans-types`](crates/ans-types) | Shared types for ANS (Badge, Fqdn, AnsName, etc.) | Ready |
| [`ans-verify`](crates/ans-verify) | Trust verification library | Ready |
| [`ans-client`](crates/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-badge` TXT records** pointing to the transparency log (with `_ra-badge` fallback)
- **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
## Installation
Add to your `Cargo.toml`:
```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"] }
```
## API Client Quick Start
```rust
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
```rust
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
```rust
// 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)
```rust
use ans_verify::{AnsVerifier, CertFingerprint, CertIdentity, VerificationOutcome};
#[tokio::main]
async fn main() -> Result<(), Box> {
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(())
}
```
### Client Verification (Server verifying mTLS Client)
```rust
use ans_verify::{AnsVerifier, CertFingerprint, CertIdentity, VerificationOutcome};
#[tokio::main]
async fn main() -> Result<(), Box> {
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(())
}
```
## Configuration
### Verifier Builder Options
```rust
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?;
```
### 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
```rust
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 |
## Verification Outcomes
| 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 |
## 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 │ │ │ │
│ └──────────┬──────────┘ └──────────┬──────────┘ │
└─────────────┼───────────────────────────┼───────────────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────────────────────────────┐
│ 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:
```toml
[dev-dependencies]
ans-verify = { ..., features = ["test-support"] }
```
```rust
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:
```bash
cargo test --workspace --features ans-verify/test-support
```
## Logging
The libraries use the `tracing` crate for structured logging:
```rust
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:
```bash
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:
```toml
[dependencies]
ans-verify = { ..., features = ["rustls"] }
```
### Server Certificate Verification (Client-side)
Use `AnsServerCertVerifier` to verify server certificates match the ANS badge during the TLS handshake:
```rust
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();
```
### Client Certificate Verification (Server-side mTLS)
Use `AnsClientCertVerifier` for the TLS handshake (validates chain to Private CA), then verify against the badge post-handshake:
```rust
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);
}
}
```
## 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` |
### Generate Test Certificates
```bash
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:
```bash
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:
```bash
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
```
### Basic Verification Examples
```bash
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
```