Skip to content
Merged
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

Large diffs are not rendered by default.

367 changes: 367 additions & 0 deletions .agent/plans/2026-05-27-ap-enrollment-key-naming/research.md

Large diffs are not rendered by default.

23 changes: 14 additions & 9 deletions docs/advanced/key-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ AAuth agents need persistent signing keys. The SDK provides two built-in storage

The `IKeyStore` interface defines async key storage for agent workflows (enrollment, token refresh). The SDK ships two built-in implementations: `InMemoryKeyStore` and `FileKeyStore`.

> **Note:** For AP-enrolled agents, the `handle` parameter passed to `IKeyStore` methods is the `LocalKeyHandle` returned by `EnrolAsync` (defaults to the durable key's JWK thumbprint). It is a purely local identifier — not an AP-assigned value. The AP identifies the agent at refresh time from the HTTP signature, never from this string.

```csharp
namespace AAuth.Crypto;

public interface IKeyStore
{
Task<IAAuthKey?> LoadAsync(string keyId, CancellationToken ct = default);
Task StoreAsync(string keyId, IAAuthKey key, CancellationToken ct = default);
Task DeleteAsync(string keyId, CancellationToken ct = default);
Task<IAAuthKey?> LoadAsync(string handle, CancellationToken ct = default);
Task StoreAsync(string handle, IAAuthKey key, CancellationToken ct = default);
Task DeleteAsync(string handle, CancellationToken ct = default);
Task<string[]> ListAsync(CancellationToken ct = default);
}
```
Expand Down Expand Up @@ -108,11 +110,14 @@ public sealed class AzureKeyVaultStore : IKeyStore

public AzureKeyVaultStore(SecretClient client) => _client = client;

public async Task<IAAuthKey?> LoadAsync(string keyId, CancellationToken ct)
// Spec: 'handle' is agent-chosen, never leaves the agent.
// It is distinct from the AP-published kid (AgentTokenKid) and
// the JWK thumbprint used for cryptographic identity.
public async Task<IAAuthKey?> LoadAsync(string handle, CancellationToken ct)
{
try
{
var secret = await _client.GetSecretAsync(keyId, cancellationToken: ct);
var secret = await _client.GetSecretAsync(handle, cancellationToken: ct);
return AAuthKey.FromJwkJson(secret.Value.Value);
}
catch (RequestFailedException ex) when (ex.Status == 404)
Expand All @@ -121,15 +126,15 @@ public sealed class AzureKeyVaultStore : IKeyStore
}
}

public async Task StoreAsync(string keyId, IAAuthKey key, CancellationToken ct)
public async Task StoreAsync(string handle, IAAuthKey key, CancellationToken ct)
{
var jwk = ((AAuthKey)key).ToPrivateJwk().ToJsonString();
await _client.SetSecretAsync(new KeyVaultSecret(keyId, jwk), ct);
await _client.SetSecretAsync(new KeyVaultSecret(handle, jwk), ct);
}

public async Task DeleteAsync(string keyId, CancellationToken ct)
public async Task DeleteAsync(string handle, CancellationToken ct)
{
await _client.StartDeleteSecretAsync(keyId, ct);
await _client.StartDeleteSecretAsync(handle, ct);
}

public async Task<string[]> ListAsync(CancellationToken ct)
Expand Down
2 changes: 1 addition & 1 deletion docs/concepts.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ AAuth is a protocol for autonomous agent authorization. This page maps protocol
| **Person Server (PS)** | Represents the user. Manages consent, federates to AS. | `TokenExchangeClient`, `ServerMetadata` |
| **Access Server (AS)** | Issues auth tokens. Enforces resource access policy. | `AuthTokenBuilder` |

