.NET / EF Core
Adopt the Crypto Agility Plane in .NET: resolve an intent, run the operation on the resolved algorithm, and observe what ran.
Prerequisites
- .NET 6+ (the reference client is BCL-only: System.Net.Http, System.Text.Json, AesGcm).
- A CAP API key (dke_cap_…) for the calling tenant — see Getting Started.
- An active tenant policy (bootstrap one from a template).
Install
The .NET client is a single self-contained class, DuoKey.Cap.CapClient,
downloaded from your Cockpit. It has no NuGet dependencies — it uses only the base
class library. The download endpoint returns a JSON envelope (language,
filename, content), so extract the content field — for example with jq —
rather than saving the response body directly:
curl -H "Authorization: Bearer $COCKPIT_SESSION_JWT" \
"https://cockpit.example.com/api/cap/sdk/download/dotnet" \
| jq -r '.content' > Cap/CapClient.csThe client surface
| Member | Purpose |
|---|---|
new CapClient(string baseUrl, string capKey) | Create a client. baseUrl is the Cockpit API root (…/api); capKey is the dke_cap_… key. |
new CapClient(baseUrl, capKey, byte[]? dataKey) | Optional: supply a 32-byte key for the local demo Protect/Reveal (see caveat below). |
Task<CapDecision> ResolveAsync(string dataClass, string purpose) | Resolve an intent to a decision. |
Task ObserveAsync(string dataClass, string purpose, string observedAlgorithm, string source) | Report the algorithm that actually ran (drift detection). |
Task<string> ProtectAsync(string value, string dataClass, string purpose) | Convenience: resolve + apply (demo helper — see caveat). |
string Reveal(string tagged) | Reverse a value produced by Protect (demo helper). |
CapDecision is a record: Algorithm, Posture, QuantumResistant, Backend.
CapDecision d = await cap.ResolveAsync("pan", "storage");
d.Algorithm; // e.g. "aes256-gcm"
d.Posture; // "classical" | "hybrid" | "pqc_only"
d.QuantumResistant; // false for classical, true otherwise
d.Backend; // "software" | "fips1403_hsm" | "pqc_capable" | "local_tokenizer"Register with dependency injection
{
"Cap": {
"BaseUrl": "https://cockpit.example.com/api"
}
}builder.Services.AddSingleton(sp =>
{
var cfg = sp.GetRequiredService<IConfiguration>();
var baseUrl = cfg["Cap:BaseUrl"]!;
var capKey = Environment.GetEnvironmentVariable("CAP_KEY")!;
return new DuoKey.Cap.CapClient(baseUrl, capKey);
});Bind the key from an environment variable (CAP_KEY) or your secret manager. Never
commit a dke_cap_… key.
Resolve and run a signature
public class SignatureService
{
private readonly CapClient _cap;
public SignatureService(CapClient cap) => _cap = cap;
public async Task<byte[]> SignAsync(byte[] payload)
{
// 1) Resolve WHAT (pii + sign) -> HOW
CapDecision d = await _cap.ResolveAsync("pii", "sign");
// 2) Run the signature with d.Algorithm using the referenced key.
string algorithm = d.Algorithm; // e.g. "hybrid-ml-dsa65-ecdsa-p256"
byte[] signature = _signer.Sign(payload, algorithm);
// 3) Close the loop: report what actually ran.
await _cap.ObserveAsync("pii", "sign", algorithm, "payments-svc");
return signature;
}
}Protect fields with EF Core
A common pattern is to protect a sensitive column with an EF Core value converter that delegates to CAP. The intent (data class + purpose) is fixed per column; the algorithm is resolved by policy.
public class Customer
{
public int Id { get; set; }
// The plane resolves HOW; this column just declares it is a PAN to be tokenized.
public string Pan { get; set; } = default!;
}
// A converter that tokenizes on save and reveals on read via CAP.
public sealed class CapTokenizeConverter : ValueConverter<string, string>
{
public CapTokenizeConverter(CapClient cap)
: base(v => cap.ProtectAsync(v, "pan", "tokenize").GetAwaiter().GetResult(),
v => cap.Reveal(v)) { }
}
protected override void OnModelCreating(ModelBuilder b) =>
b.Entity<Customer>().Property(c => c.Pan).HasConversion(new CapTokenizeConverter(_cap));In the downloaded reference client, ProtectAsync/Reveal run
aes256-gcm with a local demo key and a stand-in tokenizer — for wiring
and testing, not production. Production tokenization and vault-backed encryption run
server-side against the policy's key_ref. Do not ship the demo helpers as real
tokenization.
Observe and drift
ObserveAsync(...) reports the algorithm that actually executed. If it is weaker than
the resolved decision, CAP records a drift finding (warning if still
quantum-resistant, critical if it dropped to classical), reviewable in the Cockpit.
await _cap.ObserveAsync("pii", "sign", "ecdsa-p256", "legacy-batch");
// -> CAP flags a drift finding: declared hybrid, observed classicalThe posture flip — zero code change
Today
ResolveAsync("pii","sign") returns hybrid-ml-dsa65-ecdsa-p256 under a hybrid policy.
Security raises the floor
A new policy version sets a pqc_only posture (or bootstraps cnsa_2_0).
Next resolve
The same code now receives ml-dsa65 — no recompile, no redeploy.
Error handling
| Situation | What you get | What to do |
|---|---|---|
| No active policy for the tenant | resolved: false, reason "no active CAP policy…" | Bootstrap a policy from a template. |
| Posture floor cannot be met | resolved: false with a reason | Fix the policy rule or the requested residency. |
| Unknown data class / purpose | Validation error | Use a valid data class and one of storage / transport / tokenize / sign. |
| Invalid or revoked key | HTTP 401/403 from /cap/sdk/* | Mint a new dke_cap_ key. |
| cap.enabled feature off | Access denied | The tenant needs the Enterprise or Free Trial edition. |