Skip to content

Using the AnkaSecure SDK in Java

SDK Version: 3.4.0
Java Version: 17+
Artifact: ankasecure-sdk-3.4.0.jar (co.ankatech:ankasecure-sdk on Maven Central)

This guide shows how to integrate the AnkaSecure SDK into your own Java application. It assumes you have the ankasecure-sdk JAR available (either through a Maven dependency or direct inclusion) and that you have valid credentials: the clientId and clientSecret of a cryptographic actor (an application identity). The SDK has no user sign-in.

Prerequisites: provision in the control plane first

The SDK is data-plane only. The credentials, keys, and capability grants this guide uses must already exist — provision them in the Admin Console, or manage keys and grants with the Key Management SDK or CLI, before integrating. See Before You Integrate: Control-Plane Prerequisites.

Code Examples

This documentation contains code examples showing how to use the SDK API. These examples are intended for developers integrating the SDK into their applications.

For complete working examples, see the Integration Flows section which provides 21 detailed scenarios.


1. Setup & Dependencies

There are two common approaches:

The SDK is published to Maven Central. Add it to your pom.xml; no repository declaration is needed:

<dependency>
    <groupId>co.ankatech</groupId>
    <artifactId>ankasecure-sdk</artifactId>
    <version>3.4.0</version>
</dependency>

Gradle:

implementation 'co.ankatech:ankasecure-sdk:3.4.0'

Every transitive dependency resolves from Maven Central too. Each artifact on Central carries an .asc signature made with the AnkaTech release key; see Downloads.

Do not repackage Bouncy Castle into a fat JAR

The SDK depends on Bouncy Castle (bcprov-jdk18on, bcutil-jdk18on, bcpkix-jdk18on), whose JARs are signed for the JCE. A JDK that enforces provider signatures, such as an Oracle JDK, refuses the provider once its classes are repackaged into another JAR ("JCE cannot authenticate the provider BC"), and the PQC transport then fails with every KEM. If you build a fat JAR, exclude org.bouncycastle from it and ship the three JARs unmodified beside it, on the class path or in the manifest Class-Path.

1.2 Local JAR (Alternative)

For a build that cannot reach Maven Central, download the SDK JAR and add it to your project's classpath:

Maven (system scope):

<dependency>
    <groupId>co.ankatech</groupId>
    <artifactId>ankasecure-sdk</artifactId>
    <version>3.4.0</version>
    <scope>system</scope>
    <systemPath>${project.basedir}/libs/ankasecure-sdk-3.4.0.jar</systemPath>
</dependency>

Gradle:

implementation files('libs/ankasecure-sdk-3.4.0.jar')

2. Loading Configuration

The SDK requires connection details (scheme, host, port) and authentication credentials. You can store them in a Properties object, often loaded from a configuration file:

Properties props = new Properties();
try (FileInputStream fis = new FileInputStream("cli.properties")) {
    props.load(fis);
}
Example sdk-config.properties:

# Application credentials
clientId=mySdkApp
clientSecret=mySdkSecret

# API connection
openapi.scheme=https
openapi.host=demo.ankatech.co
openapi.port=443

# TLS settings
openapi.insecureSkipTlsVerify=false

# Timeouts (milliseconds)
openapi.connectTimeoutMs=10000
openapi.readTimeoutMs=30000
openapi.writeTimeoutMs=30000

Security Note

Never commit clientSecret to version control. Use environment variables or secret managers in production. If your application must persist credentials locally, encrypt them at rest using AES-256-GCM with PBKDF2 key derivation.

See Security Best Practices for complete credential management guidance including encryption examples.


2.1 Complete Configuration Reference

The SDK supports extensive configuration through properties. This reference documents all available properties with defaults and recommendations.

API Connection Settings

Property Default Description
openapi.scheme https Protocol (http or https)
openapi.host demo.ankatech.co API server hostname
openapi.port 443 API server port
openapi.insecureSkipTlsVerify false Skip TLS certificate validation (DEV ONLY)

Example:

openapi.scheme=https
openapi.host=prod.ankatech.co
openapi.port=443
openapi.insecureSkipTlsVerify=false  # NEVER true in production

Timeout Configuration

Property Default Recommended Description
openapi.connectTimeoutMs 10000 30000 Connection timeout (milliseconds)
openapi.readTimeoutMs 30000 300000 Read timeout for large operations
openapi.writeTimeoutMs 30000 300000 Write timeout for uploads

Recommendations:

  • Small files (<100MB): Use defaults
  • Large files or heavy crypto: Increase to 300000 (5 minutes)
  • Parallel operations: Increase timeouts to prevent cancellations

Example:

# For production with large file processing
openapi.connectTimeoutMs=30000
openapi.readTimeoutMs=300000
openapi.writeTimeoutMs=300000

Connection Pool (HTTP Client)

High-Load Configuration

These settings are CRITICAL for parallel execution and production workloads. Insufficient pool size causes StreamResetException: stream was reset: CANCEL errors.

Property Default Recommended (Parallel) Description
okhttp.pool.maxIdleConnections 1000 256 Max idle connections in pool
okhttp.pool.keepAliveDurationMinutes 5 5 Keep-alive duration
okhttp.dispatcher.maxRequests 10000 256 Max concurrent requests (all hosts)
okhttp.dispatcher.maxRequestsPerHost 5000 128 Max concurrent requests per host

Why Connection Pool Matters:

  • Without proper configuration: HTTP/2 stream exhaustion → requests cancelled
  • With proper configuration: Each thread gets its own connection → no cancellation

