Using the AnkaSecure SDK in Java
SDK Version: 3.4.0
Java Version: 17+
Artifact:ankasecure-sdk-3.4.0.jar(co.ankatech:ankasecure-sdkon 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:
1.1 Maven Dependency (Recommended)
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:
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:
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);
}
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:
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
- Never hardcode secrets - Use environment variables:
${ENV_VAR_NAME} - Start conservative - Begin with small pool sizes, increase if needed
- Monitor errors -
StreamResetException: CANCEL→ increase pool size - Tune timeouts - ML-DSA-87, RSA-8192, FRODO take 5-20 seconds each
- 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:
AnkaSecureSdkis the factory, not the operational SDK - Return value: Authentication methods return
AuthenticatedSdk(not void) - Thread-safety:
AuthenticatedSdkis 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 fromisTokenExpired()) checks only the localexpclaim and does not detect server-side revocation; a revoked token surfaces as a typedTokenRevokedExceptionon the next operation. See Token Revocation Architecture* and the SDK security best practices
Authentication methods:
- Application authentication (recommended): Use the
clientIdandclientSecretissued 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()andgetDeletedAt()(when the key was destroyed). - It reports no shape, material or usage:
getKty(),getAlg(),getCompositeMode(),getKdf(),getExportable(),getOrigin(),getRestricted(),getExpiresAt(),getSoftLimitExpiration(),getUsageCount(),getSoftUsageLimit()andgetMaxUsageLimit()arenull, andgetKeyOps()is an empty list. None of them is defaulted. - Because no shape is reported,
isComposite(),isCompositeKem()andisCompositeSignature()answerfalseon such a row whatever the destroyed key was. ReadgetStatus()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()isnullon 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
DUALimport) - 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, andextensions.unresolvedActors[]names every one - At most 16 signers and recipients together per conversion;
analyzePkcs7reports the bound inlimits.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:
- Security Best Practices: Credential encryption, token lifecycle, secure integration patterns
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.