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) and Malaysia's KAZ-SIGN.
- Algorithm choice can come from policy instead of being hard-coded.
- Outputs are self-describing. 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 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. Declare at least 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) such as 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 instead of 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. That holds 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). They carry 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 such as"protect mykad"or"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(). Manual calls can leak context across threads.
Policy-driven resolution
Register a PolicyResolver. It is 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(). It comes 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. RSA key generation below 1024 bits is rejected. PQC algorithms (ML-DSA · SLH-DSA · KAZ-SIGN) skip the digest upgrade.
The agility envelope
Antrapol does not return a bare signature or ciphertext or digest or key. It 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 or plaintext or IV / AAD / auth-tag or symmetric key.
Encoding and decoding are automatic. Callers still see ordinary JCE objects.
Self-describing = auto-verify
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. No algorithm name is 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. The provider then behaves like a plain delegate.
Mode is resolved per purpose (a per-purpose override or 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 rather than fatal. STRICT: a conflict throws aGeneralSecurityExceptionwith the caller and 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. The
auto-verifier then 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 Malaysia's KAZ-SIGN-128/192/256. Sign purely by business purpose
instead by binding
a context and using 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). It comes 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 and KEM and digest and cipher. Or talk to us about an enterprise integration.