Sizing Guidelines:

Scenario maxIdleConnections maxRequests maxRequestsPerHost
Sequential (1-4 threads) 16 32 16
Moderate parallel (5-10 threads) 64 128 64
High parallel (10-20 threads) 256 256 128
Very high parallel (20+ threads) 512 512 256

Example (High-Parallel Configuration):

# For 8-16 parallel threads (recommended for load testing)
okhttp.pool.maxIdleConnections=256
okhttp.pool.keepAliveDurationMinutes=5
okhttp.dispatcher.maxRequests=256
okhttp.dispatcher.maxRequestsPerHost=128

Example (Conservative Production):

# For moderate production load (up to 10 concurrent operations)
okhttp.pool.maxIdleConnections=32
okhttp.pool.keepAliveDurationMinutes=5
okhttp.dispatcher.maxRequests=64
okhttp.dispatcher.maxRequestsPerHost=32

HTTP Protocol Settings

Property Default Description
okhttp.http2.enabled true Enable HTTP/2 multiplexing. Required for the streaming operations — see below
okhttp.rateLimitPerSecond disabled Max requests per second (optional throttling)

Setting okhttp.http2.enabled=false disables the streaming operations

The streaming request bodies are duplex, and HTTP/1.1 cannot frame a duplex body. With this property set to false, a streaming call fails fast with a typed Http2RequiredException raised before any request byte is written — the SDK refuses rather than downgrading, because a downgrade deadlocks instead of degrading. Non-streaming calls are unaffected and work over any protocol. See Integration Flows for the full contract.

Example:

okhttp.http2.enabled=true
# okhttp.rateLimitPerSecond=100  # Uncomment to limit to 100 req/s

Proxy Configuration (Optional)

Property Default Description
openapi.proxyHost - HTTP proxy hostname
openapi.proxyPort - HTTP proxy port
openapi.proxyUser - Proxy authentication username
openapi.proxyPassword - Proxy authentication password

Example:

openapi.proxyHost=proxy.corporate.com
openapi.proxyPort=8080
openapi.proxyUser=myuser
openapi.proxyPassword=mypass

Debugging & Diagnostics

Property Default Description
openapi.debugging false Enable HTTP header logging
openapi.enableProgressBar false Show upload progress bar (CLI mode)
openapi.enableTimingMetrics false Log operation timing metrics

Example:

# Development environment
openapi.debugging=true
openapi.enableTimingMetrics=true

# Production (disable all)
openapi.debugging=false
openapi.enableProgressBar=false
openapi.enableTimingMetrics=false

PQC Transport Encryption (Optional)

Property Default Description
pqc.transport.enabled false Enable quantum-resistant transport encryption
pqc.handshake.endpoint /api/pqc-handshake/init Handshake endpoint path — the SDK default is unversioned and does not reach a deployment; set it explicitly (see PQC Transport)
pqc.session.ttl.seconds 600 Client-side lifetime of the cached AES session key
pqc.kem.algorithm ML-KEM-768 KEM for the handshake: ML-KEM-512/768/1024, HQC-128/192/256 or FRODO-976/1344 (see PQC Transport); any other value is refused when the client is built

Example:

# Enable quantum-resistant transport layer
pqc.transport.enabled=true
pqc.handshake.endpoint=/api/v3/pqc-handshake/init
pqc.kem.algorithm=ML-KEM-1024
pqc.session.ttl.seconds=600

Complete Production Configuration Example

# ===================================================================
# AnkaSecure SDK - Production Configuration
# ===================================================================

# Authentication
clientId=prod-app-2024
clientSecret=${ANKASECURE_CLIENT_SECRET}  # From environment variable

# API Connection
openapi.scheme=https
openapi.host=api.ankatech.co
openapi.port=443
openapi.insecureSkipTlsVerify=false

# Timeouts (extended for large files)
openapi.connectTimeoutMs=30000
openapi.readTimeoutMs=300000
openapi.writeTimeoutMs=300000

# Connection Pool (optimized for moderate parallel load)
okhttp.pool.maxIdleConnections=32
okhttp.pool.keepAliveDurationMinutes=5
okhttp.dispatcher.maxRequests=64
okhttp.dispatcher.maxRequestsPerHost=32

# HTTP Protocol
okhttp.http2.enabled=true

# Diagnostics (disabled in production)
openapi.debugging=false
openapi.enableProgressBar=false
openapi.enableTimingMetrics=false

# PQC Transport (optional - quantum-resistant communication)
pqc.transport.enabled=true
pqc.handshake.endpoint=/api/v3/pqc-handshake/init
pqc.kem.algorithm=ML-KEM-768
pqc.session.ttl.seconds=600

Load Testing Configuration Example

# ===================================================================
# AnkaSecure SDK - Load Testing / Parallel Execution Configuration
# ===================================================================

# Authentication
clientId=load-test-app
clientSecret=${ANKASECURE_CLIENT_SECRET}  # From environment variable

# API Connection
openapi.scheme=https
openapi.host=api.ankatech.co
openapi.port=443
openapi.insecureSkipTlsVerify=false

# Timeouts (extended for heavy operations)
openapi.connectTimeoutMs=30000
openapi.readTimeoutMs=300000
openapi.writeTimeoutMs=300000

# Connection Pool (HIGH-LOAD: 8-16 parallel threads)
okhttp.pool.maxIdleConnections=256
okhttp.pool.keepAliveDurationMinutes=5
okhttp.dispatcher.maxRequests=256
okhttp.dispatcher.maxRequestsPerHost=128

