Skip to content

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>
implementation 'co.ankatech:ankasecure-key-management-sdk:3.4.1'

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:

  1. 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.
  2. It generates an EC P-256 key in memory for the instance, never written anywhere.
  3. 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. No client_secret or password is ever sent.
  4. 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_API member;
  • the get<Member>AsString() accessor beside it, on every generated model (getStatusAsString() beside getStatus()), returns the value the server sent, verbatim, as a List<String> for a list of enums; for a value this release declares it returns that value's wire form, and for an absent member null;
  • 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