> **Agent Provider (AP)** is a supporting role — it issues agent tokens binding keys to identities (`AgentProviderClient`) but is not one of the four protocol participants.
> **Agent Provider (AP)** is a supporting role — it issues agent tokens binding keys to identities (`AgentProviderClient`) but is not one of the four protocol participants. The AP and the agent never share a keystore: the agent holds the **private** durable key locally in its own `IKeyStore`; the AP holds only the **public** key, indexed by JWK thumbprint. At refresh time the AP identifies the agent from the HTTP signature, not from any string the agent sends. See [Bootstrap & Enrollment](workflows/bootstrap-enrollment.md#key-identifiers-what-goes-where) for the three identifiers in play.

## Three Layers

Expand Down
41 changes: 29 additions & 12 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,30 +131,30 @@ var enrol = await AAuthClientBuilder
.WithKeyStore(keyStore)
.EnrolAsync();

// Only the key ID needs to be recorded in app config
// (the key itself is already in the keystore)
Console.WriteLine($"Enrolled. Add to config: AAuth:KeyId = {enrol.EnrolledKeyId}");
// Only the local key handle needs to be recorded in app config
// (the key itself is already in the keystore; defaults to the JWK thumbprint)
Console.WriteLine($"Enrolled. Add to config: AAuth:LocalKeyHandle = {enrol.LocalKeyHandle}");
```

### Application (every startup)

Load the key by ID from the store and let the SDK manage agent tokens:
Load the key by handle from the store and let the SDK manage agent tokens:

```csharp
using AAuth.Agent;
using AAuth.Crypto;
using AAuth.HttpSig;

var keyStore = FileKeyStore.Default();
var keyId = configuration["AAuth:KeyId"]!;
var localKeyHandle = configuration["AAuth:LocalKeyHandle"]!;
var apRefreshEndpoint = configuration["AAuth:ApRefreshEndpoint"]!;
var key = await keyStore.LoadAsync(keyId)
?? throw new InvalidOperationException($"Key '{keyId}' not found. Run enrollment first.");
var key = await keyStore.LoadAsync(localKeyHandle)
?? throw new InvalidOperationException($"Key '{localKeyHandle}' not found. Run enrollment first.");

// The SDK acquires the agent token lazily on first request
// via WithTokenRefresh, then keeps it fresh automatically.
using var client = new AAuthClientBuilder(key)
.WithTokenRefresh(AgentProviderTokenRefresher.Create(apRefreshEndpoint, keyId)
.WithTokenRefresh(AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
.WithKeyStore(keyStore)
.Build())
.WithChallengeHandling("https://ps.example")
Expand All @@ -164,6 +164,22 @@ var response = await client.GetAsync("https://resource.example/protected");
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

> **Shortcut — `From(EnrollResult)`**: If you still have the enrollment result object
> (e.g. in a CLI that enrols and immediately calls a resource), use the convenience factory
> to auto-configure the signing mode:
>
> ```csharp
> using var client = AAuthClientBuilder.From(enrol)
> .WithTokenRefresh(AgentProviderTokenRefresher.Create(enrol.ApRefreshEndpoint, enrol.LocalKeyHandle)
> .WithKeyStore(keyStore)
> .Build())
> .WithChallengeHandling("https://ps.example")
> .Build();
> ```
>
> `From()` sets up `UseJwksUri` when the enrollment includes a `JwksUri` and `AgentTokenKid`,
> falling back to the default `jwt` mode otherwise.

<details>
<summary>Step-by-Step (Advanced)</summary>

Expand All @@ -182,16 +198,17 @@ var enrol = await apClient.EnrolAsync(
enrollEndpoint: "https://ap.example/enrol",
personServer: "https://ps.example");

// enrol.Key — your Ed25519 signing key (in keystore)
// enrol.EnrolledKeyId — persisted key identifier (save this to config)
// enrol.AgentToken — initial aa-agent+jwt (short-lived, do not persist)
// enrol.Key — your Ed25519 signing key (in keystore)
// enrol.LocalKeyHandle — agent-local IKeyStore handle (defaults to JWK thumbprint); persist this
// enrol.AgentTokenKid — AP-internal JWT `kid` (opaque; diagnostic only)
// enrol.AgentToken — initial aa-agent+jwt (short-lived, do not persist)
```

### 2. Build the Signed Client with Challenge Handling

```csharp
using var client = new AAuthClientBuilder(enrol.Key)
.WithTokenRefresh(AgentProviderTokenRefresher.Create("https://ap.example/refresh", enrol.EnrolledKeyId)
.WithTokenRefresh(AgentProviderTokenRefresher.Create("https://ap.example/refresh", enrol.LocalKeyHandle)
.WithKeyStore(keyStore)
.Build())
.WithChallengeHandling(personServer: "https://ps.example")
Expand Down
20 changes: 10 additions & 10 deletions docs/reference/dependency-injection.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ flowchart LR
H2 --> H3["Runtime: self-issue token via AgentTokenBuilder"]
end
subgraph CLI/Desktop
P["Provisioning: EnrolAsync(keyStore)"] --> C["App config: AAuth:KeyId"]
P["Provisioning: EnrolAsync(keyStore)"] --> C["App config: AAuth:LocalKeyHandle"]
C --> S["Startup: keyStore.LoadAsync → AddAAuthAgent"]
S --> R["Runtime: SDK calls AP refresh before expiry"]
end
Expand Down Expand Up @@ -75,20 +75,20 @@ app.MapAAuthAgentWellKnown(new AAuthAgentMetadataOptions

### Identity-Based (JWT) — AP-Enrolled (CLI/Desktop Agents)

Load the key by ID from the store and configure token refresh:
Load the key by local handle from the store and configure token refresh:

```csharp
var keyStore = FileKeyStore.Default();
var keyId = configuration["AAuth:KeyId"]!;
var key = await keyStore.LoadAsync(keyId)
?? throw new InvalidOperationException($"Key '{keyId}' not found.");
var localKeyHandle = configuration["AAuth:LocalKeyHandle"]!;
var key = await keyStore.LoadAsync(localKeyHandle)
?? throw new InvalidOperationException($"Key '{localKeyHandle}' not found.");
var apRefreshEndpoint = configuration["AAuth:ApRefreshEndpoint"]!;

builder.Services.AddAAuthAgent("identity", options =>
{
options.Key = key;
options.PersonServer = "https://ps.example";
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, keyId)
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
.WithKeyStore(keyStore)
.Build();
});
Expand All @@ -101,7 +101,7 @@ builder.Services.AddAAuthAgent("identity", options =>
{
options.Key = key;
options.PersonServer = "https://ps.example";
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, keyId)
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
.WithKeyStore(keyStore)
.Build();
});
Expand All @@ -116,7 +116,7 @@ builder.Services.AddAAuthAgent("interactive", options =>
{
options.Key = key;
options.PersonServer = "https://ps.example";
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, keyId)
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
.WithKeyStore(keyStore)
.Build();
options.InteractionHandling = true;
Expand All @@ -141,7 +141,7 @@ builder.Services.AddAAuthAgent("refreshing", options =>
{
options.Key = key;
options.PersonServer = "https://ps.example";
options.TokenRefresher = AgentProviderTokenRefresher.Create("https://ap.example/refresh", keyId)
options.TokenRefresher = AgentProviderTokenRefresher.Create("https://ap.example/refresh", localKeyHandle)
.WithKeyStore(keyStore)
.Build();
});
Expand Down Expand Up @@ -264,7 +264,7 @@ builder.Services.AddAAuthAgent("downstream", options =>
{
options.Key = agentKey;
options.PersonServer = "https://ps.example";
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, keyId)
options.TokenRefresher = AgentProviderTokenRefresher.Create(apRefreshEndpoint, localKeyHandle)
.WithKeyStore(keyStore)
.Build();
});
Expand Down
2 changes: 2 additions & 0 deletions docs/signing-modes/agent-identity-jwks-uri.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ var key = AAuthKey.Generate();

// The jwks_uri comes from the AP's enrollment response — it points
// to the per-agent JWKS endpoint where the AP publishes this agent's key.
// "my-key-1" is the AP-published kid (EnrollResult.AgentTokenKid).
// The AP chooses this value — there is no valid fallback if the AP didn't provide it.
using var client = new AAuthClientBuilder(key)
.UseJwksUri("https://ap.example/agents/aauth:myapp@ap.example/jwks.json", "my-key-1")
.Build();
Expand Down
2 changes: 1 addition & 1 deletion docs/signing-modes/agent-token-jwt.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ using AAuth.Crypto;
using AAuth.HttpSig;

var keyStore = FileKeyStore.Default();
var key = await keyStore.LoadAsync(configuration["AAuth:KeyId"]!);
var key = await keyStore.LoadAsync(configuration["AAuth:LocalKeyHandle"]!);
var apRefreshEndpoint = configuration["AAuth:ApRefreshEndpoint"]!;

using var client = new AAuthClientBuilder(key!)
Expand Down
1 change: 1 addition & 0 deletions docs/signing-modes/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ using AAuth.HttpSig;
var key = AAuthKey.Generate();

// Builder API (recommended)
// For jwks_uri: kid = AP-published JWKS kid (AgentTokenKid) or self-chosen kid for self-hosted
using var client = mode switch
{
"hwk" => new AAuthClientBuilder(key).UseHwk().Build(),
Expand Down
Loading
Loading