# HTTP Protocol
okhttp.http2.enabled=true

# Diagnostics (enabled for troubleshooting)
openapi.debugging=false
openapi.enableTimingMetrics=true

Configuration Best Practices

  1. Never hardcode secrets - Use environment variables: ${ENV_VAR_NAME}
  2. Start conservative - Begin with small pool sizes, increase if needed
  3. Monitor errors - StreamResetException: CANCEL → increase pool size
  4. Tune timeouts - ML-DSA-87, RSA-8192, FRODO take 5-20 seconds each
  5. Production vs Development - Different pool sizes for different environments

3. Instantiating the SDK Factory

After loading properties, create an unauthenticated SDK factory:

import co.ankatech.ankasecure.sdk.AnkaSecureSdk;
import java.util.Properties;

Properties props = loadProperties();  // from previous section

// Create the SDK factory (unauthenticated)
AnkaSecureSdk factory = new AnkaSecureSdk(props);

Note: The factory itself does not perform cryptographic operations. You must authenticate to obtain an AuthenticatedSdk instance (see next section).


4. Authentication

Authenticate the factory to obtain an immutable, thread-safe AuthenticatedSdk instance:

import co.ankatech.ankasecure.sdk.AuthenticatedSdk;

try {
    // Application-based authentication (recommended)
    String clientId = props.getProperty("clientId");
    SecretChars clientSecret = new SecretChars(props.getProperty("clientSecret").toCharArray());

    // Returns AuthenticatedSdk instance (immutable, thread-safe)
    AuthenticatedSdk sdk = factory.authenticateApplication(clientId, clientSecret);
    // The SDK has no human sign-in: it authenticates a cryptographic actor, even when a person
    // runs the program that embeds it.

    // Now use sdk for all operations
    EncryptResult result = sdk.encrypt("my-key", data);

} catch (AnkaSecureSdkException e) {
    System.err.println("Authentication failed: " + e.getMessage());
    return;
}

Key architectural points:

  • Factory pattern: AnkaSecureSdk is the factory, not the operational SDK
  • Return value: Authentication methods return AuthenticatedSdk (not void)
  • Thread-safety: AuthenticatedSdk is immutable and can be shared across 10,000+ threads
  • Token lifecycle: The authenticated instance is usable until its token expires or is revoked. sdk.isTokenExpiredLocally() (renamed from isTokenExpired()) checks only the local exp claim and does not detect server-side revocation; a revoked token surfaces as a typed TokenRevokedException on the next operation. See Token Revocation Architecture* and the SDK security best practices

Authentication methods:

  • Application authentication (recommended): Use the clientId and clientSecret issued for the application's actor in the Admin Console (Application Management*)

5. Key Access (Read-Only)

The SDK is data-plane only: it discovers keys but never creates, imports, rotates, revokes, or deletes them. Provision keys in the control plane (Admin Console Key Management or ankasecure-km keys create) and grant the actor the capabilities it needs — see Before You Integrate. The methods below let the SDK enumerate what has been provisioned.

5.1 Listing Keys

Key listing is paginated. Filters (origin, restricted, kty, status) are carried by KeyFilters and applied server-side before pagination, so the returned totals are filtered counts.

status takes a published key-status token — the same lower-case value KeyMetadata.getStatus() returns, so a value read out of one page can be passed straight back as a filter: active, pending_rotation, rotated, expired, revoked, archived, deleted_logical, suspended, pending_destruction. Any other value is rejected with 400 application/problem+json.

kty holds the same property: it takes one of the twenty-one published key-type tokens — the same value KeyMetadata.getKty() returns — ML-KEM, HQC, FRODO, CMCE, BIKE, SABER, NTRU, NTRUPRIME, SMAUG-T, ML-DSA, SLH-DSA, FALCON, XMSS, LMS, EC, RSA, oct, OKP, SM2, GOST-EC, COMPOSITE. Every hybrid key returns and is selected by the coarse COMPOSITE; which construction it is comes from KeyMetadata.getCompositeMode() (COMPOSITE_KEM_COMBINE / COMPOSITE_SIGNATURE), and KeyMetadata.isComposite() / isCompositeKem() / isCompositeSignature() answer from those two members rather than by inspecting the token. A composite mode name is not a key type and is rejected with 400.

Page-by-page — one server request per page:

import co.ankatech.ankasecure.sdk.model.KeyFilters;
import co.ankatech.ankasecure.sdk.model.KeyMetadata;
import co.ankatech.ankasecure.sdk.model.KeyPage;

KeyPage page = sdk.listKeys(new KeyFilters(null, null, "ML-KEM", "active"), 0, 50);

System.out.println("Total keys: " + page.totalElements()
                   + " across " + page.totalPages() + " pages");
for (KeyMetadata key : page.content()) {
    System.out.println("  - " + key.getKid() +
                       " (" + key.getAlg() + ")" +
                       " - Status: " + key.getStatus());
}

All matching keys — listAllKeys auto-paginates client-side (200-element pages) until every page is fetched:

import co.ankatech.ankasecure.sdk.model.KeyFilters;
import co.ankatech.ankasecure.sdk.model.KeyMetadata;
import java.util.List;

List<KeyMetadata> allKeys = sdk.listAllKeys(KeyFilters.none());

System.out.println("Total keys: " + allKeys.size());

listAllKeys throws PaginationCeilingExceededException if the result spans more than 10,000 pages — it never truncates silently. Use listKeys(filters, page, size) for bounded fetches over very large inventories.

