Skip to content
ecdsa.com
Java · Kotlin

Java JCA (SunEC, JDK 25)

The Java Cryptography Architecture with the JDK's SunEC provider, as in JDK 25: DER by default with an in-P1363-format variant, random nonces, only the three NIST curves, plus Ed25519, Ed448, ML-KEM and ML-DSA.
License
GPL-2.0-only WITH Classpath-exception-2.0
Checked version
25.0.4.1 ·
Source
github.com/openjdk/jdk
Documentation
docs.oracle.com/en/java/javase/25/security/java-cryptography-architecture-jca-reference-guide.html

Compatibility at a glance

ECDSAEdDSAPost-quantum

ECDSA curves
P-256P-384P-521
EdDSA
Ed25519Ed448
Private key formats
pkcs8-derraw-scalar

Java code signs through java.security.Signature, KeyPairGenerator and KeyFactory, and the JDK's own providers do the work: SunEC for elliptic curves and EdDSA, plus the newer ML-KEM (javax.crypto.KEM, JDK 24) and ML-DSA providers. Android, Kotlin and every JVM framework sit on the same API, so its defaults surface in Spring, Keycloak and countless SDKs.

The algorithm name decides the encoding. SHA256withECDSA produces and expects ASN.1 DER; SHA256withECDSAinP1363Format uses the 64-byte raw r‖s that JWS and WebCrypto need, as in the sample. Mixing them up behaves asymmetrically: a raw signature given to SHA256withECDSA throws SignatureException, while a DER signature given to the P1363 variant just returns false. NONEwithECDSA signs a digest you computed — its signature verified with SHA256withECDSA against the message — and the hash name is independent of the curve, so SHA256withECDSA on a P-384 key is legal and fails against peers that expect SHA-384.

SunEC in JDK 25 offers only secp256r1, secp384r1 and secp521r1 for ECDSA: P-224, secp256k1 and the Brainpool curves raised errors, which is why Bitcoin and Ethereum code on the JVM uses Bouncy Castle or web3j. Nonces are random, s is not normalized and verification accepts both halves, so signatures for strict low-S peers need s replaced by n − s.

Keys come in as X.509 SPKI DER (X509EncodedKeySpec), PKCS#8 DER (PKCS8EncodedKeySpec) or raw components (ECPrivateKeySpec). PEM support arrived as a preview API in JDK 25 (java.security.PEMDecoder, JEP 470), which decoded a public key in the check only with --enable-preview; production code still strips the armour and base64-decodes. Post-quantum support is real: ML-KEM-512/768/1024 encapsulated and ML-DSA-44/65/87 signed and verified; SLH-DSA is not in this JDK.

Compare signature encodings with the DER ⇄ raw converter, and inspect signed JARs with the JAR signature inspector.

Facts and the checks behind them

