Antrapol JCE Provider
A drop-in Java Security Provider that adds post-quantum cryptography and
policy-driven crypto-agility to code you have already written. No rewrite required.
It registers under the name "Antrapol" and keeps the ordinary JCE APIs you
already use: Signature, KeyPairGenerator, KeyFactory,
KeyStore, MessageDigest, Cipher.
Your call sites do not change. What changes is which algorithm runs and how the bytes are represented. You govern both 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), plus Malaysia's sovereign KAZ-SIGN.
- 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).
It then packages the result in a self-describing agility envelope. This is the mechanism that lets you migrate from classical to post-quantum crypto without touching application code.
On KAZ-SIGN
KAZ-SIGN is an optional, policy-selectable sovereign (Malaysian) signature algorithm offered 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 are
loaded, removed ones are unloaded.
Core concepts
Crypto-agility
Callers request an operation without committing to one algorithm at the call site. The choice comes from the caller or from policy.
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 policy resolver keys off the purpose. The purpose also 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, such as
profile.getSignature()orprofile.getKeyPairGenerator(), already bound to"Antrapol"with the policy's curve and size applied. - Baseline hygiene is enforced. Weak signature digests
(
MD5,SHA-1,SHA-224,SHA-256) are upgraded to SHA-384, and 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 or 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 do not have to remember which algorithm produced a blob.Signature.getInstance("Antrapol", "Antrapol") reads it straight
from the envelope and verifies, with 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): wraps outputs in the envelope and 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). Signatures can
override it per-operation via AntrapolSignatureParameterSpec.outputMode(...).
Enforcement mode (EnforcementMode) governs caller-vs-policy conflicts:
OFF: the caller value is used and conflicts do not fail.WARN(default): the caller value is used and 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. You 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 protects 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) derives 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.