Using the AnkaSecure Key Management SDK in Java
SDK Version: 3.4.1
Java Version: 17+
Artifact:co.ankatech:ankasecure-key-management-sdk:3.4.1
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.1</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, and archive(kid, ArchiveKeyRequest) takes a required 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().reason("Retired after the PQC migration").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; the request's reason is required |
requestVersion(stableKid, version, MaterialMarkForDestructionRequest) | The same for one RETIRED, REVOKED or PENDING_PRIMARY version; the reason is required |
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 tenant's governed business scenarios: create, update, suspend, resume, close |
| Exchanges | sdk.exchanges() | a directed relationship between two endpoints: create a draft, add its grants, validate, preview and activate it, suspend, resume, revoke |
| Capability grants | sdk.capabilityGrants() | which actor may use which key for which capability, in which context; preview shows what a grant would confer before it is created |
| Constraint policies | sdk.constraintPolicies() | the limits and schedule a grant pins, and their version history |
Every grant belongs to exactly one exchange context, and an actor's live grants never overlap: a grant that would overlap one is refused with 409 capability-grant-overlap.
9.1 Exchanges and their grants
ExchangeDetailResponse draft = sdk.exchanges().create(exchangeRequest); // origin, destination, context
sdk.exchanges().createGrant(draft.getId(), new ExchangeGrantRequest()
.actorId(actorId)
.capabilities(List.of("ENCRYPT"))
.assetKids(List.of("payments-data-key")));
ValidationResultResponse result = sdk.exchanges().validate(draft.getId());
ExchangeActivationPreviewResponse preview = sdk.exchanges().previewActivation(draft.getId());
sdk.exchanges().activate(draft.getId(), new ActivateRequest().version(draft.getVersion()));
| Method | Does |
|---|---|
createGrant(UUID id, ExchangeGrantRequest request) | Adds a grant owned by the exchange, in its context. The actor belongs to the origin or destination application; the grant names one key (assetKids), or 1 to 32 ordered keyPairs for REENCRYPT or RESIGN. It authorizes only while the exchange is active |
revokeGrant(UUID id, UUID grantId, RevokeExchangeGrantRequest request) | Revokes one grant of the exchange; the reason is required. The last live grant of an active or suspended exchange is not revoked alone (422) |
activate(UUID id, ActivateRequest request) | Makes every live grant of the exchange authorize; the request carries only the expected version |
previewActivation(UUID id) | What activation would confer, writing nothing |
list(ExchangeListQuery query) | Filters include withRelationship (SELF, INTERNAL, OUTBOUND, INBOUND) and withExchangeContextId |
State changes of an exchange carry its expected version, so a concurrent change is refused with 409 instead of being overwritten.
9.2 Capability grants
| Method | Does |
|---|---|
revoke(UUID grantId, RevokeCapabilityGrantRequest request) | Revokes a grant; terminal. The reason is required |
replacePolicyVersion(UUID grantId, ReplaceCapabilityGrantPolicyVersionRequest request) | Re-pins the grant to another constraint policy version (constraintPolicyVersionId), from its next request on, and returns the new assignment |
policyAssignments(UUID grantId) | The versions the grant has been pinned to, oldest first |
effectiveRestrictions(UUID grantId, OffsetDateTime at) | The version, limits and schedule in force at an instant; null means now |
A re-pin keeps the uses already counted against the grant, and the new version's maxUsage applies to that count. Uses made while no limit was pinned are not counted, so they do not count toward a limit pinned later: a grant re-pinned from an unlimited version starts counting at 0.
CapabilityGrantPolicyAssignment pin = sdk.capabilityGrants().replacePolicyVersion(grantId,
new ReplaceCapabilityGrantPolicyVersionRequest().constraintPolicyVersionId(versionId));
sdk.capabilityGrants().revoke(otherGrantId,
new RevokeCapabilityGrantRequest().reason("Partner contract ended"));
9.3 Constraint policy versions
| Method | Does |
|---|---|
versions(UUID policyId) | Every version of a policy, oldest first, each with the number of live grants pinned to it |
version(UUID policyId, int version) | One version: the limits and schedule a grant pinned to it enforces |
compareVersions(UUID baseVersionId, UUID targetVersionId) | The fields that differ between two versions of one policy |
9.4 Exchange contexts
create(CreateExchangeContextRequest request) creates a context owned by the tenant; a context names no application. close(UUID contextId, CloseExchangeContextRequest request) closes it for good — it revokes every exchange in it that is not terminal and retires every live grant in it — and the reason is required. A context an internal cryptographic use case owns reads ownedByUseCaseId and admits only that use case's grants.
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.
| Method | Does |
|---|---|
renew(UUID useCaseId) | Extends validUntil to one year after the later of now and its current value; an expired use case becomes active again with the same actor and credential, a suspended one stays suspended. A revoked use case is refused (409) |
revoke(UUID useCaseId, RevokeInternalCryptoUseCaseRequest request) | Revokes the use case and the identity, grants and credential it provisioned; terminal. The reason is required |
suspend, resume | Suspend and resume the use case |
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.1
Last updated: 2026-10-26