Nothing below is copied from documentation. The check content/libraries/verify/java-jca/run.sh ran Java JCA (SunEC, JDK 25) 25.0.4.1 on Eclipse Adoptium 25.0.4.1+1-LTS in the container image maven:3.9.16-eclipse-temurin-25 on October 20, 2026; the page must match what it printed, and each fact lists those lines.

  • ECDSA signature encoding

    der

    SHA256withECDSA returns ASN.1 DER; SHA256withECDSAinP1363Format returns 64 bytes of raw r‖s.

    Recorded by the check
    fact ecdsa-signature-encoding = der
    evidence ecdsa-signature-encoding: SHA256withECDSAinP1363Format output is 64 bytes
  • ECDSA nonce

    random

    Random nonce: the RFC 6979 test key signed the same message twice with different results.

    Recorded by the check
    fact ecdsa-nonce = random
  • Low-S when signing

    either

    s is not normalized: high-S values appeared among 64 signatures.

    Recorded by the check
    fact ecdsa-low-s-sign = either
  • High-S when verifying

    accepts-high-s

    A DER signature with s replaced by n − s verified.

    Recorded by the check
    fact ecdsa-low-s-verify = accepts-high-s
  • What the signing call hashes

    hashes-message-or-digest

    SHA256withECDSA hashes the data; NONEwithECDSA signs a precomputed digest, and that signature verified with SHA256withECDSA against the message.

    Recorded by the check
    fact ecdsa-prehash = hashes-message-or-digest
  • Hash bound to the curve

    none

    The hash in the algorithm name is not tied to the curve: SHA256withECDSA signed and verified with a P-384 key.

    Recorded by the check
    fact ecdsa-hash-curve-binding = none
  • Malformed ECDSA signature

    throws

    A raw signature passed to SHA256withECDSA and a 3-byte blob throw SignatureException; a DER signature passed to the P1363 variant returns false.

    Recorded by the check
    fact ecdsa-malformed-signature = throws
    evidence ecdsa-malformed-signature: raw r||s to SHA256withECDSA: throws SignatureException; DER to the P1363 algorithm: false; 3 bytes: throws SignatureException
  • ECDSA curves

    P-256P-384P-521

    secp256r1, secp384r1 and secp521r1; secp224r1, secp256k1 and brainpoolP256r1 were not available.

    Recorded by the check
    fact ecdsa-curves = P-256, P-384, P-521
    evidence ecdsa-curves: P-224 (secp224r1) not available
    evidence ecdsa-curves: secp256k1 (secp256k1) not available
    evidence ecdsa-curves: brainpoolP256r1 (brainpoolP256r1) not available
  • EdDSA

    Ed25519Ed448

    Ed25519 and Ed448 through KeyPairGenerator and Signature with those names.

    Recorded by the check
    fact eddsa = Ed25519, Ed448
  • Post-quantum algorithms

    ML-KEMML-DSA

    ML-KEM-512/768/1024 through javax.crypto.KEM and ML-DSA-44/65/87 through Signature worked; there is no SLH-DSA KeyPairGenerator.

    Recorded by the check
    fact pqc = ML-KEM, ML-DSA
    evidence pqc: SLH-DSA KeyPairGenerator available: false
  • Public key formats

    spki-der

    Public keys load from X.509 SPKI DER; getEncoded() returns the same form.

    Recorded by the check
    fact public-key-formats = spki-der
    evidence public-key-formats: getEncoded() of 256-bit key returns X.509 SPKI DER; format X.509
  • Private key formats

    pkcs8-derraw-scalar

    Private keys load from PKCS#8 DER and from the raw scalar with ECPrivateKeySpec.

    Recorded by the check
    fact private-key-formats = pkcs8-der, raw-scalar
  • PEM API

    preview

    java.security.PEMDecoder exists as a preview API (JEP 470) and decoded a PEM public key only with --enable-preview.

    Recorded by the check
    fact pem-api = preview
    evidence pem-api: java.security.PEMDecoder present: true (JEP 470, preview in JDK 25; needs --enable-preview)
    evidence pem-api: PEMDecoder.of().decode(PEM text) with --enable-preview: true

Usage

Sign and verify in the raw r‖s format that JWS and WebCrypto use, by choosing the in-P1363-format algorithm name.
import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;

public class Usage {
    public static void main(String[] args) throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
        generator.initialize(new ECGenParameterSpec("secp256r1"));
        KeyPair pair = generator.generateKeyPair();
        byte[] message = "order #1042 approved".getBytes(StandardCharsets.UTF_8);

        // The algorithm name picks the hash AND the encoding:
        // SHA256withECDSA = DER, SHA256withECDSAinP1363Format = raw r||s (JWS, WebCrypto).
        Signature signer = Signature.getInstance("SHA256withECDSAinP1363Format");
        signer.initSign(pair.getPrivate());
        signer.update(message);
        byte[] raw = signer.sign();

        Signature verifier = Signature.getInstance("SHA256withECDSAinP1363Format");
        verifier.initVerify(pair.getPublic());
        verifier.update(message);
        System.out.println("raw bytes: " + raw.length + ", verified: " + verifier.verify(raw));
    }
}

The snippet harness runs every sample verbatim in the container image maven:3.9.16-eclipse-temurin-25, and the run fails unless the output matches.

Command
java Usage.java
Output must match (regular expression)
raw bytes: 64, verified: true

Libraries