Java / Spring Boot
Adopt the Crypto Agility Plane in Java: resolve an intent, run the operation on the resolved algorithm, and observe what ran.
Prérequis
- JDK 17+ (the reference client uses `java.net.http.HttpClient` and a Java record, which requires JDK 16+).
- A CAP API key (dke_cap_…) for the calling tenant — see Getting Started.
- An active tenant policy (bootstrap one from a template).
Install
The Java client is a single self-contained class, ch.duokey.cap.CapClient,
downloaded from your Cockpit. It depends only on the JDK HTTP client plus Jackson for
JSON.
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/java" \
| jq -r '.content' > src/main/java/ch/duokey/cap/CapClient.java<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.8</version>
</dependency>The 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). |
CapDecision resolve(String dataClass, String purpose) | Resolve an intent to a decision. |
void observe(String dataClass, String purpose, String observedAlgorithm, String source) | Report the algorithm that actually ran (drift detection). |
String protect(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 = cap.resolve("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"Configure with Spring
cap.base-url=https://cockpit.example.com/api
cap.key=${CAP_KEY}@Configuration
public class CapConfig {
@Bean
CapClient capClient(
@Value("${cap.base-url}") String baseUrl,
@Value("${cap.key}") String capKey) {
return new CapClient(baseUrl, capKey);
}
}Bind cap.key from an environment variable or your secret manager. Never commit a
dke_cap_… key.
Resolve and run a signature
You never hard-code the algorithm. You resolve the intent, run the operation with the resolved algorithm and the policy-referenced key, then observe.
@Service
public class SignatureService {
private final CapClient cap;
public SignatureService(CapClient cap) { this.cap = cap; }
public byte[] sign(byte[] payload) {
// 1) Resolve WHAT (pii + sign) -> HOW (algorithm, posture, key, backend)
CapDecision d = cap.resolve("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.
cap.observe("pii", "sign", algorithm, "payments-svc");
return signature;
}
}CAP resolves the decision (algorithm, posture, key reference, backend) — it
never holds your key bytes. The signature/encryption itself runs in your process or
on the resolved backend, using the key referenced by d. For PQC and hybrid
algorithms, the cryptographic operation executes on the DuoKey backend in production.
The convenience helpers (protect / reveal)
For symmetric storage and tokenization the client offers one-liners:
String tagged = cap.protect("4111111111111111", "pan", "tokenize");
String pan = cap.reveal(tagged);In the downloaded reference client, protect/reveal run
aes256-gcm with a local demo key and a stand-in tokenizer — they
are 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
protect() as real tokenization.
Observe and drift
observe(...) 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). Security teams review
findings via the Cockpit (GET /api/cap/drift).
// Suppose a legacy path signed with plain ECDSA while policy resolved to hybrid:
cap.observe("pii", "sign", "ecdsa-p256", "legacy-batch");
// -> CAP flags a drift finding for pii.sign (declared hybrid, observed classical)The posture flip — zero code change
Today
resolve("pii","sign") returns hybrid-ml-dsa65-ecdsa-p256 under a hybrid policy.
Security raises the floor
The team creates a new policy version with a pqc_only posture (or bootstraps cnsa_2_0).
Next resolve
The same code now receives ml-dsa65 — no recompile, no redeploy of application logic.
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 for the intent | resolved: false with a reason | Fix the policy rule or the requested residency. |
| Unknown data class / purpose | Client/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. |