Architecture playbook: crypto-agility for the post-quantum era. Learn more

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
AntrapolBootstrap.java
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 call Antrapol.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 (default false)

The purpose is what the policy resolver keys off, and it drives output-mode resolution.

Bind context safely

Use ContextBinder.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:

  1. An algorithm the caller explicitly requested wins (subject to enforcement).
  2. Otherwise, the policy-resolved algorithm is used.
  3. If neither is present, a GeneralSecurityException ("Unable to resolve algorithm") is thrown.
PolicyWiring.java
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() or profile.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 a GeneralSecurityException with 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.

SignWithMlDsa.java
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:

PolicyDrivenSign.java
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.

MlKemWrap.java
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.

MlKemEncapsulate.java
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.