Using the AnkaSecure Key Management SDK in Java
SDK Version: 3.4.0
Java Version: 17+
Artifact:co.ankatech:ankasecure-key-management-sdk:3.4.0
This guide covers the configuration, the sign-in and every operation group of the Key Management SDK. Read Prerequisites first: the workload, its key and its scopes must exist.
1. Installation
<dependency>
<groupId>co.ankatech</groupId>
<artifactId>ankasecure-key-management-sdk</artifactId>
<version>3.4.0</version>
</dependency>
For a build that cannot reach Maven Central, the JAR and its checksums are on the Downloads page.
2. Configuration
Properties props = new Properties();
props.setProperty("kmsdk.scheme", "https");
props.setProperty("kmsdk.host", "api.example.com");
props.setProperty("kmsdk.port", "443");
AnkaSecureKeyManagementSdk factory = new AnkaSecureKeyManagementSdk(props)
.withCorrelationIdSupplier(() -> MDC.get("requestId")); // optional: the X-Request-ID of each call
Every key and its default is listed in Prerequisites § Configuration. The factory is thread-safe and can produce several authenticated instances.
3. Signing in
authenticateWorkload(WorkloadCredential) is the only sign-in. It returns an AuthenticatedKeyManagementSdk, which is AutoCloseable: close it when the work is done.
| Credential | Factory method | The workload proves itself with |
|---|---|---|
| Private key file | WorkloadCredential.privateKeyJwt(workloadId, Path) | a private_key_jwt assertion signed by the key in a PKCS#8 PEM or private JWK file (owner-only on POSIX) |
| Private key object | WorkloadCredential.privateKeyJwt(workloadId, PrivateKey) | the same, with a JCA key your product already holds |
| Federated token | WorkloadCredential.federated(workloadId, Supplier<String>) | a token an external issuer the workload is bound to minted for it |
| Federated token file | WorkloadCredential.federatedTokenFile(workloadId, Path) | the token in a file a platform refreshes (for example a projected service-account token) |
| Federated token command | WorkloadCredential.federatedTokenCommand(workloadId, List<String>) | the token a command prints |
What happens on sign-in:
- The SDK reads the authorization server metadata and checks that it names the configured platform issuer (
kmsdk.auth.issuer), lists ES256 DPoP proofs and publishes a token endpoint. A deployment that does not issue DPoP-bound workload tokens is refused here, before any credential is sent. - It generates an EC P-256 key in memory for the instance, never written anywhere.
- It requests a token with
grant_type=client_credentials, the workload id, the assertion (or the federated token) and a DPoP proof for the published token endpoint. Noclient_secretor password is ever sent. - Every later request carries
Authorization: DPoP <token>and a new proof bound to the request's method, path and token. The five-minute token is renewed within 60 seconds of its expiry.
The published token endpoint must be on the origin the SDK dials, or sign-in is refused. The proofs name that origin, and the assertion audience is that origin followed by /api. No request follows a redirect: a 3xx answer is an error naming the status.
A refused sign-in raises KeyManagementSdkException with getOAuthError(): invalid_client (the workload, its key or its federation binding was refused), invalid_dpop_proof, or invalid_request (the deployment does not issue workload tokens). A 503 carries getRetryAfter().
try (AuthenticatedKeyManagementSdk sdk = factory.authenticateWorkload(
WorkloadCredential.federatedTokenFile(workloadId, Path.of("/var/run/secrets/tokens/ankasecure")))) {
SessionInfo session = sdk.viewSession(); // workload, tenant, expiry, scopes; never the token
System.out.println(session);
}
4. Keys — sdk.keys()
PageAdminKeyResponse page = sdk.keys().list(KeyListQuery.firstPage()
.withStatus("active").withKty("ML-KEM").withSize(50));
AdminKeyResponse key = sdk.keys().get("payments-data-key");
sdk.keys().create(new OrchestrationKeyRequest()
.kid("payments-data-key").kty("ML-KEM").algorithm("ML-KEM-768")
.purpose(OrchestrationKeyRequest.PurposeEnum.ENCRYPT_DECRYPT));
sdk.keys().update("payments-data-key", new AdminKeyMergePatchRequest()); // JSON Merge Patch of the metadata
AdminKeyExportResponse publicPart = sdk.keys().export("payments-data-key");
List<SupportedAlgorithm> algorithms = sdk.keys().supportedAlgorithms(SupportedAlgorithmQuery.all());
| Method | Does |
|---|---|
list(KeyListQuery) | One page of the tenant's keys; the query carries every filter the endpoint accepts (status, origin, restricted, kty, conformance, page, size, sort) |
get(kid) | The key's metadata |
create(OrchestrationKeyRequest) | A new key generated by the platform; a Key ID that any key of the tenant ever held, destroyed keys included, is refused with 409 …/key-id-retired |
update(kid, AdminKeyMergePatchRequest) | Changes the key's metadata; a weakening change the quorum policy gates throws QuorumApprovalPendingException |
importKey(KeyImportRequest) | Imports one key (JWK) |
importKeystore(Path, metadata, SecretChars) / analyzeKeystore(...) | Imports, or analyses without importing, a PKCS#12 or JKS keystore; the password is sent as the raw bytes of its multipart part and never written to a file |
export(kid) | The public part of the key |
lifecycleCauseVerdicts(kid) | Which lifecycle causes the tenant's policy admits for the key's transitions |
supportedAlgorithms(SupportedAlgorithmQuery) | The algorithms the tenant may use |
suspend, reactivate, revoke, archive | The key's lifecycle transitions; revoke and suspend take a cause and a reason |
5. Key versions — sdk.materials()
A key keeps one Key ID across rotations; each rotation adds a material version.
List<MaterialVersionResponse> versions = sdk.materials().list("payments-data-key");
MaterialVersionResponse v1 = sdk.materials().get("payments-data-key", 1);
sdk.materials().revoke("payments-data-key", 1, new MaterialRevokeRequest());
6. Rotation — sdk.rotation()
RotationAdmissibilityResponse check = sdk.rotation().admissibility("payments-data-key");
KeySummaryResponse rotated = sdk.rotation().rotate("payments-data-key", request, "rotation-2026-11-01");
KeyRotationScheduleView schedule = sdk.rotation().schedule("payments-data-key");
sdk.rotation().replaceSchedule("payments-data-key", scheduleRequest);
KeyRotationExecutionsResponse history = sdk.rotation().listExecutions("payments-data-key",
RotationExecutionListQuery.firstPage());
The rotate overload with an idempotency key sends Idempotency-Key, so a retried call rotates once. listExecutions returns the key's automatic-rotation schedule and the executions of its rotation obligation only.
7. Destruction — sdk.destruction()
A workload never destroys key material. It requests the destruction, and people approve it.
QuorumRequestReceipt receipt = sdk.destruction().requestKey("old-archive-key",
new MarkForDestructionRequest().coolingOffDays(30));
System.out.println(receipt.requestId() + " " + receipt.status());
List<KeyDestructionOrder> orders = sdk.destruction().orders("old-archive-key");
sdk.destruction().cancelKey("old-archive-key"); // only while the destruction is scheduled
| Method | Does |
|---|---|
requestKey(kid, MarkForDestructionRequest) | Opens the quorum request for an ARCHIVED key and returns its receipt |
requestVersion(stableKid, version, MaterialMarkForDestructionRequest) | The same for one RETIRED, REVOKED or PENDING_PRIMARY version |
cancelKey(kid) / cancelVersion(stableKid, version) | Cancels a scheduled destruction; every version returns to the status it had before the approval |
orders(kid) | What each approval scheduled and how it ended |
The whole workflow — mandatory approval, cooling-off period, automatic execution, the Key ID that is never reused — is on the Key Destruction page.
8. Own quorum requests — sdk.quorumRequests()
QuorumRequestPageResponse mine = sdk.quorumRequests().list(QuorumRequestListQuery.firstPage());
QuorumRequestResponse request = sdk.quorumRequests().get(receipt.requestId());
sdk.quorumRequests().cancel(UUID.fromString(receipt.requestId())); // withdraw before approval
An operation the tenant's quorum policy gates throws QuorumApprovalPendingException:
try {
sdk.keys().update("payments-data-key", patch);
} catch (QuorumApprovalPendingException pending) {
QuorumRequestReceipt r = pending.getReceipt();
// r.requestId(), r.operation(), r.requiredApprovals(), r.expiresAt(), r.location()
}
Nothing changes until people approve the request in the Admin Console. The SDK has no approve or reject method.
9. Exchange governance
| Group | Accessor | Typical use |
|---|---|---|
| Exchange contexts | sdk.exchangeContexts() | the context in which an application exchanges protected data; create, update, suspend, resume, close |
| Exchanges | sdk.exchanges() | a governed exchange: create a draft, validate it, preview and activate it, suspend, resume, revoke |
| Capability grants | sdk.capabilityGrants() | which actor may use which key for which capability; preview shows what a grant would confer before it is created |
| Constraint policies | sdk.constraintPolicies() | usage limits and constraints a grant references |
ExchangeDetailResponse draft = sdk.exchanges().create(exchangeRequest);
ValidationResultResponse result = sdk.exchanges().validate(draft.getId());
ExchangeActivationPreviewResponse preview = sdk.exchanges().previewActivation(draft.getId(), "ENCRYPT");
sdk.exchanges().activate(draft.getId(), new ActivateRequest());
State changes of an exchange carry its expected version, so a concurrent change is refused with 409 instead of being overwritten.
10. Internal cryptographic use cases — sdk.internalCryptoUseCases()
try (InternalCryptoUseCaseProvisioning created = sdk.internalCryptoUseCases().create(useCaseRequest)) {
created.clientSecret().ifPresent(secret ->
vault.store(created.useCase().getUseCaseId(), secret.chars())); // shown once: store it now
}
The client secret of a new use case is returned once, in a zeroizable SecretChars holder, and never in the response model. clientSecret() is empty when the use case issued no secret. Closing the provisioning result zeroes the secret, so store it inside the try block.
11. Directory — sdk.directory()
Read-only lookups of the tenant's applications, actors and counterparty types, for building requests that reference them: listApplications, getApplication, listActors, listApplicationActors, getActor, listCounterpartyTypes, getCounterpartyType. Creating or changing them is done in the Admin Console.
12. Values a newer server adds
A server release can add a value to an enumerated member, such as a new key status. The SDK never refuses such a response:
- the enum getter reads the generated
UNKNOWN_DEFAULT_OPEN_APImember; - the
get<Member>AsString()accessor beside it, on every generated model (getStatusAsString()besidegetStatus()), returns the value the server sent, verbatim, as aList<String>for a list of enums; for a value this release declares it returns that value's wire form, and for an absent membernull; - writing the model back, or printing it as JSON, sends the server's value, never the sentinel.
AdminKeyResponse key = sdk.keys().get("payments-data-key");
if (key.getStatus() == AdminKeyResponse.StatusEnum.UNKNOWN_DEFAULT_OPEN_API) {
System.out.println("Status not known to this SDK release: " + key.getStatusAsString());
}
Branch on the enum for the values you know, read the string when you show or forward one you do not, and upgrade the SDK to interpret the new value.
13. Errors
Every refusal is a KeyManagementSdkException:
| Accessor | Holds |
|---|---|
getHttpStatus() | The HTTP status |
getProblemType() | The RFC 9457 type; classify on it |
getProblemTitle(), getProblemDetail(), getProblemInstance() | The other problem members |
getProblemExtensions() | The extension members, including the support requestId |
getOAuthError(), getRetryAfter() | On a refused token request: the OAuth error and the server's Retry-After |
The complete list of problem types is on the error pages.
Version: 3.4.0
Last updated: 2026-10-26