Antrapol JCE Provider
A drop-in Java Security Provider that adds post-quantum cryptography and
policy-driven crypto-agility to code you've already written — with no rewrite.
It registers under the name "Antrapol" and keeps the ordinary JCE APIs you
know — Signature, KeyPairGenerator, KeyFactory,
KeyStore, MessageDigest, Cipher. Your call sites
don't change. What changes is which algorithm runs and how the bytes are
represented — both become things you govern from policy.
What you get:
- Standard JCE call sites keep working — you just pass the
"Antrapol"provider. - Post-quantum algorithms available out of the box: ML-DSA (FIPS 204), ML-KEM (FIPS 203), SLH-DSA (FIPS 205).
- Algorithm choice can come from policy instead of being hard-coded.
- Outputs are self-describing, so data signed or encrypted today still verifies after you switch algorithms tomorrow.
Antrapol never implements raw crypto itself. It delegates execution to
underlying providers (BouncyCastle, the KAZ provider, SunRsaSign,
SunEC, SunJCE) and packages the result in a
self-describing "agility envelope" — the mechanism that makes a classical → post-quantum
migration possible without touching application code.
On KAZ-SIGN
KAZ-SIGN is offered as an optional, policy-selectable sovereign (Malaysian) signature algorithm alongside the NIST standards — it is not a NIST-standardized algorithm. Agility means you can select or replace it at any time.Install & configure
Call Antrapol.install() once at startup, then hand it an immutable
AntrapolConfig. At minimum, declare one delegate provider
(BouncyCastle is typical) and choose a default output mode.
Prerequisites:
- A Java 17 toolchain
jciphagile-1.0.0.jar(the envelope library)- Your delegate provider JAR(s), e.g. BouncyCastle
import com.antrapol.jce.Antrapol;
import com.antrapol.jce.config.AntrapolConfig;
import com.antrapol.jce.config.DelegateProviderConfig;
import com.antrapol.jce.config.EnforcementMode;
import com.antrapol.jce.config.OutputMode;
import java.util.List;
// Register the "Antrapol" provider at position 1 (idempotent).
Antrapol.install();
Antrapol.configure(
AntrapolConfig.builder()
.delegateProviders(List.of(
DelegateProviderConfig.builder()
.providerName("BC")
.providerClassName("org.bouncycastle.jce.provider.BouncyCastleProvider")
.jarPaths(List.of("/opt/crypto/bcprov-jdk18on-1.78.1.jar"))
.build()
))
.defaultOutputMode(OutputMode.AGILITY) // wrap outputs in the agility envelope
.enforcementMode(EnforcementMode.WARN) // caller-vs-policy conflicts warn, not fail
.acceptRawInput(true) // decode paths tolerate non-envelope bytes
.build()
);
Ordering rule
Always callAntrapol.install() before requesting any JCE
instance with provider "Antrapol". You can call configure(...)
again later to publish a new config — delegate JARs are reconciled on each call (new ones
loaded, removed ones unloaded).
Core concepts
Crypto-agility
Callers request an operation without hard-committing to one algorithm at the call site. The choice can come from the caller or from policy, and every output carries enough metadata to be decoded and verified later — even after the default algorithm has changed.
Delegate providers
Antrapol wraps; it does not compute. Each delegate is declared as a
DelegateProviderConfig and loaded through an isolated classloader.
- Resolution order:
["BC", "KAZ", "SunRsaSign", "SunEC", "SunJCE"]—"Antrapol"is always skipped to avoid recursion. - Keys come back wrapped (
WrappedPublicKey/WrappedPrivateKey/WrappedSecretKey), carrying both the delegate key and the agility-encoded bytes. Unwrap first if you need the raw JCE key type.
Purpose & operation context
A PurposeContext is a small record with three fields:
purpose— a business label, e.g."protect mykad","txn.sign"operation— one of"signature","keypair_generation","digest","cipher","kem"includeSignaturePlaintext— a flag (defaultfalse)
The purpose is what the policy resolver keys off, and it drives output-mode resolution.
Bind context safely
UseContextBinder.withContext(...) to bind a context for a scope — it restores
the previous context in a finally block. Prefer it over manual
bind() / clear(), which can leak context across threads.
Policy-driven resolution
Register a PolicyResolver — a function that returns an
Optional<PolicyProfile> for a given PurposeContext. When an
operation runs, the effective algorithm is chosen in this order:
- An algorithm the caller explicitly requested wins (subject to enforcement).
- Otherwise, the policy-resolved algorithm is used.
- If neither is present, a
GeneralSecurityException("Unable to resolve algorithm") is thrown.
import com.antrapol.jce.Antrapol;
import com.antrapol.jce.policy.profile.SignaturePolicyProfile;
import java.util.Map;
import java.util.Optional;
Antrapol.configurePolicyResolver(ctx -> {
if ("signature".equals(ctx.operation())) {
return Optional.of(new SignaturePolicyProfile(
"SHA384withECDSA", // signatureAlgorithm
"SHA-384", // digestAlgorithm
"EC", // keyAlgorithm
"secp256r1", // curve
0, // keySize (unused for EC)
Map.of("curve", "secp256r1")
));
}
return Optional.empty();
});
Two things happen for free once policy is wired:
- Typed profiles are factories. A resolved profile can mint a ready-to-use
engine — e.g.
profile.getSignature()orprofile.getKeyPairGenerator()— already bound to"Antrapol"with the policy's curve/size applied. - Baseline hygiene is enforced. Weak signature digests
(
MD5,SHA-1,SHA-224,SHA-256) are upgraded to SHA-384; RSA key generation below 1024 bits is rejected. PQC algorithms (ML-DSA, SLH-DSA, KAZ-SIGN) skip the digest upgrade.
The agility envelope
Rather than returning a bare signature, ciphertext, digest, or key, Antrapol wraps the bytes in a ciphagile agility envelope. The envelope carries:
- the operation tag and the algorithm / variant name;
- optional extras — an embedded public key, plaintext, IV / AAD / auth-tag, or symmetric key.
Encoding and decoding are automatic, so callers still see ordinary JCE objects.
Self-describing = auto-verify
Because the envelope names its own algorithm, you don't have to remember which algorithm produced a blob.Signature.getInstance("Antrapol", "Antrapol") reads it straight
from the envelope and verifies — no algorithm name needed. A signature can even embed the
signer's public key for standalone verification.
Output & enforcement modes
Output mode (OutputMode) has two values:
AGILITY(default) — wrap outputs in the envelope; enables transparent decode and auto-verify.RAW— no packaging; bytes pass through, so the provider behaves like a plain delegate.
Mode is resolved per purpose (a per-purpose override, else the default), and signatures can
override it per-operation via AntrapolSignatureParameterSpec.outputMode(...).
Enforcement mode (EnforcementMode) governs caller-vs-policy conflicts:
OFF— caller value is used; conflicts don't fail.WARN(default) — caller value is used; the conflict is logged, not fatal.STRICT— a conflict throws aGeneralSecurityExceptionwith the caller, policy, and purpose details.
Guide: sign with ML-DSA
Signing looks like ordinary JCE — just pass "Antrapol". Here an
ML-DSA-65 key pair signs a message into an AGILITY envelope, and the
auto-verifier reads the algorithm back out of the envelope to verify it.
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
// 1. Generate an ML-DSA-65 key pair (strength-based init).
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-DSA", "Antrapol");
kpg.initialize(65); // 44 | 65 | 87
KeyPair kp = kpg.generateKeyPair();
// 2. Sign — result is an AGILITY envelope by default.
Signature signer = Signature.getInstance("ML-DSA-65", "Antrapol");
signer.initSign(kp.getPrivate());
signer.update(message);
byte[] sig = signer.sign();
// 3. Verify with the auto ("Antrapol") signer — algorithm read from the envelope.
Signature verifier = Signature.getInstance("Antrapol", "Antrapol");
verifier.initVerify(kp.getPublic());
verifier.update(message);
boolean ok = verifier.verify(sig);
The same shape works for SLH-DSA-128/192/256 and the sovereign
KAZ-SIGN-128/192/256. To sign purely by business purpose, bind a context
and use the virtual "Antrapol" signer — policy picks the algorithm:
import com.antrapol.jce.context.ContextBinder;
import com.antrapol.jce.context.PurposeContext;
import java.security.Signature;
byte[] sig = ContextBinder.withContext(
new PurposeContext("protect mykad", "signature"),
() -> {
Signature s = Signature.getInstance("Antrapol", "Antrapol");
s.initSign(privateKey); // algorithm chosen by policy for this purpose
s.update(message);
return s.sign();
}
);
Guide: encapsulate with ML-KEM
ML-KEM (FIPS 203) is exposed through
Cipher.getInstance("ML-KEM", "Antrapol") at strengths 512 / 768 / 1024
(default 768), in two conventions.
WRAP / UNWRAP — protect a session key: encapsulate to the recipient's public key, then decapsulate with the private key.
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import javax.crypto.Cipher;
KeyPairGenerator kpg = KeyPairGenerator.getInstance("ML-KEM", "Antrapol");
kpg.initialize(768); // 512 | 768 | 1024
KeyPair kp = kpg.generateKeyPair();
// Encapsulate: wrap a session key to the recipient's public key.
Cipher wrap = Cipher.getInstance("ML-KEM", "Antrapol");
wrap.init(Cipher.WRAP_MODE, kp.getPublic());
byte[] wrapped = wrap.wrap(sessionKey);
// Decapsulate: recover the session key with the private key.
Cipher unwrap = Cipher.getInstance("ML-KEM", "Antrapol");
unwrap.init(Cipher.UNWRAP_MODE, kp.getPrivate());
java.security.Key recovered = unwrap.unwrap(wrapped, "AES", Cipher.SECRET_KEY);
ENCRYPT / DECRYPT (KEM-only) — derive a fresh shared secret directly. Encapsulation returns a KEM envelope; decapsulation returns the shared-secret bytes.
import javax.crypto.Cipher;
// Encapsulate: derive + package a fresh shared secret for the public key.
Cipher encaps = Cipher.getInstance("ML-KEM", "Antrapol");
encaps.init(Cipher.ENCRYPT_MODE, kp.getPublic());
byte[] kemEnvelope = encaps.doFinal(); // AsymkeyCipherContext, tag 0x0B
// Decapsulate: recover the shared secret with the private key.
Cipher decaps = Cipher.getInstance("ML-KEM", "Antrapol");
decaps.init(Cipher.DECRYPT_MODE, kp.getPrivate());
byte[] sharedSecret = decaps.doFinal(kemEnvelope);
Next steps: browse the full algorithm support matrix for every registered signature, KEM, digest, and cipher — or talk to us about an enterprise integration.