Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ jobs:
distribution: 'temurin'

- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@0723195856401067f7a2779048b490ace7a47d7c # v5.0.2
uses: gradle/actions/wrapper-validation@39e147cb9de83bb9910b8ef8bd7fff0ee20fcd6f # v6.0.1

- name: Cache Gradle packages
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.gradle/caches
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ AgentConnection conn = client.connect("https://target-agent.example.com",
// Full verification - DANE + Badge
AgentConnection conn = client.connect("https://target-agent.example.com",
ConnectOptions.builder()
.verificationPolicy(VerificationPolicy.FULL)
.verificationPolicy(VerificationPolicy.DANE_AND_BADGE)
.build());

// With mTLS client certificate
Expand Down Expand Up @@ -507,7 +507,7 @@ ConnectOptions.builder()

// Full verification (DANE + Badge)
ConnectOptions.builder()
.verificationPolicy(VerificationPolicy.FULL)
.verificationPolicy(VerificationPolicy.DANE_AND_BADGE)
.build();
```

Expand Down
5 changes: 5 additions & 0 deletions ans-sdk-agent-client/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ val jacksonVersion: String by project
val bouncyCastleVersion: String by project
val slf4jVersion: String by project
val reactorVersion: String by project
val caffeineVersion: String by project
val junitVersion: String by project
val mockitoVersion: String by project
val assertjVersion: String by project
Expand All @@ -28,6 +29,9 @@ dependencies {
// dnsjava for DANE/TLSA DNS lookups (JNDI doesn't support TLSA)
implementation("dnsjava:dnsjava:3.6.4")

// Caffeine for high-performance caching with TTL and automatic eviction
implementation("com.github.ben-manes.caffeine:caffeine:$caffeineVersion")

// Logging
implementation("org.slf4j:slf4j-api:$slf4jVersion")

Expand All @@ -38,5 +42,6 @@ dependencies {
testImplementation("org.assertj:assertj-core:$assertjVersion")
testImplementation("org.wiremock:wiremock:$wiremockVersion")
testImplementation("io.projectreactor:reactor-test:$reactorVersion")
testImplementation("com.upokecenter:cbor:4.5.4")
testRuntimeOnly("org.slf4j:slf4j-simple:$slf4jVersion")
}
3 changes: 2 additions & 1 deletion ans-sdk-agent-client/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,8 @@ All examples support different ANS verification policies:
| `DANE_REQUIRED` | Requires DANE/TLSA verification |
| `BADGE_REQUIRED` | Requires transparency log verification |
| `DANE_AND_BADGE` | Requires both DANE and Badge |
| `FULL` | DANE + Badge (maximum security) |
| `SCITT_REQUIRED` | Requires SCITT header verification (recommended) |
| `SCITT_ENHANCED` | SCITT required with badge fallback if no headers |

## Integration Patterns

Expand Down
90 changes: 78 additions & 12 deletions ans-sdk-agent-client/examples/a2a-client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,35 +5,45 @@ This example demonstrates ANS verification integration with the official

## Overview

The A2A SDK's built-in `JdkA2AHttpClient` doesn't expose SSL customization, so this
example includes an `HttpClientA2AAdapter` that implements `A2AHttpClient` with a custom
`SSLContext` for ANS certificate capture.
The example includes two verification approaches:

1. **Manual Verification** - Low-level DANE/Badge flow with certificate capture
2. **SCITT with AnsVerifiedClient** - High-level SCITT verification (recommended)

## Prerequisites

- A2A server with HTTPS endpoint (implements `/.well-known/agent-card.json`)
- For Badge verification: Agent in ANS transparency log
- For DANE verification: TLSA DNS records configured
- For SCITT verification: Agent with receipt and status token, client keystore

## Usage

```bash
# Run with default settings
# Run with default settings (Manual DANE/Badge example)
./gradlew :ans-sdk-agent-client:examples:a2a-client:run

# Run with custom server URL
./gradlew :ans-sdk-agent-client:examples:a2a-client:run --args="https://your-a2a-server.example.com:8443"

# Run SCITT example (requires keystore and agent ID)
./gradlew :ans-sdk-agent-client:examples:a2a-client:run \
--args="https://your-server:8443 /path/to/client.p12 password agentId"
```

## Integration Pattern
## Example 1: Manual DANE/Badge Verification

The integration follows a **Pre-verify / Connect / Post-verify** pattern:
The manual integration follows a **Pre-verify / Connect / Post-verify** pattern:

```java
// 1. Set up ConnectionVerifier
// 1. Set up ConnectionVerifier with DANE and Badge
ConnectionVerifier verifier = DefaultConnectionVerifier.builder()
.daneVerifier(new DaneVerifier(new DefaultDaneTlsaVerifier(DaneConfig.defaults())))
.badgeVerifier(new BadgeVerifier(agentVerificationService))
.daneVerifier(new DaneVerifier(new DefaultDaneTlsaVerifier(
DaneConfig.builder().validationMode(DnssecValidationMode.VALIDATE_IN_CODE).build())))
.badgeVerifier(new BadgeVerifier(
BadgeVerificationService.builder()
.transparencyClient(TransparencyClient.builder().build())
.build()))
.build();

// 2. Pre-verify (async DANE lookup)
Expand All @@ -45,7 +55,7 @@ SSLContext sslContext = AnsVerifiedSslContextFactory.create();
// 4. Create A2A HTTP client adapter with custom SSLContext
HttpClientA2AAdapter httpClient = new HttpClientA2AAdapter(sslContext);

// 5. Fetch AgentCard (triggers TLS handshake)
// 5. Fetch AgentCard (triggers TLS handshake, captures certificate)
A2ACardResolver cardResolver = new A2ACardResolver(httpClient, serverUrl, null);
AgentCard agentCard = cardResolver.getAgentCard();

Expand All @@ -72,6 +82,47 @@ client.sendMessage(message);
CertificateCapturingTrustManager.clearCapturedCertificates(hostname);
```

## Example 2: SCITT with AnsVerifiedClient (Recommended)

The high-level approach using `AnsVerifiedClient` handles SCITT automatically:

```java
// 1. Create AnsVerifiedClient with SCITT policy
try (AnsVerifiedClient ansClient = AnsVerifiedClient.builder()
.agentId(agentId)
.keyStorePath(keystorePath, keystorePassword)
.policy(VerificationPolicy.SCITT_REQUIRED)
.build()) {

// 2. Connect (performs preflight for SCITT header exchange)
try (AnsConnection connection = ansClient.connect(serverUrl)) {
System.out.println("SCITT artifacts from server: " + connection.hasScittArtifacts());

// 3. Create A2A HTTP client with ANS SSLContext
HttpClientA2AAdapter httpClient = new HttpClientA2AAdapter(ansClient.sslContext());

// 4. Fetch AgentCard (triggers TLS handshake)
A2ACardResolver cardResolver = new A2ACardResolver(httpClient, serverUrl, null);
AgentCard agentCard = cardResolver.getAgentCard();

// 5. Post-verify server certificate
VerificationResult result = connection.verifyServer();
if (!result.isSuccess()) {
throw new SecurityException("SCITT verification failed: " + result.reason());
}

// 6. Create A2A client and send messages
JSONRPCTransportConfig transportConfig = new JSONRPCTransportConfig(httpClient);
Client client = Client.builder(agentCard)
.withTransport(JSONRPCTransport.class, transportConfig)
.build();

Message message = A2A.toUserMessage("Hello from SCITT-verified A2A client!");
client.sendMessage(message);
}
}
```

## HttpClientA2AAdapter

The adapter wraps Java's `HttpClient` to implement A2A's `A2AHttpClient` interface:
Expand All @@ -92,14 +143,28 @@ This is necessary because:
- `A2AHttpClientFactory` SPI doesn't pass configuration parameters
- The adapter pattern provides a clean way to inject our SSL configuration

## Verification Policies

| Policy | Description | Use Case |
|--------|-------------|----------|
| `PKI_ONLY` | System trust store only | Development, testing |
| `DANE_REQUIRED` | Requires DANE/TLSA | High security with DNSSEC |
| `BADGE_REQUIRED` | Requires transparency log | Legacy production |
| `DANE_AND_BADGE` | Both DANE and Badge | Maximum legacy security |
| `SCITT_REQUIRED` | Requires SCITT artifacts | **Recommended for production** |
| `SCITT_ENHANCED` | SCITT with badge fallback | Migration from badge |

## Key Classes

| Class | Purpose |
|-------|---------|
| `HttpClientA2AAdapter` | A2AHttpClient implementation with custom SSLContext |
| `AnsVerifiedClient` | High-level client with SCITT support and mTLS |
| `AnsConnection` | Connection handle for SCITT verification flow |
| `AnsVerifiedSslContextFactory` | Creates SSLContext with certificate capture |
| `CertificateCapturingTrustManager` | Stores certificates during TLS handshake |
| `DefaultConnectionVerifier` | Coordinates DANE, Badge verification |
| `DefaultConnectionVerifier` | Coordinates DANE, Badge, SCITT verification |
| `TransparencyClient` | Fetches SCITT artifacts and root public key |

## Dependencies

Expand All @@ -109,5 +174,6 @@ dependencies {
implementation("io.github.a2asdk:a2a-java-sdk-client-transport-jsonrpc:1.0.0.Alpha1")
implementation("io.github.a2asdk:a2a-java-sdk-http-client:1.0.0.Alpha1")
implementation("io.github.a2asdk:a2a-java-sdk-spec:1.0.0.Alpha1")
implementation(project(":ans-sdk-agent-client"))
}
```
```
Loading