Which keys a listing holds. Without a status filter (KeyFilters.none(), or a KeyFilters whose status is null) a listing holds every key the tenant still holds, in whatever lifecycle state it is in: active, expired, suspended, revoked, archived and pending_destruction. A destroyed key is not among them.

Destroyed keys. Destroyed keys are returned only when status is deleted_logical. Such a row is built from the key identity alone:

  • It carries getKid(), getUuid(), getStatus() (deleted_logical), getCreatedAt() and getDeletedAt() (when the key was destroyed).
  • It reports no shape, material or usage: getKty(), getAlg(), getCompositeMode(), getKdf(), getExportable(), getOrigin(), getRestricted(), getExpiresAt(), getSoftLimitExpiration(), getUsageCount(), getSoftUsageLimit() and getMaxUsageLimit() are null, and getKeyOps() is an empty list. None of them is defaulted.
  • Because no shape is reported, isComposite(), isCompositeKem() and isCompositeSignature() answer false on such a row whatever the destroyed key was. Read getStatus() first.
  • A kid can be taken again after its key is destroyed, so a destroyed-key row can share its kid with a live key, and several destroyed-key rows can share one kid. getUuid() is the key's own identity, never reused, and is the member that tells such rows apart. getDeletedAt() is null on every key the tenant still holds.
import co.ankatech.ankasecure.sdk.model.KeyFilters;
import co.ankatech.ankasecure.sdk.model.KeyMetadata;
import java.util.List;

List<KeyMetadata> destroyed = sdk.listAllKeys(new KeyFilters(null, null, null, "deleted_logical"));
for (KeyMetadata key : destroyed) {
    System.out.println(key.getKid() + " [" + key.getUuid() + "] destroyed at " + key.getDeletedAt());
}

5.2 Key Metadata

Fetch a single key's metadata by kid:

import co.ankatech.ankasecure.sdk.model.KeyMetadata;

KeyMetadata meta = sdk.getKeyMetadata("my-ml-kem-key");
System.out.println("Algorithm: " + meta.getAlg() + " · Status: " + meta.getStatus());

Key creation, export, rotation, and deletion are control-plane operations

The SDK has no method to create, import, export, rotate, revoke, or delete a key. Perform those in the Admin Console Key Management feature or with ankasecure-km (keys create, rotation rotate, keys revoke, keys export). See Before You Integrate and the Key Management CLI Commands.


6. Encryption & Decryption

6.1 In-Memory Operations (< 100 MB)

import co.ankatech.ankasecure.sdk.model.EncryptResult;
import co.ankatech.ankasecure.sdk.model.DecryptResult;
import java.nio.charset.StandardCharsets;

// Encrypt
byte[] plaintext = "Sensitive data".getBytes(StandardCharsets.UTF_8);
EncryptResult encResult = sdk.encrypt("my-ml-kem-key", plaintext);
String jweToken = encResult.getJweToken();

// Decrypt
DecryptResult decResult = sdk.decrypt(jweToken);
byte[] recovered = decResult.getDecryptedData();
System.out.println("Decrypted: " + new String(recovered, StandardCharsets.UTF_8));

A JWE addressed to several keys — the General JSON JWE a multi-recipient PKCS#7 conversion emits, one recipient per tenant key — is decrypted with the ONE recipient you name. Every decrypt method has an overload that takes it as its first argument (decrypt, decryptFile, decryptFileCompact, decryptFileStream, and the decryptThenVerify* family):

// Custodian B opens board minutes addressed to three custodians
DecryptResult decResult = sdk.decrypt("custodian-b", boardMinutesJwe);
System.out.println(decResult.getMeta().getKeyRequested() + " v" + decResult.getMeta().getMaterialVersion());

The server takes one DECRYPT grant decision and one lifecycle decision on that key only and unwraps only its entry; it never tries another recipient. recipientKid is required for a JWE addressed to several keys (HTTP 400 naming it when absent or naming no recipient) and optional for a single-key JWE, where it must equal the one key the JWE names.

6.2 File Operations (Non-Streaming, < 100 MB)

import java.nio.file.Path;

Path inputFile = Path.of("/data/document.pdf");
Path encryptedFile = Path.of("/data/document.pdf.jwe");
Path decryptedFile = Path.of("/data/document_decrypted.pdf");

// Encrypt file
EncryptResult encResult = sdk.encryptFile("my-key", inputFile, encryptedFile);

// Decrypt file
DecryptResultMetadata decMeta = sdk.decryptFile(encryptedFile, decryptedFile);

6.3 Streaming Operations (Large Files > 100 MB)

Path largeFile = Path.of("/data/backup_500MB.tar.gz");
Path encrypted = Path.of("/data/backup_500MB.tar.gz.jwe");
Path decrypted = Path.of("/data/backup_500MB_restored.tar.gz");

// Encrypt with streaming (detached JWE format)
EncryptResult encResult = sdk.encryptFileStream("archive-key", largeFile, encrypted);

// Decrypt with streaming
DecryptResultMetadata decMeta = sdk.decryptFileStream(encrypted, decrypted);

7. Signing & Verification

7.1 In-Memory Operations

import co.ankatech.ankasecure.sdk.model.SignResult;
import co.ankatech.ankasecure.sdk.model.VerifySignatureResult;

byte[] document = Files.readAllBytes(Path.of("contract.pdf"));

// Sign
SignResult signResult = sdk.sign("my-signing-key", document);
String jwsToken = signResult.getJwsToken();

// Verify
VerifySignatureResult verifyResult = sdk.verifySignature(jwsToken);

if (verifyResult.isValid()) {
    System.out.println("Signature is VALID");
} else {
    System.out.println("Signature is INVALID");
}

// One entry per signature, in token order
for (SignatureResult s : verifyResult.getSignatureResults()) {
    System.out.println(s.getKid() + " (" + s.getAlgorithm() + ") v" + s.getMaterialVersion()
            + " verified=" + s.getVerified());
}

A JWS signed by several keys (the General JSON JWS a multi-signer PKCS#7 conversion emits) is verified with the same call: isValid() is true only when EVERY signature verifies, and getSignatureResults() reports each one. The authenticated client needs a VERIFY grant on every signer. SignatureResult is co.ankatech.secure.client.model.SignatureResult.

7.2 File Operations

Path document = Path.of("/legal/contract.pdf");
Path signature = Path.of("/legal/contract.pdf.jws");

// Sign file
SignResult signResult = sdk.signFile("legal-signing-key", document, signature);

// Verify signature
VerifySignatureResult verifyResult = sdk.verifySignature(signature);
System.out.println("Valid: " + verifyResult.isValid());

7.3 Streaming Operations (Large Files)

Path largeLog = Path.of("/audit/access_log_500MB.csv");
Path signature = Path.of("/audit/access_log.sig");

// Sign with streaming
SignResult signResult = sdk.signFileStream("audit-key", largeLog, signature);

// Verify with streaming
VerifySignatureResult verifyResult = sdk.verifySignatureStream(largeLog, signature);

8. Re-encrypt & Re-sign (Key Rotation)

8.1 Re-encrypt

Switch encrypted data from one key to another without exposing plaintext:

import co.ankatech.ankasecure.sdk.model.ReencryptResult;

String oldJwe = "eyJhbGc...";  // Encrypted with old-key-2023

ReencryptResult result = sdk.reencrypt("new-key-2024", oldJwe);
String newJwe = result.getJweToken();

System.out.println("Migrated from: " + result.getOldKeyAlgorithmUsed());
System.out.println("Migrated to: " + result.getNewKeyAlgorithmUsed());

A JWE addressed to several keys: name the ONE recipient being rotated in sourceKid. The output keeps every other recipient in its position and puts newKid in place of the replaced one, under a fresh content-encryption key, so the replaced key cannot read it:

// Rotate custodian A of a three-custodian JWE to the 2026 archive key
ReencryptResult result = sdk.reencrypt("archive-2026", "custodian-a", boardMinutesJwe);
for (ReencryptedRecipient r : result.getRecipients()) {
    System.out.println(r.getKid() + " (" + r.getAlgorithm() + ") v" + r.getMaterialVersion());
}

The authenticated client needs REENCRYPT from the replaced key to newKid and to every kept recipient, and no DECRYPT grant. sourceKid is null for a JWE that names one key (present, it must equal that key) and names the source key for a JWE that names none. The sourceKid overloads are reencrypt, reencryptFile, reencryptFileCompact and reencryptFileStream.

8.2 Re-sign

import co.ankatech.ankasecure.sdk.model.ResignResult;

String oldJws = "eyJhbGc...";  // Signed with old-rsa-key

ResignResult result = sdk.resign("new-ml-dsa-key", oldJws);
String newJws = result.getJwsToken();

A JWS signed by several keys: name the ONE signer being rotated in sourceKid. Every signature is verified before anything is signed — one that does not verify refuses the whole call with HTTP 400 — and only the named signature is replaced; every other signature and the payload are returned as received:

ResignResult result = sdk.resign("treasury-signer-2026", "treasury-signer-2024", coSignedJws);
for (SignatureResult s : result.getSignatureResults()) {
    System.out.println(s.getKid() + " verified=" + s.getVerified());
}

The authenticated client needs RESIGN from the replaced signer to newKid and VERIFY on every kept signer. The sourceKid overloads are resign (also with a JwsSerialization), resignFileCompact and resignFileStream(oldJwsHeader, newKid, sourceKid, input, output).

For file-based rotation, use sdk.reencryptFile(...) or sdk.resignFileCompact(...). For streaming (files > 100MB), use sdk.reencryptFileStream(...) or sdk.resignFileStream(...).


9. Composite Hybrid Keys (Quantum-Resistant)

Composite keys combine classical and post-quantum algorithms for defense-in-depth security. Provision them in the control plane, then use them from the SDK exactly like simple keys.

9.1 Provision the Composite Key (Control Plane)

Composite keys are created in the Admin Console Key Management feature or with ankasecure-km keys create — the SDK never creates them. For example:

  • A hybrid encryption key: COMPOSITE_KEM_COMBINE, X25519+ML-KEM-768 (requires BOTH X25519 and ML-KEM-768 to decrypt — AND-decrypt model).
  • A composite signature key: COMPOSITE_SIGNATURE, Ed25519+ML-DSA-65 (one composite signature, valid only when BOTH components verify).

Grant the actor ENCRYPT/DECRYPT (encryption key) or SIGN/VERIFY (signature key) on the resulting kid. See Before You Integrate.

9.2 Using Composite Keys (Transparent API)

Composite keys work with the same encryption/signing methods as simple keys:

// Encryption with composite key (transparent - same API)
EncryptResult encResult = sdk.encrypt("hybrid-encryption-key", data);

// Signing with composite key (transparent - same API)
SignResult signResult = sdk.sign("hybrid-signature-key", document);

The SDK automatically handles the hybrid encryption or composite signature based on the key type.

Supported combinations: 41 validated pairings

  • 27 COMPOSITE_KEM_COMBINE (encryption)
  • 14 COMPOSITE_SIGNATURE (signatures)

See Composite Algorithms Catalog for complete list of valid combinations.


10. Error Handling & Logging

Exception Handling

AnkaSecureSdkException: Thrown for server-side errors (HTTP 401, 404, 500, etc.) or internal SDK errors (invalid responses, configuration issues).

Common error scenarios:

  • 401 Unauthorized: Invalid credentials or expired token
  • 404 Not Found: Key ID does not exist
  • 403 Forbidden: Insufficient permissions for operation
  • 500 Internal Server Error: Server-side error (check correlation ID)

getErrorCode() returns the SdkErrorCode of the failure, and getRetryAfterSeconds() the server's Retry-After value in whole seconds (null when the header was absent or not in integer-seconds form). The platform sends Retry-After on refusals that are expected to clear: a 502/504 while a circuit breaker toward an upstream dependency is open, a 503 for a dependency or key-protection backend that is not ready yet or for the token endpoint's credential-verification bound, a 429 from the token endpoint's per-source rate limit, and the 409 whose code is STATEFUL_KEY_BUSY.

A 503 whose problem type is tenant-in-maintenance means the caller's tenant is temporarily unavailable for cryptographic and key operations while its keys move to another custody: nothing was executed, and the same request can be sent again after getRetryAfterSeconds() seconds. Its getErrorCode() is HTTP; read the type from getResponseBody(). The response states the condition only — no reason, no end time — so retry on the hint rather than on a schedule of your own, and do not treat it as an authentication or permission failure.

Stateful signing keys: STATEFUL_KEY_EXHAUSTED and STATEFUL_KEY_BUSY

A stateful hash-based key (XMSS, XMSS^MT, LMS/HSS) holds a finite number of one-time signatures, and the server spends one per signature (Stateful Hash-Based Signature Keys). Every method that produces a signature — sign*, signThenEncrypt*, resign* (the new key is the one that signs) and convertPkcs7ToJose* — can therefore be refused with an HTTP 409 that the SDK types by its RFC 9457 problem type, so the two opposite recoveries are told apart without parsing the message:

SdkErrorCode Problem type Meaning Recovery
STATEFUL_KEY_EXHAUSTED stateful-key-exhausted Permanent for that key: no one-time signature left, or the key predates a database restore Do not retry — the same request is refused again. Rotate the key (a control-plane operation) or sign with another one; its earlier signatures still verify
STATEFUL_KEY_BUSY stateful-key-busy Transient: another request is reserving the key's next signature Retry the same request after getRetryAfterSeconds() seconds; wait at least one second when it is null

Either refusal is decided before any output exists, so a file method writes no result. On a streaming route and on the PKCS#7 conversion the one-time signature is reserved before the request body is processed, so a request then refused for its body spends one index without producing a signature. Every other failure keeps its own code.

try {
    return sdk.sign(kid, data);
} catch (AnkaSecureSdkException e) {
    if (e.getErrorCode() == SdkErrorCode.STATEFUL_KEY_BUSY) {
        Long seconds = e.getRetryAfterSeconds();
        Thread.sleep(1000L * (seconds == null ? 1L : seconds));
        return sdk.sign(kid, data);
    }
    if (e.getErrorCode() == SdkErrorCode.STATEFUL_KEY_EXHAUSTED) {
        // Permanent for this key: rotate it instead of retrying
        logger.warn("Key {} has no one-time signature left; rotate it", kid);
    }
    throw e;
}

Flow 16 applies the same handling to its shared XMSS key.

Logging

The SDK uses two loggers:

  • devLogger: Debug-level information (stack traces, HTTP details)
  • userLogger: User-friendly messages (WARN/INFO level)

Configuration:

To enable debug logging, configure your logging framework (Logback, Log4J) to display DEBUG or TRACE messages for the co.ankatech.cli.dev logger.

Example logback.xml:

<logger name="co.ankatech.cli.dev" level="DEBUG"/>
<logger name="co.ankatech.cli.user" level="INFO"/>

10.1 Values a newer server adds

A server newer than this SDK can return a value this release does not declare, such as a new key status or key origin. The SDK does not refuse the response. The typed accessor reads the SDK's own unknown member, and an AsString accessor next to it returns the value the server sent, verbatim:

KeyMetadata key = sdk.getKeyMetadata("my-ml-kem-768-key");
if (key.getOrigin() == KeyOrigin.UNKNOWN) {
    System.out.println("Origin not known to this SDK release: " + key.getOriginAsString());
}

For a declared value the AsString accessor returns its wire value; for an absent member it returns null; a list of enums reads as a List<String>. Serializing the model writes the server's value back. The accessors are KeyMetadata.getCompositeModeAsString(), getOriginAsString() and getStatusAsString(); AlgorithmInfo.getCompositeModeAsString(), getStatusAsString() and getCategoryAsString(); QualifiedTimestampInfo.getTrustStatusAsString(); and get<Name>AsString() on every generated model. An unknown member is never accepted as a filter value. Upgrade the SDK to interpret the new value.


11. Utility (Caller-Supplied Key) Operations

These operations enable cross-platform cryptographic collaboration using caller-supplied public keys that are NOT stored in the keystore. Because no stored key is referenced, they need no capability grant.

11.1 Verify a Signature with an External Public Key

Verify a detached JWS using a partner's public key without importing it.

import co.ankatech.ankasecure.sdk.model.VerifySignatureResult;
import java.nio.file.Path;

// kty/alg describe the partner key; publicKeyB64 is their Base64-encoded public key
VerifySignatureResult result = sdk.verifySignatureUtilityStream(
    "ML-DSA", "ML-DSA-87",
    partnerPublicKeyB64,
    detachedSignatureB64,
    Path.of("contract.pdf"));

System.out.println("Signature valid: " + result.isValid());

Use cases:

  • Verify documents from external organizations without importing their keys
  • Continuous interoperability with partner PKI systems
  • One-time signature verification for untrusted sources

11.2 Encrypt for an External Recipient's Public Key

Encrypt a file for an external recipient using their public key, without storing it in your keystore.

import co.ankatech.ankasecure.sdk.model.EncryptResult;
import java.nio.file.Path;

EncryptResult result = sdk.encryptFileUtilityStream(
    "ML-KEM", "ML-KEM-1024",
    recipientPublicKeyB64,
    Path.of("data.bin"),
    Path.of("data.jwe"));

System.out.println("File encrypted for external recipient");

Use cases:

  • Encrypt files for external partners without storing their keys
  • Continuous secure data exchange with external systems
  • Temporary encryption for one-time recipients

Key distinction:

  • Utility operations: caller-supplied public key, nothing stored, no grant
  • Standard operations: operate on a provisioned kid and require a capability grant

12. PKCS#7 / CMS Interop & Migration

These data-plane operations analyze legacy PKCS#7/CMS artifacts and convert them to modern JOSE (JWE/JWS). The SDK does not import keystores. When a conversion needs to decrypt EnvelopedData, the recipient's private key must have been imported in the control plane first (Admin Console Import Keystore tab, or the admin-api migration import surface). A conversion is authorized by the re-operation grants, never by DECRYPT, ENCRYPT or SIGN: the actor needs REENCRYPT from the recipient that opens the content to every recipient, and RESIGN from every signer's key to itself. A REENCRYPT or RESIGN grant is requested with exactly two assetKids, so a file with N recipients needs N-1 pair grants [opening recipient, recipient] (any of them also covers the opener's own pair); a single-recipient file needs the in-place grant, requested with the kid twice (assetKids: [kid, kid]) and stored as [kid], and each signer needs RESIGN [signer, signer]. There is no wildcard REENCRYPT or RESIGN grant. A caller without them is refused 403.

12.1 Legacy Keystore Import (Control Plane)

Importing a legacy JKS/PKCS#12 keystore is a control-plane action, not an SDK call. Use the Admin Console Import Keystore tab (see Key Management*) or the Admin API operation it performs, POST /api/v3/admin/tenants/{tenantId}/keys/import-keystore. Import produces one kid per alias (<prefix><alias>); resolve each alias's purpose when its KeyUsage is ambiguous, or the import returns 422 purpose-required. The resulting kid is then usable by the SDK as a conversion recipient.

12.2 Analyze PKCS#7 Structure

Introspect PKCS#7/CMS files to understand their structure before conversion. Analysis is purely structural: it decrypts nothing and validates no certificate path or timestamp, so it takes no key and no option.

import co.ankatech.secure.client.model.Pkcs7AnalysisRequest;
import co.ankatech.secure.client.model.Pkcs7AnalysisResponse;
import java.io.File;
import java.nio.file.Files;
import java.util.Base64;

// Compact: the Base64 file is the only request member
Pkcs7AnalysisRequest request = new Pkcs7AnalysisRequest()
    .pkcs7Data(Base64.getEncoder().encodeToString(
        Files.readAllBytes(new File("legacy_signed.p7s").toPath())));
Pkcs7AnalysisResponse response = sdk.analyzePkcs7(request);

// Streaming: the file only, with no size ceiling
Pkcs7AnalysisResponse streamed = sdk.analyzePkcs7Stream(new File("legacy_archive.p7m"));

System.out.println("Structure: " + response.getDetectedFormat().getStructure());
System.out.println("Standard: " + response.getDetectedFormat().getStandard());
System.out.println("Actor bound: " + response.getLimits().getMaxActors());

if (response.getSigners() != null) {
    response.getSigners().forEach(signer -> {
        System.out.println("  - " + signer.getIssuerDN());
        System.out.println("    Algorithm: " + signer.getSignatureAlgorithm());
    });
}

if (Boolean.TRUE.equals(response.getSupportStatus().getSupported())) {
    System.out.println("Migration path: " + response.getRecommendations().getMigrationPath());
} else {
    System.out.println("Not convertible: " + response.getSupportStatus().getReasons());
}

getLimits().getMaxActors() is the most signers and recipients, counted together, one conversion accepts; a file above it is reported unsupported with ACTOR_LIMIT_EXCEEDED.

Supported structures:

  • SignedData (signed documents)
  • EnvelopedData (encrypted data)
  • SignedAndEnvelopedData (combined signature + encryption)

12.3 Convert PKCS#7 to JOSE Format

Convert legacy PKCS#7/CMS files to modern JOSE format (JWS/JWE). Every signer and every recipient is carried: N signers become N JWS signatures and M recipients M JWE recipient entries, in encoded order. The complete, runnable example is Flow 20 (ExampleScenario20); the calls below are the ones it makes.

import co.ankatech.secure.client.model.Pkcs7ConversionRequest;
import co.ankatech.secure.client.model.Pkcs7ConversionResponse;
import co.ankatech.secure.client.model.Pkcs7ConversionStreamRequest;
import java.io.File;

// Compact: the Base64 file in the request body (up to the migration upload limit, default 5 MB)
Pkcs7ConversionRequest request = new Pkcs7ConversionRequest()
    .pkcs7Data(pkcs7Base64)
    .decryptionKid("legacy-recipient-key")   // optional; see below
    .serialization(Pkcs7ConversionRequest.SerializationEnum.AUTO)
    .targetFormat(Pkcs7ConversionRequest.TargetFormatEnum.AUTO);
Pkcs7ConversionResponse response = sdk.convertPkcs7ToJose(request);

System.out.println("Structure     : " + response.getStructure());       // JWS, JWE or JWE_JWS_NESTED
System.out.println("Serialization : " + response.getSerialization());   // COMPACT or JSON
System.out.println("Decision      : " + response.getDecision().getReason());
String joseToken = response.getJoseToken();

if (response.getSigners() != null) {
    response.getSigners().forEach(signer -> System.out.println(
        "  " + signer.getOriginalIssuerDN() + " -> " + signer.getAssignedKid() + " (" + signer.getAlgorithm() + ")"));
}

// Streaming: file in, detached JOSE out, no size ceiling
Pkcs7ConversionStreamRequest streamRequest = new Pkcs7ConversionStreamRequest()
    .decryptionKid("legacy-recipient-key")
    .serialization(Pkcs7ConversionStreamRequest.SerializationEnum.AUTO)
    .targetFormat(Pkcs7ConversionStreamRequest.TargetFormatEnum.AUTO);
File output = sdk.convertPkcs7ToJoseStream(streamRequest,
    new File("legacy_encrypted.p7m"), new File("modern_encrypted.jose"));

serialization (exact case): AUTO (default) gives the single-actor form for at most one signer and at most one recipient and General JSON otherwise; COMPACT forces the single-actor form and a file with more than one signer or recipient is refused 422 INCOMPATIBLE_SERIALIZATION; JSON always gives General JSON. On the compact call the single-actor form is the Compact serialization; on the streaming call the output is detached and JSON-serialized by construction.

decryptionKid is optional. For an encrypted structure it names the recipient key that decrypts and must match one of the recipients (400 otherwise); when it is absent, the first recipient in encoded order whose tenant key admits decryption and encryption decrypts. It is ignored for SignedData.

JWS payload: the compact call's JWS payload is a claims set carrying the content base64url-encoded in data (with tid and iat); the streaming call's JWS signs the raw content. See PKCS#7 Conversion.

Prerequisites:

  • The tenant holds a key for every signer and recipient, imported in the control plane (Admin Console Import Keystore / migration import) and matched by the issuer DN and serial number of its certificate, always with its private key: a key admissible to sign for each signer, to decrypt and encrypt for the recipient that decrypts, and to encrypt for every other recipient (a keystore import with its private key, or the encryption key of a DUAL import)
  • Current limitation: a certificate imported alone is a verify-only trust anchor, and a key imported restricted (an expired certificate or a failed chain under IMPORT_ONLY) is read-only; neither is admissible as a signer or a recipient
  • A signer or recipient without an admissible key is refused 422 missing-private-key, and extensions.unresolvedActors[] names every one
  • At most 16 signers and recipients together per conversion; analyzePkcs7 reports the bound in limits.maxActors

Next steps for PQC:

After conversion to JOSE, use standard SDK methods to upgrade to post-quantum. A result with several signatures or several recipients is opened, verified, re-signed and re-encrypted like a single-actor one: name the recipient that decrypts in recipientKid, and the ONE signer or recipient being rotated in sourceKid (see Re-encrypt & Re-sign). The PQ signing and encryption keys are provisioned in the control plane first; the SDK then re-signs and re-encrypts against them:

// Re-sign the converted JWS onto a pre-provisioned PQ signing key (RESIGN grant on both kids)
ResignResult resignResult = sdk.resign("pq-signing-key", oldJws);

// Re-encrypt the converted JWE onto a pre-provisioned PQ encryption key (REENCRYPT grant on both kids)
ReencryptResult reencResult = sdk.reencrypt("pq-encryption-key", oldJwe);

Key distinction:

  • PKCS#7 conversion: the recipient key is imported in the control plane; the SDK performs the conversion
  • Utility operations: caller-supplied public key, nothing stored, no grant

13. Next Steps

Security Best Practices

Before deploying to production, review security recommendations:

Integration Examples

Explore real-world integration scenarios:

  • Integration Examples: Common integration patterns
  • Integration Flows: 21 detailed flows covering all SDK operations
    • Flow 1-4: Streaming encrypt/decrypt, sign/verify, re-encrypt
    • Flow 5-12: Non-streaming (compact JWE/JWS) and re-sign
    • Flow 13-17: Algorithm discovery, in-memory quick-start, token rotation, detached-JWS stream verification, sign-then-encrypt
    • Flow 18: Composite hybrid keys (quantum-resistant defence-in-depth)
    • Flow 19-21: PKCS#7/CMS analysis, PKCS#7 to JOSE migration, streaming sign-then-encrypt

Technical Documentation

For deeper understanding of the SDK architecture:

  • SDK & CLI Overview*: Architectural overview
  • OpenAPI Client: Underlying REST client implementation
  • Crypto CLI: Command-line tool built on the SDK (reference implementation)

API Reference

For complete API documentation:

  • Core API Endpoints*: REST API reference
  • Streaming Operations*: Streaming API details
  • Algorithm Catalog: 89 simple algorithm profiles and 41 composite pairings

© 2025 AnkaTech. All rights reserved.

* restricted content — see higher-tier documentation