8347938: Add Support for the Latest ML-KEM and ML-DSA Private Key Encodings

Reviewed-by: mullan, bperez, mpowers
This commit is contained in:
Weijun Wang 2026-02-03 16:32:21 +00:00
parent 99bc98357d
commit e51ccef9cb
22 changed files with 1491 additions and 289 deletions

View File

@ -498,7 +498,7 @@ public final class ML_KEM {
/*
Main internal algorithms from Section 6 of specification
*/
protected ML_KEM_KeyPair generateKemKeyPair(byte[] kem_d, byte[] kem_z) {
protected ML_KEM_KeyPair generateKemKeyPair(byte[] kem_d_z) {
MessageDigest mlKemH;
try {
mlKemH = MessageDigest.getInstance(HASH_H_NAME);
@ -508,7 +508,8 @@ public final class ML_KEM {
}
//Generate K-PKE keys
var kPkeKeyPair = generateK_PkeKeyPair(kem_d);
//The 1st 32-byte `d` is used in K-PKE key pair generation
var kPkeKeyPair = generateK_PkeKeyPair(kem_d_z);
//encaps key = kPke encryption key
byte[] encapsKey = kPkeKeyPair.publicKey.keyBytes;
@ -527,7 +528,8 @@ public final class ML_KEM {
// This should never happen.
throw new RuntimeException(e);
}
System.arraycopy(kem_z, 0, decapsKey,
// The 2nd 32-byte `z` is copied into decapsKey
System.arraycopy(kem_d_z, 32, decapsKey,
kPkePrivateKey.length + encapsKey.length + 32, 32);
return new ML_KEM_KeyPair(
@ -535,6 +537,12 @@ public final class ML_KEM {
new ML_KEM_DecapsulationKey(decapsKey));
}
public byte[] privKeyToPubKey(byte[] decapsKey) {
int pkLen = (mlKem_k * ML_KEM_N * 12) / 8 + 32 /* rho */;
int skLen = (mlKem_k * ML_KEM_N * 12) / 8;
return Arrays.copyOfRange(decapsKey, skLen, skLen + pkLen);
}
protected ML_KEM_EncapsulateResult encapsulate(
ML_KEM_EncapsulationKey encapsulationKey, byte[] randomMessage) {
MessageDigest mlKemH;
@ -648,10 +656,12 @@ public final class ML_KEM {
throw new RuntimeException(e);
}
mlKemG.update(seed);
// Note: only the 1st 32-byte in the seed is used
mlKemG.update(seed, 0, 32);
mlKemG.update((byte)mlKem_k);
var rhoSigma = mlKemG.digest();
mlKemG.reset();
var rho = Arrays.copyOfRange(rhoSigma, 0, 32);
var sigma = Arrays.copyOfRange(rhoSigma, 32, 64);
Arrays.fill(rhoSigma, (byte)0);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -26,9 +26,12 @@
package com.sun.crypto.provider;
import sun.security.jca.JCAUtil;
import sun.security.pkcs.NamedPKCS8Key;
import sun.security.provider.NamedKEM;
import sun.security.provider.NamedKeyFactory;
import sun.security.provider.NamedKeyPairGenerator;
import sun.security.util.KeyChoices;
import sun.security.x509.NamedX509Key;
import java.security.*;
import java.util.Arrays;
@ -37,6 +40,20 @@ import javax.crypto.DecapsulateException;
public final class ML_KEM_Impls {
private static final int SEED_LEN = 64;
public static byte[] seedToExpanded(String pname, byte[] seed) {
return new ML_KEM(pname).generateKemKeyPair(seed)
.decapsulationKey()
.keyBytes();
}
public static NamedX509Key privKeyToPubKey(NamedPKCS8Key npk) {
return new NamedX509Key(npk.getAlgorithm(),
npk.getParams().getName(),
new ML_KEM(npk.getParams().getName()).privKeyToPubKey(npk.getExpanded()));
}
public sealed static class KPG
extends NamedKeyPairGenerator permits KPG2, KPG3, KPG5 {
@ -50,25 +67,27 @@ public final class ML_KEM_Impls {
}
@Override
protected byte[][] implGenerateKeyPair(String name, SecureRandom random) {
byte[] seed = new byte[32];
protected byte[][] implGenerateKeyPair(String pname, SecureRandom random) {
byte[] seed = new byte[SEED_LEN];
var r = random != null ? random : JCAUtil.getDefSecureRandom();
r.nextBytes(seed);
byte[] z = new byte[32];
r.nextBytes(z);
ML_KEM mlKem = new ML_KEM(name);
ML_KEM mlKem = new ML_KEM(pname);
ML_KEM.ML_KEM_KeyPair kp;
kp = mlKem.generateKemKeyPair(seed);
var expanded = kp.decapsulationKey().keyBytes();
try {
kp = mlKem.generateKemKeyPair(seed, z);
return new byte[][]{
kp.encapsulationKey().keyBytes(),
KeyChoices.writeToChoice(
KeyChoices.getPreferred("mlkem"),
seed, expanded),
expanded
};
} finally {
Arrays.fill(seed, (byte)0);
Arrays.fill(z, (byte)0);
Arrays.fill(seed, (byte) 0);
}
return new byte[][] {
kp.encapsulationKey().keyBytes(),
kp.decapsulationKey().keyBytes()
};
}
}
@ -94,8 +113,39 @@ public final class ML_KEM_Impls {
public KF() {
super("ML-KEM", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024");
}
public KF(String name) {
super("ML-KEM", name);
public KF(String pname) {
super("ML-KEM", pname);
}
@Override
protected byte[] implExpand(String pname, byte[] input)
throws InvalidKeyException {
return KeyChoices.choiceToExpanded(pname, SEED_LEN, input,
ML_KEM_Impls::seedToExpanded);
}
@Override
protected Key engineTranslateKey(Key key) throws InvalidKeyException {
var nk = toNamedKey(key);
if (nk instanceof NamedPKCS8Key npk) {
var type = KeyChoices.getPreferred("mlkem");
if (KeyChoices.typeOfChoice(npk.getRawBytes()) != type) {
var encoding = KeyChoices.choiceToChoice(
type,
npk.getParams().getName(),
SEED_LEN, npk.getRawBytes(),
ML_KEM_Impls::seedToExpanded);
nk = NamedPKCS8Key.internalCreate(
npk.getAlgorithm(),
npk.getParams().getName(),
encoding,
npk.getExpanded().clone());
if (npk != key) { // npk is neither input or output
npk.destroy();
}
}
}
return nk;
}
}
@ -121,15 +171,15 @@ public final class ML_KEM_Impls {
private static final int SEED_SIZE = 32;
@Override
protected byte[][] implEncapsulate(String name, byte[] encapsulationKey,
protected byte[][] implEncapsulate(String pname, byte[] encapsulationKey,
Object ek, SecureRandom secureRandom) {
byte[] randomBytes = new byte[SEED_SIZE];
var r = secureRandom != null ? secureRandom : JCAUtil.getDefSecureRandom();
r.nextBytes(randomBytes);
ML_KEM mlKem = new ML_KEM(name);
ML_KEM.ML_KEM_EncapsulateResult mlKemEncapsulateResult = null;
ML_KEM mlKem = new ML_KEM(pname);
ML_KEM.ML_KEM_EncapsulateResult mlKemEncapsulateResult;
try {
mlKemEncapsulateResult = mlKem.encapsulate(
new ML_KEM.ML_KEM_EncapsulationKey(
@ -145,49 +195,49 @@ public final class ML_KEM_Impls {
}
@Override
protected byte[] implDecapsulate(String name, byte[] decapsulationKey,
protected byte[] implDecapsulate(String pname, byte[] decapsulationKey,
Object dk, byte[] cipherText)
throws DecapsulateException {
ML_KEM mlKem = new ML_KEM(name);
ML_KEM mlKem = new ML_KEM(pname);
var kpkeCipherText = new ML_KEM.K_PKE_CipherText(cipherText);
return mlKem.decapsulate(new ML_KEM.ML_KEM_DecapsulationKey(
decapsulationKey), kpkeCipherText);
}
@Override
protected int implSecretSize(String name) {
protected int implSecretSize(String pname) {
return ML_KEM.SECRET_SIZE;
}
@Override
protected int implEncapsulationSize(String name) {
ML_KEM mlKem = new ML_KEM(name);
protected int implEncapsulationSize(String pname) {
ML_KEM mlKem = new ML_KEM(pname);
return mlKem.getEncapsulationSize();
}
@Override
protected Object implCheckPublicKey(String name, byte[] pk)
protected Object implCheckPublicKey(String pname, byte[] pk)
throws InvalidKeyException {
ML_KEM mlKem = new ML_KEM(name);
ML_KEM mlKem = new ML_KEM(pname);
return mlKem.checkPublicKey(pk);
}
@Override
protected Object implCheckPrivateKey(String name, byte[] sk)
protected Object implCheckPrivateKey(String pname, byte[] sk)
throws InvalidKeyException {
ML_KEM mlKem = new ML_KEM(name);
ML_KEM mlKem = new ML_KEM(pname);
return mlKem.checkPrivateKey(sk);
}
public K() {
super("ML-KEM", "ML-KEM-512", "ML-KEM-768", "ML-KEM-1024");
super("ML-KEM", new KF());
}
public K(String name) {
super("ML-KEM", name);
public K(String pname) {
super("ML-KEM", new KF(pname));
}
}

View File

@ -25,11 +25,8 @@
package sun.security.pkcs;
import sun.security.util.DerInputStream;
import sun.security.util.DerValue;
import sun.security.x509.AlgorithmId;
import javax.security.auth.DestroyFailedException;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
@ -39,6 +36,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.ProviderException;
import java.security.spec.NamedParameterSpec;
import java.util.Arrays;
import java.util.Objects;
/// Represents a private key from an algorithm family that is specialized
/// with a named parameter set.
@ -50,6 +48,28 @@ import java.util.Arrays;
/// identifier in the PKCS #8 encoding of the key is always a single OID derived
/// from the parameter set name.
///
/// Besides the existing [PKCS8Key#privKeyMaterial] field, this class optionally
/// supports an expanded format stored in [#expanded]. While `privKeyMaterial`
/// always represents the format used for encoding, `expanded` is always used
/// in computations. The expanded format must be self-sufficient for
/// cryptographic computations without requiring the encoding format.
///
/// 1. If only `privKeyMaterial` is present, it's also the expanded format.
/// 2. If both `privKeyMaterial` and `expanded` are available, `privKeyMaterial`
/// is the encoding format, and `expanded` is the expanded format.
///
/// If the two formats are the same, only `privKeyMaterial` is included, and
/// `expanded` must be `null`. Some implementations might be tempted to put the
/// same value into `privKeyMaterial` and `expanded`. However, problems can
/// arise if they happen to be the same object. To avoid ambiguity, always set
/// `expanded` to `null`.
///
/// If the `expanded` field is required by the algorithm, it is either
/// [calculated from the PKCS #8 encoding][#NamedPKCS8Key(String, byte\[\], Expander)],
/// or [provided directly][#internalCreate(String, String, byte\[\], byte\[\])].
/// In the latter case, the caller must ensure the consistency of the `encoded`
/// and `expanded` arguments. For example, seed and expanded key must match.
///
/// @see sun.security.provider.NamedKeyPairGenerator
public final class NamedPKCS8Key extends PKCS8Key {
@Serial
@ -57,42 +77,64 @@ public final class NamedPKCS8Key extends PKCS8Key {
private final String fname;
private final transient NamedParameterSpec paramSpec;
private final byte[] rawBytes;
private final transient byte[] expanded;
private transient boolean destroyed = false;
/// Ctor from family name, parameter set name, raw key bytes.
/// Key bytes won't be cloned, caller must relinquish ownership
public NamedPKCS8Key(String fname, String pname, byte[] rawBytes) {
/// Creates a `NamedPKCS8Key` from raw components.
///
/// @param fname family name
/// @param pname parameter set name
/// @param encoded raw key bytes, not null
/// @param expanded expanded key format, can be `null`.
private NamedPKCS8Key(String fname, String pname, byte[] encoded, byte[] expanded) {
this.fname = fname;
this.paramSpec = new NamedParameterSpec(pname);
this.expanded = expanded;
this.privKeyMaterial = Objects.requireNonNull(encoded);
try {
this.algid = AlgorithmId.get(pname);
} catch (NoSuchAlgorithmException e) {
throw new ProviderException(e);
}
this.rawBytes = rawBytes;
DerValue val = new DerValue(DerValue.tag_OctetString, rawBytes);
try {
this.privKeyMaterial = val.toByteArray();
} finally {
val.clear();
}
}
/// Ctor from family name, and PKCS #8 bytes
public NamedPKCS8Key(String fname, byte[] encoded) throws InvalidKeyException {
/// Creates a `NamedPKCS8Key` from raw components.
///
/// `encoded` and `expanded` won't be cloned, caller must relinquish
/// ownership. This caller must ensure `encoded` and `expanded` match
/// each other and `encoded` is valid and internally-consistent.
///
/// @param fname family name
/// @param pname parameter set name
/// @param encoded raw key bytes, not null
/// @param expanded expanded key format, can be `null`.
public static NamedPKCS8Key internalCreate(String fname, String pname,
byte[] encoded, byte[] expanded) {
return new NamedPKCS8Key(fname, pname, encoded, expanded);
}
/// Creates a `NamedPKCS8Key` from family name and PKCS #8 encoding.
///
/// @param fname family name
/// @param encoded PKCS #8 encoding. It is copied so caller can modify
/// it after the method call.
/// @param expander a function that is able to calculate the expanded
/// format from the encoding format inside `encoded`. If it recognizes
/// the input already in expanded format, it must return `null`.
/// This argument must be `null` if the algorithm's expanded format
/// is always the same as its encoding format. Whatever the case, the
/// ownership of the result is fully granted to this object.
public NamedPKCS8Key(String fname, byte[] encoded, Expander expander)
throws InvalidKeyException {
super(encoded);
this.fname = fname;
try {
paramSpec = new NamedParameterSpec(algid.getName());
if (algid.getEncodedParams() != null) {
throw new InvalidKeyException("algorithm identifier has params");
}
rawBytes = new DerInputStream(privKeyMaterial).getOctetString();
} catch (IOException e) {
throw new InvalidKeyException("Cannot parse input", e);
this.expanded = expander == null
? null
: expander.expand(algid.getName(), this.privKeyMaterial);
paramSpec = new NamedParameterSpec(algid.getName());
if (algid.getEncodedParams() != null) {
throw new InvalidKeyException("algorithm identifier has params");
}
}
@ -104,9 +146,15 @@ public final class NamedPKCS8Key extends PKCS8Key {
}
/// Returns the reference to the internal key. Caller must not modify
/// the content or keep a reference.
/// the content or pass the reference to untrusted application code.
public byte[] getRawBytes() {
return rawBytes;
return privKeyMaterial;
}
/// Returns the reference to the key in expanded format. Caller must not
/// modify the content or pass the reference to untrusted application code.
public byte[] getExpanded() {
return expanded == null ? privKeyMaterial : expanded;
}
@Override
@ -127,9 +175,11 @@ public final class NamedPKCS8Key extends PKCS8Key {
}
@Override
public void destroy() throws DestroyFailedException {
Arrays.fill(rawBytes, (byte)0);
public void destroy() {
Arrays.fill(privKeyMaterial, (byte)0);
if (expanded != null) {
Arrays.fill(expanded, (byte)0);
}
if (encodedKey != null) {
Arrays.fill(encodedKey, (byte)0);
}
@ -140,4 +190,17 @@ public final class NamedPKCS8Key extends PKCS8Key {
public boolean isDestroyed() {
return destroyed;
}
/// Expands from encoding format to expanded format.
@FunctionalInterface
public interface Expander {
/// The expand method
///
/// @param pname parameter set name
/// @param input input encoding
/// @return the expanded key, `null` if `input` is already in expanded
/// @throws InvalidKeyException if `input` is invalid, for example,
/// wrong encoding, or internal inconsistency
byte[] expand(String pname, byte[] input) throws InvalidKeyException;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -568,6 +568,54 @@ public class ML_DSA {
return new ML_DSA_KeyPair(sk, pk);
}
private static int[][] deepClone(int[][] array) {
int[][] clone = new int[array.length][];
for (int i = 0; i < array.length; i++) {
clone[i] = array[i].clone();
}
return clone;
}
// This is similar to the generateKeyPairInternal method. Instead of
// generating from a seed, it uses stored fields inside the private key
// to calculate the public key. It performs several checks during the
// calculation to make sure the private key is a valid one. Otherwise,
// an IllegalArgumentException is thrown.
public ML_DSA_PublicKey privKeyToPubKey(ML_DSA_PrivateKey sk) {
// Sample A
int[][][] keygenA = generateA(sk.rho); //A is in NTT domain
// Compute t and tr
// make a copy of sk.s1 and modify it. Although we can also
// take it out of NTT domain later, it was modified for a while.
var s1 = deepClone(sk.s1);
mlDsaVectorNtt(s1); //s1 now in NTT domain
int[][] As1 = integerMatrixAlloc(mlDsa_k, ML_DSA_N);
matrixVectorPointwiseMultiply(As1, keygenA, s1);
mlDsaVectorInverseNtt(As1);
int[][] t = vectorAddPos(As1, sk.s2);
int[][] t0 = integerMatrixAlloc(mlDsa_k, ML_DSA_N);
int[][] t1 = integerMatrixAlloc(mlDsa_k, ML_DSA_N);
power2Round(t, t0, t1);
if (!Arrays.deepEquals(t0, sk.t0)) {
throw new IllegalArgumentException("t0 does not patch");
}
var crHash = new SHAKE256(TR_LEN);
ML_DSA_PublicKey pk = new ML_DSA_PublicKey(sk.rho, t1);
byte[] publicKeyBytes = pkEncode(pk);
crHash.update(publicKeyBytes);
byte[] tr = crHash.digest();
if (!Arrays.equals(tr, sk.tr)) {
throw new IllegalArgumentException("tr does not patch");
}
//Encode PK
return new ML_DSA_PublicKey(sk.rho, t1);
}
public ML_DSA_Signature signInternal(byte[] message, byte[] rnd, byte[] skBytes) {
//Decode private key and initialize hash function
ML_DSA_PrivateKey sk = skDecode(skBytes);

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -26,12 +26,35 @@
package sun.security.provider;
import sun.security.jca.JCAUtil;
import sun.security.pkcs.NamedPKCS8Key;
import sun.security.util.KeyChoices;
import sun.security.x509.NamedX509Key;
import java.security.*;
import java.security.SecureRandom;
import java.util.Arrays;
public class ML_DSA_Impls {
private static final int SEED_LEN = 32;
public static byte[] seedToExpanded(String pname, byte[] seed) {
var impl = new ML_DSA(name2int(pname));
var sk = impl.generateKeyPairInternal(seed).privateKey();
try {
return impl.skEncode(sk);
} finally {
sk.destroy();
}
}
public static NamedX509Key privKeyToPubKey(NamedPKCS8Key npk) {
var dsa = new ML_DSA(name2int(npk.getParams().getName()));
return new NamedX509Key(npk.getAlgorithm(),
npk.getParams().getName(),
dsa.pkEncode(dsa.privKeyToPubKey(dsa.skDecode(npk.getExpanded()))));
}
public enum Version {
DRAFT, FINAL
}
@ -43,16 +66,16 @@ public class ML_DSA_Impls {
// --add-exports java.base/sun.security.provider=ALL-UNNAMED
public static Version version = Version.FINAL;
static int name2int(String name) {
if (name.endsWith("44")) {
static int name2int(String pname) {
if (pname.endsWith("44")) {
return 2;
} else if (name.endsWith("65")) {
} else if (pname.endsWith("65")) {
return 3;
} else if (name.endsWith("87")) {
} else if (pname.endsWith("87")) {
return 5;
} else {
// should not happen
throw new ProviderException("Unknown name " + name);
throw new ProviderException("Unknown name " + pname);
}
}
@ -69,20 +92,26 @@ public class ML_DSA_Impls {
}
@Override
protected byte[][] implGenerateKeyPair(String name, SecureRandom sr) {
byte[] seed = new byte[32];
var r = sr != null ? sr : JCAUtil.getDefSecureRandom();
protected byte[][] implGenerateKeyPair(String pname, SecureRandom random) {
byte[] seed = new byte[SEED_LEN];
var r = random != null ? random : JCAUtil.getDefSecureRandom();
r.nextBytes(seed);
ML_DSA mlDsa = new ML_DSA(name2int(name));
ML_DSA mlDsa = new ML_DSA(name2int(pname));
ML_DSA.ML_DSA_KeyPair kp = mlDsa.generateKeyPairInternal(seed);
var expanded = mlDsa.skEncode(kp.privateKey());
try {
return new byte[][]{
mlDsa.pkEncode(kp.publicKey()),
mlDsa.skEncode(kp.privateKey())
KeyChoices.writeToChoice(
KeyChoices.getPreferred("mldsa"),
seed, expanded),
expanded
};
} finally {
kp.privateKey().destroy();
Arrays.fill(seed, (byte)0);
Arrays.fill(seed, (byte) 0);
}
}
}
@ -109,8 +138,39 @@ public class ML_DSA_Impls {
public KF() {
super("ML-DSA", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87");
}
public KF(String name) {
super("ML-DSA", name);
public KF(String pname) {
super("ML-DSA", pname);
}
@Override
protected byte[] implExpand(String pname, byte[] input)
throws InvalidKeyException {
return KeyChoices.choiceToExpanded(pname, SEED_LEN, input,
ML_DSA_Impls::seedToExpanded);
}
@Override
protected Key engineTranslateKey(Key key) throws InvalidKeyException {
var nk = toNamedKey(key);
if (nk instanceof NamedPKCS8Key npk) {
var type = KeyChoices.getPreferred("mldsa");
if (KeyChoices.typeOfChoice(npk.getRawBytes()) != type) {
var encoding = KeyChoices.choiceToChoice(
type,
npk.getParams().getName(),
SEED_LEN, npk.getRawBytes(),
ML_DSA_Impls::seedToExpanded);
nk = NamedPKCS8Key.internalCreate(
npk.getAlgorithm(),
npk.getParams().getName(),
encoding,
npk.getExpanded().clone());
if (npk != key) { // npk is neither input or output
npk.destroy();
}
}
}
return nk;
}
}
@ -134,16 +194,16 @@ public class ML_DSA_Impls {
public sealed static class SIG extends NamedSignature permits SIG2, SIG3, SIG5 {
public SIG() {
super("ML-DSA", "ML-DSA-44", "ML-DSA-65", "ML-DSA-87");
super("ML-DSA", new KF());
}
public SIG(String name) {
super("ML-DSA", name);
public SIG(String pname) {
super("ML-DSA", new KF(pname));
}
@Override
protected byte[] implSign(String name, byte[] skBytes,
protected byte[] implSign(String pname, byte[] skBytes,
Object sk2, byte[] msg, SecureRandom sr) {
var size = name2int(name);
var size = name2int(pname);
var r = sr != null ? sr : JCAUtil.getDefSecureRandom();
byte[] rnd = new byte[32];
r.nextBytes(rnd);
@ -160,10 +220,10 @@ public class ML_DSA_Impls {
}
@Override
protected boolean implVerify(String name, byte[] pkBytes,
protected boolean implVerify(String pname, byte[] pkBytes,
Object pk2, byte[] msg, byte[] sigBytes)
throws SignatureException {
var size = name2int(name);
var size = name2int(pname);
var mlDsa = new ML_DSA(size);
if (version == Version.FINAL) {
// FIPS 204 Algorithm 3 ML-DSA.Verify prepend {0, len(ctx)}
@ -176,18 +236,18 @@ public class ML_DSA_Impls {
}
@Override
protected Object implCheckPublicKey(String name, byte[] pk)
protected Object implCheckPublicKey(String pname, byte[] pk)
throws InvalidKeyException {
ML_DSA mlDsa = new ML_DSA(name2int(name));
ML_DSA mlDsa = new ML_DSA(name2int(pname));
return mlDsa.checkPublicKey(pk);
}
@Override
protected Object implCheckPrivateKey(String name, byte[] sk)
protected Object implCheckPrivateKey(String pname, byte[] sk)
throws InvalidKeyException {
ML_DSA mlDsa = new ML_DSA(name2int(name));
ML_DSA mlDsa = new ML_DSA(name2int(pname));
return mlDsa.checkPrivateKey(sk);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -42,7 +42,6 @@ import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.NamedParameterSpec;
import java.util.Arrays;
import java.util.Objects;
/// A base class for all `KEM` implementations that can be
/// configured with a named parameter set. See [NamedKeyPairGenerator]
@ -50,21 +49,19 @@ import java.util.Objects;
public abstract class NamedKEM implements KEMSpi {
private final String fname; // family name
private final String[] pnames; // allowed parameter set name (at least one)
private final NamedKeyFactory fac;
/// Creates a new `NamedKEM` object.
///
/// @param fname the family name
/// @param pnames the standard parameter set names, at least one is needed.
protected NamedKEM(String fname, String... pnames) {
/// @param fac the `KeyFactory` used to translate foreign keys and
/// perform key validation
protected NamedKEM(String fname, NamedKeyFactory fac) {
if (fname == null) {
throw new AssertionError("fname cannot be null");
}
if (pnames == null || pnames.length == 0) {
throw new AssertionError("pnames cannot be null or empty");
}
this.fname = fname;
this.pnames = pnames;
this.fac = fac;
}
@Override
@ -76,8 +73,7 @@ public abstract class NamedKEM implements KEMSpi {
"The " + fname + " algorithm does not take any parameters");
}
// translate also check the key
var nk = (NamedX509Key) new NamedKeyFactory(fname, pnames)
.engineTranslateKey(publicKey);
var nk = (NamedX509Key) fac.toNamedKey(publicKey);
var pk = nk.getRawBytes();
return getKeyConsumerImpl(this, nk.getParams(), pk,
implCheckPublicKey(nk.getParams().getName(), pk), secureRandom);
@ -92,16 +88,15 @@ public abstract class NamedKEM implements KEMSpi {
"The " + fname + " algorithm does not take any parameters");
}
// translate also check the key
var nk = (NamedPKCS8Key) new NamedKeyFactory(fname, pnames)
.engineTranslateKey(privateKey);
var sk = nk.getRawBytes();
var nk = (NamedPKCS8Key) fac.toNamedKey(privateKey);
var sk = nk.getExpanded();
return getKeyConsumerImpl(this, nk.getParams(), sk,
implCheckPrivateKey(nk.getParams().getName(), sk), null);
}
// We don't have a flag on whether key is public key or private key.
// The correct method should always be called.
private record KeyConsumerImpl(NamedKEM kem, String name, int sslen,
private record KeyConsumerImpl(NamedKEM kem, String pname, int sslen,
int clen, byte[] key, Object k2, SecureRandom sr)
implements KEMSpi.EncapsulatorSpi, KEMSpi.DecapsulatorSpi {
@Override
@ -110,7 +105,7 @@ public abstract class NamedKEM implements KEMSpi {
if (encapsulation.length != clen) {
throw new DecapsulateException("Invalid key encapsulation message length");
}
var ss = kem.implDecapsulate(name, key, k2, encapsulation);
var ss = kem.implDecapsulate(pname, key, k2, encapsulation);
try {
return new SecretKeySpec(ss,
from, to - from, algorithm);
@ -121,7 +116,7 @@ public abstract class NamedKEM implements KEMSpi {
@Override
public KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm) {
var enc = kem.implEncapsulate(name, key, k2, sr);
var enc = kem.implEncapsulate(pname, key, k2, sr);
try {
return new KEM.Encapsulated(
new SecretKeySpec(enc[1],
@ -146,46 +141,46 @@ public abstract class NamedKEM implements KEMSpi {
private static KeyConsumerImpl getKeyConsumerImpl(NamedKEM kem,
NamedParameterSpec nps, byte[] key, Object k2, SecureRandom sr) {
String name = nps.getName();
return new KeyConsumerImpl(kem, name, kem.implSecretSize(name), kem.implEncapsulationSize(name),
String pname = nps.getName();
return new KeyConsumerImpl(kem, pname, kem.implSecretSize(pname), kem.implEncapsulationSize(pname),
key, k2, sr);
}
/// User-defined encap function.
///
/// @param name parameter name
/// @param pname parameter name
/// @param pk public key in raw bytes
/// @param pk2 parsed public key, `null` if none. See [#implCheckPublicKey].
/// @param sr SecureRandom object, `null` if not initialized
/// @return the key encapsulation message and the shared key (in this order)
/// @throws ProviderException if there is an internal error
protected abstract byte[][] implEncapsulate(String name, byte[] pk, Object pk2, SecureRandom sr);
protected abstract byte[][] implEncapsulate(String pname, byte[] pk, Object pk2, SecureRandom sr);
/// User-defined decap function.
///
/// @param name parameter name
/// @param pname parameter name
/// @param sk private key in raw bytes
/// @param sk2 parsed private key, `null` if none. See [#implCheckPrivateKey].
/// @param encap the key encapsulation message
/// @return the shared key
/// @throws ProviderException if there is an internal error
/// @throws DecapsulateException if there is another error
protected abstract byte[] implDecapsulate(String name, byte[] sk, Object sk2, byte[] encap)
protected abstract byte[] implDecapsulate(String pname, byte[] sk, Object sk2, byte[] encap)
throws DecapsulateException;
/// User-defined function returning shared secret key length.
///
/// @param name parameter name
/// @param pname parameter name
/// @return shared secret key length
/// @throws ProviderException if there is an internal error
protected abstract int implSecretSize(String name);
protected abstract int implSecretSize(String pname);
/// User-defined function returning key encapsulation message length.
///
/// @param name parameter name
/// @param pname parameter name
/// @return key encapsulation message length
/// @throws ProviderException if there is an internal error
protected abstract int implEncapsulationSize(String name);
protected abstract int implEncapsulationSize(String pname);
/// User-defined function to validate a public key.
///
@ -196,11 +191,11 @@ public abstract class NamedKEM implements KEMSpi {
///
/// The default implementation returns `null`.
///
/// @param name parameter name
/// @param pname parameter name
/// @param pk public key in raw bytes
/// @return a parsed key, `null` if none.
/// @throws InvalidKeyException if the key is invalid
protected Object implCheckPublicKey(String name, byte[] pk) throws InvalidKeyException {
protected Object implCheckPublicKey(String pname, byte[] pk) throws InvalidKeyException {
return null;
}
@ -213,11 +208,11 @@ public abstract class NamedKEM implements KEMSpi {
///
/// The default implementation returns `null`.
///
/// @param name parameter name
/// @param pname parameter name
/// @param sk private key in raw bytes
/// @return a parsed key, `null` if none.
/// @throws InvalidKeyException if the key is invalid
protected Object implCheckPrivateKey(String name, byte[] sk) throws InvalidKeyException {
protected Object implCheckPrivateKey(String pname, byte[] sk) throws InvalidKeyException {
return null;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -42,7 +42,6 @@ import java.security.spec.NamedParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Objects;
/// A base class for all `KeyFactory` implementations that can be
/// configured with a named parameter set. See [NamedKeyPairGenerator]
@ -58,7 +57,7 @@ import java.util.Objects;
///
/// When reading from a RAW format, it needs enough info to derive the
/// parameter set name.
public class NamedKeyFactory extends KeyFactorySpi {
public abstract class NamedKeyFactory extends KeyFactorySpi {
private final String fname; // family name
private final String[] pnames; // allowed parameter set name (at least one)
@ -78,92 +77,110 @@ public class NamedKeyFactory extends KeyFactorySpi {
this.pnames = pnames;
}
private String checkName(String name) throws InvalidKeyException {
for (var pname : pnames) {
if (pname.equalsIgnoreCase(name)) {
private String checkName(String pname) throws InvalidKeyException {
for (var n : pnames) {
if (n.equalsIgnoreCase(pname)) {
// return the stored standard name
return pname;
return n;
}
}
throw new InvalidKeyException("Unsupported parameter set name: " + name);
throw new InvalidKeyException("Unsupported parameter set name: " + pname);
}
@Override
protected PublicKey engineGeneratePublic(KeySpec keySpec)
throws InvalidKeySpecException {
if (keySpec instanceof X509EncodedKeySpec xspec) {
try {
return fromX509(xspec.getEncoded());
} catch (InvalidKeyException e) {
throw new InvalidKeySpecException(e);
return switch (keySpec) {
case X509EncodedKeySpec xspec -> {
try {
yield fromX509(xspec.getEncoded());
} catch (InvalidKeyException e) {
throw new InvalidKeySpecException(e);
}
}
} else if (keySpec instanceof RawKeySpec rks) {
if (pnames.length == 1) {
return new NamedX509Key(fname, pnames[0], rks.getKeyArr());
} else {
throw new InvalidKeySpecException("Parameter set name unavailable");
case RawKeySpec rks -> {
if (pnames.length == 1) {
yield new NamedX509Key(fname, pnames[0], rks.getKeyArr());
} else {
throw new InvalidKeySpecException("Parameter set name unavailable");
}
}
} else if (keySpec instanceof EncodedKeySpec espec
&& espec.getFormat().equalsIgnoreCase("RAW")) {
if (pnames.length == 1) {
return new NamedX509Key(fname, pnames[0], espec.getEncoded());
} else {
throw new InvalidKeySpecException("Parameter set name unavailable");
case EncodedKeySpec espec when espec.getFormat().equalsIgnoreCase("RAW") -> {
if (pnames.length == 1) {
yield new NamedX509Key(fname, pnames[0], espec.getEncoded());
} else {
throw new InvalidKeySpecException("Parameter set name unavailable");
}
}
} else {
throw new InvalidKeySpecException("Unsupported keyspec: " + keySpec);
}
case null -> throw new InvalidKeySpecException(
"keySpec must not be null");
default ->
throw new InvalidKeySpecException(keySpec.getClass().getName() +
" not supported.");
};
}
@Override
protected PrivateKey engineGeneratePrivate(KeySpec keySpec)
throws InvalidKeySpecException {
if (keySpec instanceof PKCS8EncodedKeySpec pspec) {
var bytes = pspec.getEncoded();
try {
return fromPKCS8(bytes);
} catch (InvalidKeyException e) {
throw new InvalidKeySpecException(e);
} finally {
Arrays.fill(bytes, (byte) 0);
}
} else if (keySpec instanceof RawKeySpec rks) {
if (pnames.length == 1) {
var bytes = rks.getKeyArr();
return switch (keySpec) {
case PKCS8EncodedKeySpec pspec -> {
var bytes = pspec.getEncoded();
try {
return new NamedPKCS8Key(fname, pnames[0], bytes);
yield fromPKCS8(bytes);
} catch (InvalidKeyException e) {
throw new InvalidKeySpecException(e);
} finally {
Arrays.fill(bytes, (byte) 0);
}
} else {
throw new InvalidKeySpecException("Parameter set name unavailable");
}
} else if (keySpec instanceof EncodedKeySpec espec
&& espec.getFormat().equalsIgnoreCase("RAW")) {
if (pnames.length == 1) {
var bytes = espec.getEncoded();
try {
return new NamedPKCS8Key(fname, pnames[0], bytes);
} finally {
Arrays.fill(bytes, (byte) 0);
case RawKeySpec rks -> {
if (pnames.length == 1) {
var raw = rks.getKeyArr();
try {
yield fromRaw(pnames[0], raw);
} catch (InvalidKeyException e) {
throw new InvalidKeySpecException("Invalid key input", e);
}
} else {
throw new InvalidKeySpecException("Parameter set name unavailable");
}
} else {
throw new InvalidKeySpecException("Parameter set name unavailable");
}
} else {
throw new InvalidKeySpecException("Unsupported keyspec: " + keySpec);
}
case EncodedKeySpec espec when espec.getFormat().equalsIgnoreCase("RAW") -> {
if (pnames.length == 1) {
var raw = espec.getEncoded();
try {
yield fromRaw(pnames[0], raw);
} catch (InvalidKeyException e) {
throw new InvalidKeySpecException("Invalid key input", e);
}
} else {
throw new InvalidKeySpecException("Parameter set name unavailable");
}
}
case null -> throw new InvalidKeySpecException(
"keySpec must not be null");
default ->
throw new InvalidKeySpecException(keySpec.getClass().getName() +
" not supported.");
};
}
private PrivateKey fromRaw(String pname, byte[] raw)
throws InvalidKeyException {
return NamedPKCS8Key.internalCreate(
fname, pname, raw, implExpand(pname, raw));
}
private PrivateKey fromPKCS8(byte[] bytes)
throws InvalidKeyException, InvalidKeySpecException {
var k = new NamedPKCS8Key(fname, bytes);
throws InvalidKeyException {
var k = new NamedPKCS8Key(fname, bytes, this::implExpand);
checkName(k.getParams().getName());
return k;
}
private PublicKey fromX509(byte[] bytes)
throws InvalidKeyException, InvalidKeySpecException {
throws InvalidKeyException {
var k = new NamedX509Key(fname, bytes);
checkName(k.getParams().getName());
return k;
@ -184,7 +201,7 @@ public class NamedKeyFactory extends KeyFactorySpi {
protected <T extends KeySpec> T engineGetKeySpec(Key key, Class<T> keySpec)
throws InvalidKeySpecException {
try {
key = engineTranslateKey(key);
key = toNamedKey(key);
} catch (InvalidKeyException e) {
throw new InvalidKeySpecException(e);
}
@ -225,6 +242,12 @@ public class NamedKeyFactory extends KeyFactorySpi {
@Override
protected Key engineTranslateKey(Key key) throws InvalidKeyException {
// The base toNamedKey only makes sure key is translated into a NamedKey.
// the key material is still the same as the input.
return toNamedKey(key);
}
protected Key toNamedKey(Key key) throws InvalidKeyException {
if (key == null) {
throw new InvalidKeyException("Key must not be null");
}
@ -242,27 +265,28 @@ public class NamedKeyFactory extends KeyFactorySpi {
} else if (format.equalsIgnoreCase("RAW")) {
var kAlg = key.getAlgorithm();
if (key instanceof AsymmetricKey pk) {
String name;
String pname;
// Three cases that we can find the parameter set name from a RAW key:
// 1. getParams() returns one
// 2. getAlgorithm() returns param set name (some provider does this)
// 3. getAlgorithm() returns family name but this KF is for param set name
if (pk.getParams() instanceof NamedParameterSpec nps) {
name = checkName(nps.getName());
pname = checkName(nps.getName());
} else {
if (kAlg.equalsIgnoreCase(fname)) {
if (pnames.length == 1) {
name = pnames[0];
pname = pnames[0];
} else {
throw new InvalidKeyException("No parameter set info");
}
} else {
name = checkName(kAlg);
pname = checkName(kAlg);
}
}
var raw = key.getEncoded();
return key instanceof PrivateKey
? new NamedPKCS8Key(fname, name, key.getEncoded())
: new NamedX509Key(fname, name, key.getEncoded());
? fromRaw(pname, raw)
: new NamedX509Key(fname, pname, raw);
} else {
throw new InvalidKeyException("Unsupported key type: " + key.getClass());
}
@ -270,19 +294,26 @@ public class NamedKeyFactory extends KeyFactorySpi {
var bytes = key.getEncoded();
try {
return fromPKCS8(bytes);
} catch (InvalidKeySpecException e) {
throw new InvalidKeyException("Invalid PKCS#8 key", e);
} finally {
Arrays.fill(bytes, (byte) 0);
}
} else if (format.equalsIgnoreCase("X.509") && key instanceof PublicKey) {
try {
return fromX509(key.getEncoded());
} catch (InvalidKeySpecException e) {
throw new InvalidKeyException("Invalid X.509 key", e);
}
return fromX509(key.getEncoded());
} else {
throw new InvalidKeyException("Unsupported key format: " + key.getFormat());
}
}
/// User-defined function to generate the expanded format of
/// a [NamedPKCS8Key] from its encoding format.
///
/// This method is called when the key factory is constructing a private
/// key. The ownership of the result is fully granted to the caller.
///
/// @param pname the parameter set name
/// @param input the encoding, could be any format
/// @return the expanded key, not null
/// @throws InvalidKeyException if `input` is invalid
protected abstract byte[] implExpand(String pname, byte[] input)
throws InvalidKeyException;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -36,7 +36,6 @@ import java.security.ProviderException;
import java.security.SecureRandom;
import java.security.spec.AlgorithmParameterSpec;
import java.security.spec.NamedParameterSpec;
import java.util.Objects;
/// A base class for all `KeyPairGenerator` implementations that can be
/// configured with a named parameter set.
@ -52,15 +51,21 @@ import java.util.Objects;
/// with `getAlgorithm` returning the family name, and `getParams` returning
/// the parameter set name as a [NamedParameterSpec] object.
///
/// An implementation must include a zero-argument public constructor that
/// calls `super(fname, pnames)`, where `fname` is the family name of the
/// algorithm and `pnames` are its supported parameter set names. `pnames`
/// must contain at least one element. For an implementation of
/// `NamedKeyPairGenerator`, the first element becomes its default parameter
/// set, i.e. the parameter set to be used in key pair generation unless
/// A `NamedKeyPairGenerator` or `NamedKeyFactory` implementation must include
/// a zero-argument public constructor that calls `super(fname, pnames)`, where
/// `fname` is the family name of the algorithm and `pnames` are its supported
/// parameter set names. `pnames` must contain at least one element. For an
/// implementation of `NamedKeyPairGenerator`, the first element becomes its
/// default parameter set, i.e. the parameter set used by generated keys unless
/// [#initialize(AlgorithmParameterSpec, java.security.SecureRandom)]
/// is called on a different parameter set.
///
/// A `NamedKEM` or `NamedSignature` implementation must include a zero-argument
/// public constructor that calls `super(fname, factory)`, where `fname` is the
/// family name of the algorithm and `factory` is the `NamedKeyFactory` object
/// that is used to translate foreign keys. `factory` only recognizes
/// parameter sets supported by this implementation.
///
/// An implementation must implement all abstract methods. For all these
/// methods, the implementation must relinquish any "ownership" of any input
/// and output array argument. Precisely, the implementation must not retain
@ -69,8 +74,8 @@ import java.util.Objects;
/// array argument and must not retain any reference to an input array argument
/// after the call.
///
/// Also, an implementation must not keep any extra copy of a private key.
/// For key generation, the only copy is the one returned in the
/// Also, an implementation must not keep any extra copy of a private key in
/// any format. For key generation, the only copy is the one returned in the
/// [#implGenerateKeyPair] call. For all other methods, it must not make
/// a copy of the input private key. A `KEM` implementation also must not
/// keep a copy of the shared secret key, no matter if it's an encapsulator
@ -84,6 +89,34 @@ import java.util.Objects;
/// (For example, `implSign`) later. An implementation must not retain
/// a reference of the parsed key.
///
/// The private key, represented as a byte array when used in `NamedKEM` or
/// `NamedSignature`, is referred to as its expanded format. For some
/// algorithms, this format may differ from the
/// [key material][NamedPKCS8Key#getRawBytes()] inside a PKCS #8 file. For example,
/// [FIPS 204](https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.204.pdf)
/// Table 2 defines the ML-DSA-65 private key as a 4032-byte array, which is
/// used in the ML-DSA.Sign function in Algorithm 2, representing the
/// expanded format. However, in
/// [RFC 9881](https://datatracker.ietf.org/doc/html/rfc9881#name-private-key-format),
/// a private key can be encoded into a CHOICE of three formats, none in the
/// same as the FIPS 204 format. The choices are defined in
/// [sun.security.util.KeyChoices]. A `NamedKeyPairGenerator` implementation
/// should return both the expanded key and a preferred encoding in its
/// [#implGenerateKeyPair] method.
///
/// A `NamedKeyFactory` must override the `implExpand` method to derive
/// the expanded format from an encoding format, or return `null` if there
/// is no difference.
///
/// Implementations may support multiple encoding formats.
///
/// A `NamedKeyFactory` must not modify the encoding when generating a key
/// from a `KeySpec` object, ensuring that when re-encoded, the key retains
/// its original encoding format.
///
/// A `NamedKeyFactory` can choose a different encoding format when
/// `translateKey` is called.
///
/// When constructing a [NamedX509Key] or [NamedPKCS8Key] object from raw key
/// bytes, the key bytes are directly referenced within the object, so the
/// caller must not modify them afterward. Similarly, the key's `getRawBytes`
@ -105,9 +138,9 @@ import java.util.Objects;
public abstract class NamedKeyPairGenerator extends KeyPairGeneratorSpi {
private final String fname; // family name
private final String[] pnames; // allowed parameter set name (at least one)
private final String[] pnames; // allowed parameter set names (at least one)
protected String name; // init as
protected String pname; // parameter set name, if can be determined
private SecureRandom secureRandom;
/// Creates a new `NamedKeyPairGenerator` object.
@ -126,22 +159,22 @@ public abstract class NamedKeyPairGenerator extends KeyPairGeneratorSpi {
this.pnames = pnames;
}
private String checkName(String name) throws InvalidAlgorithmParameterException {
for (var pname : pnames) {
if (pname.equalsIgnoreCase(name)) {
// return the stored standard name
return pname;
private String checkName(String pname) throws InvalidAlgorithmParameterException {
for (var n : pnames) {
if (n.equalsIgnoreCase(pname)) {
// return the stored standard pname
return n;
}
}
throw new InvalidAlgorithmParameterException(
"Unsupported parameter set name: " + name);
"Unsupported parameter set name: " + pname);
}
@Override
public void initialize(AlgorithmParameterSpec params, SecureRandom random)
throws InvalidAlgorithmParameterException {
if (params instanceof NamedParameterSpec spec) {
name = checkName(spec.getName());
pname = checkName(spec.getName());
} else {
throw new InvalidAlgorithmParameterException(
"Unsupported AlgorithmParameterSpec: " + params);
@ -161,17 +194,21 @@ public abstract class NamedKeyPairGenerator extends KeyPairGeneratorSpi {
@Override
public KeyPair generateKeyPair() {
String pname = name != null ? name : pnames[0];
var keys = implGenerateKeyPair(pname, secureRandom);
return new KeyPair(new NamedX509Key(fname, pname, keys[0]),
new NamedPKCS8Key(fname, pname, keys[1]));
String tmpName = pname != null ? pname : pnames[0];
var keys = implGenerateKeyPair(tmpName, secureRandom);
return new KeyPair(new NamedX509Key(fname, tmpName, keys[0]),
NamedPKCS8Key.internalCreate(fname, tmpName, keys[1],
keys.length == 2 ? null : keys[2]));
}
/// User-defined key pair generator.
///
/// @param pname parameter set name
/// @param sr `SecureRandom` object, `null` if not initialized
/// @return public key and private key (in this order) in raw bytes
/// @return the public key, the private key in its encoding format, and
/// the private key in its expanded format (in this order) in
/// raw bytes. If the expanded format of the private key is the
/// same as its encoding format, the 3rd element must be omitted.
/// @throws ProviderException if there is an internal error
protected abstract byte[][] implGenerateKeyPair(String pname, SecureRandom sr);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -40,7 +40,6 @@ import java.security.SecureRandom;
import java.security.SignatureException;
import java.security.SignatureSpi;
import java.security.spec.AlgorithmParameterSpec;
import java.util.Objects;
/// A base class for all `Signature` implementations that can be
/// configured with a named parameter set. See [NamedKeyPairGenerator]
@ -50,12 +49,12 @@ import java.util.Objects;
public abstract class NamedSignature extends SignatureSpi {
private final String fname; // family name
private final String[] pnames; // allowed parameter set name (at least one)
private final NamedKeyFactory fac;
private final ByteArrayOutputStream bout = new ByteArrayOutputStream();
// init with...
private String name;
private String pname;
private byte[] secKey;
private byte[] pubKey;
@ -65,26 +64,23 @@ public abstract class NamedSignature extends SignatureSpi {
/// Creates a new `NamedSignature` object.
///
/// @param fname the family name
/// @param pnames the standard parameter set names, at least one is needed.
protected NamedSignature(String fname, String... pnames) {
/// @param fac the `KeyFactory` used to translate foreign keys and
/// perform key validation
protected NamedSignature(String fname, NamedKeyFactory fac) {
if (fname == null) {
throw new AssertionError("fname cannot be null");
}
if (pnames == null || pnames.length == 0) {
throw new AssertionError("pnames cannot be null or empty");
}
this.fname = fname;
this.pnames = pnames;
this.fac = fac;
}
@Override
protected void engineInitVerify(PublicKey publicKey) throws InvalidKeyException {
// translate also check the key
var nk = (NamedX509Key) new NamedKeyFactory(fname, pnames)
.engineTranslateKey(publicKey);
name = nk.getParams().getName();
var nk = (NamedX509Key) fac.toNamedKey(publicKey);
pname = nk.getParams().getName();
pubKey = nk.getRawBytes();
pk2 = implCheckPublicKey(name, pubKey);
pk2 = implCheckPublicKey(pname, pubKey);
secKey = null;
bout.reset();
}
@ -92,11 +88,10 @@ public abstract class NamedSignature extends SignatureSpi {
@Override
protected void engineInitSign(PrivateKey privateKey) throws InvalidKeyException {
// translate also check the key
var nk = (NamedPKCS8Key) new NamedKeyFactory(fname, pnames)
.engineTranslateKey(privateKey);
name = nk.getParams().getName();
secKey = nk.getRawBytes();
sk2 = implCheckPrivateKey(name, secKey);
var nk = (NamedPKCS8Key) fac.toNamedKey(privateKey);
pname = nk.getParams().getName();
secKey = nk.getExpanded();
sk2 = implCheckPrivateKey(pname, secKey);
pubKey = null;
bout.reset();
}
@ -116,7 +111,7 @@ public abstract class NamedSignature extends SignatureSpi {
if (secKey != null) {
var msg = bout.toByteArray();
bout.reset();
return implSign(name, secKey, sk2, msg, appRandom);
return implSign(pname, secKey, sk2, msg, appRandom);
} else {
throw new SignatureException("No private key");
}
@ -127,21 +122,21 @@ public abstract class NamedSignature extends SignatureSpi {
if (pubKey != null) {
var msg = bout.toByteArray();
bout.reset();
return implVerify(name, pubKey, pk2, msg, sig);
return implVerify(pname, pubKey, pk2, msg, sig);
} else {
throw new SignatureException("No public key");
}
}
@Override
@SuppressWarnings("deprecation")
@Deprecated
protected void engineSetParameter(String param, Object value)
throws InvalidParameterException {
throw new InvalidParameterException("setParameter() not supported");
}
@Override
@SuppressWarnings("deprecation")
@Deprecated
protected Object engineGetParameter(String param) throws InvalidParameterException {
throw new InvalidParameterException("getParameter() not supported");
}
@ -162,7 +157,7 @@ public abstract class NamedSignature extends SignatureSpi {
/// User-defined sign function.
///
/// @param name parameter name
/// @param pname parameter name
/// @param sk private key in raw bytes
/// @param sk2 parsed private key, `null` if none. See [#implCheckPrivateKey].
/// @param msg the message
@ -170,12 +165,12 @@ public abstract class NamedSignature extends SignatureSpi {
/// @return the signature
/// @throws ProviderException if there is an internal error
/// @throws SignatureException if there is another error
protected abstract byte[] implSign(String name, byte[] sk, Object sk2,
protected abstract byte[] implSign(String pname, byte[] sk, Object sk2,
byte[] msg, SecureRandom sr) throws SignatureException;
/// User-defined verify function.
///
/// @param name parameter name
/// @param pname parameter name
/// @param pk public key in raw bytes
/// @param pk2 parsed public key, `null` if none. See [#implCheckPublicKey].
/// @param msg the message
@ -183,7 +178,7 @@ public abstract class NamedSignature extends SignatureSpi {
/// @return true if verified
/// @throws ProviderException if there is an internal error
/// @throws SignatureException if there is another error
protected abstract boolean implVerify(String name, byte[] pk, Object pk2,
protected abstract boolean implVerify(String pname, byte[] pk, Object pk2,
byte[] msg, byte[] sig) throws SignatureException;
/// User-defined function to validate a public key.
@ -195,11 +190,11 @@ public abstract class NamedSignature extends SignatureSpi {
///
/// The default implementation returns `null`.
///
/// @param name parameter name
/// @param pname parameter name
/// @param pk public key in raw bytes
/// @return a parsed key, `null` if none.
/// @throws InvalidKeyException if the key is invalid
protected Object implCheckPublicKey(String name, byte[] pk) throws InvalidKeyException {
protected Object implCheckPublicKey(String pname, byte[] pk) throws InvalidKeyException {
return null;
}
@ -212,11 +207,11 @@ public abstract class NamedSignature extends SignatureSpi {
///
/// The default implementation returns `null`.
///
/// @param name parameter name
/// @param pname parameter name
/// @param sk private key in raw bytes
/// @return a parsed key, `null` if none.
/// @throws InvalidKeyException if the key is invalid
protected Object implCheckPrivateKey(String name, byte[] sk) throws InvalidKeyException {
protected Object implCheckPrivateKey(String pname, byte[] sk) throws InvalidKeyException {
return null;
}
}

View File

@ -0,0 +1,289 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package sun.security.util;
import java.security.*;
import java.util.Arrays;
import java.util.Locale;
import java.util.function.BiFunction;
/**
* The content of an ML-KEM or ML-DSA private key is defined as a CHOICE
* among three different representations. For example:
* <pre>
* ML-KEM-1024-PrivateKey ::= CHOICE {
* seed [0] OCTET STRING (SIZE (64)),
* expandedKey OCTET STRING (SIZE (3168)),
* both SEQUENCE {
* seed OCTET STRING (SIZE (64)),
* expandedKey OCTET STRING (SIZE (3168))
* }
* }
* </pre>
* This class supports reading, writing, and converting between them.
* <p>
* Current code follows draft-ietf-lamps-kyber-certificates-11 and RFC 9881.
*/
public final class KeyChoices {
public enum Type { SEED, EXPANDED_KEY, BOTH }
private record Choice(Type type, byte[] seed, byte[] expanded) {}
/**
* Gets the preferred choice type for an algorithm, defined as an
* overridable security property "jdk.<name>.pkcs8.encoding".
*
* @param name "mlkem" or "mldsa".
* @throws IllegalArgumentException if property is invalid value
* @return the type
*/
public static Type getPreferred(String name) {
var prop = SecurityProperties.getOverridableProperty(
"jdk." + name + ".pkcs8.encoding");
if (prop == null) {
return Type.SEED;
}
return switch (prop.toLowerCase(Locale.ROOT)) {
case "seed" -> Type.SEED;
case "expandedkey" -> Type.EXPANDED_KEY;
case "both" -> Type.BOTH;
default -> throw new IllegalArgumentException("Unknown format: " + prop);
};
}
/**
* Writes one of the ML-KEM or ML-DSA private key formats.
* <p>
* This method does not check the length of the inputs or whether
* they match each other. The caller must make sure `seed` and/or
* `expanded` are provided if `type` requires any of them.
*
* @param type preferred output choice type
* @param seed the seed, could be null
* @param expanded the expanded key, could be null
* @return one of the choices
*/
public static byte[] writeToChoice(Type type, byte[] seed, byte[] expanded) {
byte[] skOctets;
// Ensures using one-byte len in DER
assert seed == null || seed.length < 128;
// Ensures using two-byte len in DER
assert expanded == null || expanded.length > 256 && expanded.length < 60000;
return switch (type) {
case SEED -> {
assert seed != null;
skOctets = new byte[seed.length + 2];
skOctets[0] = (byte)0x80;
skOctets[1] = (byte) seed.length;
System.arraycopy(seed, 0, skOctets, 2, seed.length);
yield skOctets;
}
case EXPANDED_KEY -> {
assert expanded != null;
skOctets = new byte[expanded.length + 4];
skOctets[0] = 0x04;
writeShortLength(skOctets, 1, expanded.length);
System.arraycopy(expanded, 0, skOctets, 4, expanded.length);
yield skOctets;
}
case BOTH -> {
assert seed != null;
assert expanded != null;
skOctets = new byte[10 + seed.length + expanded.length];
skOctets[0] = 0x30;
writeShortLength(skOctets, 1, 6 + seed.length + expanded.length);
skOctets[4] = 0x04;
skOctets[5] = (byte)seed.length;
System.arraycopy(seed, 0, skOctets, 6, seed.length);
skOctets[6 + seed.length] = 0x04;
writeShortLength(skOctets, 7 + seed.length, expanded.length);
System.arraycopy(expanded, 0, skOctets, 10 + seed.length, expanded.length);
yield skOctets;
}
};
}
/**
* Gets the type of input.
*
* @param input input bytes
* @return the type
* @throws InvalidKeyException if input is invalid
*/
public static Type typeOfChoice(byte[] input) throws InvalidKeyException {
if (input.length < 1) {
throw new InvalidKeyException("Empty key");
}
return switch (input[0]) {
case (byte) 0x80 -> Type.SEED;
case 0x04 -> Type.EXPANDED_KEY;
case 0x30 -> Type.BOTH;
default -> throw new InvalidKeyException("Wrong tag: " + input[0]);
};
}
/**
* Splits one of the ML-KEM or ML-DSA private key formats into
* seed and expandedKey, if exists.
*
* @param seedLen correct seed length
* @param input input bytes
* @return a {@code Choice} object. Byte arrays inside are newly allocated
* @throws InvalidKeyException if input is invalid
*/
private static Choice readFromChoice(int seedLen, byte[] input)
throws InvalidKeyException {
if (input.length < seedLen + 2) {
throw new InvalidKeyException("Too short");
}
return switch (input[0]) {
case (byte) 0x80 -> {
// 80 SEED_LEN <SEED_LEN of seed>
if (input[1] != seedLen && input.length != seedLen + 2) {
throw new InvalidKeyException("Invalid seed");
}
yield new Choice(Type.SEED,
Arrays.copyOfRange(input, 2, seedLen + 2), null);
}
case 0x04 -> {
// 04 82 nn nn <nn of expandedKey>
if (readShortLength(input, 1) != input.length - 4) {
throw new InvalidKeyException("Invalid expandedKey");
}
yield new Choice(Type.EXPANDED_KEY,
null, Arrays.copyOfRange(input, 4, input.length));
}
case 0x30 -> {
// 30 82 mm mm 04 SEED_LEN <SEED_LEN of seed> 04 82 nn nn <nn of expandedKey>
if (input.length < 6 + seedLen + 4) {
throw new InvalidKeyException("Too short");
}
if (readShortLength(input, 1) != input.length - 4
|| input[4] != 0x04
|| input[5] != (byte)seedLen
|| input[seedLen + 6] != 0x04
|| readShortLength(input, seedLen + 7)
!= input.length - 10 - seedLen) {
throw new InvalidKeyException("Invalid both");
}
yield new Choice(Type.BOTH,
Arrays.copyOfRange(input, 6, 6 + seedLen),
Arrays.copyOfRange(input, seedLen + 10, input.length));
}
default -> throw new InvalidKeyException("Wrong tag: " + input[0]);
};
}
/**
* Reads from any encoding and write to the specified type.
*
* @param type preferred output choice type
* @param pname parameter set name
* @param seedLen seed length
* @param input the input encoding
* @param expander function to calculate expanded from seed, could be null
* if there is already expanded in input
* @return the preferred encoding
* @throws InvalidKeyException if input is invalid or does not have enough
* information to generate the output
*/
public static byte[] choiceToChoice(Type type, String pname,
int seedLen, byte[] input,
BiFunction<String, byte[], byte[]> expander)
throws InvalidKeyException {
var choice = readFromChoice(seedLen, input);
try {
if (type != Type.EXPANDED_KEY && choice.type == Type.EXPANDED_KEY) {
throw new InvalidKeyException(
"key contains not enough info to translate");
}
var expanded = (choice.expanded == null && type != Type.SEED)
? expander.apply(pname, choice.seed)
: choice.expanded;
return writeToChoice(type, choice.seed, expanded);
} finally {
if (choice.seed != null) {
Arrays.fill(choice.seed, (byte) 0);
}
if (choice.expanded != null) {
Arrays.fill(choice.expanded, (byte) 0);
}
}
}
/**
* Reads from any choice of encoding and return the expanded format.
*
* @param pname parameter set name
* @param seedLen seed length
* @param input input encoding
* @param expander function to calculate expanded from seed, could be null
* if there is already expanded in input
* @return the expanded key
* @throws InvalidKeyException if input is invalid
*/
public static byte[] choiceToExpanded(String pname,
int seedLen, byte[] input,
BiFunction<String, byte[], byte[]> expander)
throws InvalidKeyException {
var choice = readFromChoice(seedLen, input);
if (choice.type == Type.BOTH) {
var calculated = expander.apply(pname, choice.seed);
if (!Arrays.equals(choice.expanded, calculated)) {
throw new InvalidKeyException("seed and expandedKey do not match");
}
Arrays.fill(calculated, (byte)0);
}
try {
if (choice.expanded != null) {
return choice.expanded;
}
return expander.apply(pname, choice.seed);
} finally {
if (choice.seed != null) {
Arrays.fill(choice.seed, (byte)0);
}
}
}
// Reads a 2 bytes length from DER encoding
private static int readShortLength(byte[] input, int from)
throws InvalidKeyException {
if (input[from] != (byte)0x82) {
throw new InvalidKeyException("Unexpected length");
}
return ((input[from + 1] & 0xff) << 8) + (input[from + 2] & 0xff);
}
// Writes a 2 bytes length to DER encoding
private static void writeShortLength(byte[] input, int from, int value) {
input[from] = (byte)0x82;
input[from + 1] = (byte) (value >> 8);
input[from + 2] = (byte) (value);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -71,7 +71,8 @@ public final class NamedX509Key extends X509Key {
setKey(new BitArray(rawBytes.length * 8, rawBytes));
}
/// Ctor from family name, and X.509 bytes
/// Ctor from family name, and X.509 bytes. Input byte array
/// is copied. Caller can modify it after the method call.
public NamedX509Key(String fname, byte[] encoded) throws InvalidKeyException {
this.fname = fname;
decode(encoded);

View File

@ -1700,3 +1700,28 @@ jdk.epkcs8.defaultAlgorithm=PBEWithHmacSHA256AndAES_128
# com.sun.security.allowedAIALocations=http://some.company.com/cacert \
# ldap://ldap.company.com/dc=company,dc=com?caCertificate;binary
com.sun.security.allowedAIALocations=
#
# PKCS #8 encoding format for newly created ML-KEM and ML-DSA private keys
#
# draft-ietf-lamps-kyber-certificates-11 and RFC 9881 define three possible formats for a private key:
# a seed (64 bytes for ML-KEM, 32 bytes for ML-DSA), an expanded private key,
# or a sequence containing both.
#
# The following security properties determine the encoding format used when a
# new keypair is generated with a KeyPairGenerator, and the output of the
# translateKey method on an existing key using a ML-KEM or ML-DSA KeyFactory.
#
# Valid values for these properties are "seed", "expandedKey", and "both"
# (case-insensitive). The default is "seed".
#
# If a system property of the same name is also specified, it supersedes the
# security property value defined here.
#
# Note: These properties are currently used by the SunJCE (for ML-KEM) and SUN
# (for ML-DSA) providers in the JDK Reference implementation. They are not
# guaranteed to be supported by other implementations or third-party security
# providers.
#
#jdk.mlkem.pkcs8.encoding = seed
#jdk.mldsa.pkcs8.encoding = seed

View File

@ -41,6 +41,8 @@ import java.util.zip.ZipFile;
* ML_DSA_Impls.version might be modified
* @library /test/lib
* @modules java.base/sun.security.provider
* java.base/sun.security.util
* java.base/com.sun.crypto.provider
* @run main/othervm/timeout=480 Launcher
*/
@ -50,6 +52,8 @@ import java.util.zip.ZipFile;
* @summary Test verifying the intrinsic implementation.
* @library /test/lib
* @modules java.base/sun.security.provider
* java.base/sun.security.util
* java.base/com.sun.crypto.provider
* @run main/othervm/timeout=480 -Xcomp Launcher
*/

View File

@ -24,6 +24,7 @@ import jdk.test.lib.Asserts;
import jdk.test.lib.json.JSONValue;
import jdk.test.lib.security.FixedSecureRandom;
import sun.security.provider.ML_DSA_Impls;
import sun.security.util.DerOutputStream;
import java.security.*;
import java.security.spec.EncodedKeySpec;
@ -68,12 +69,13 @@ public class ML_DSA_Test {
System.out.println(">> " + pname);
for (var c : t.get("tests").asArray()) {
System.out.print(c.get("tcId").asString() + " ");
g.initialize(np, new FixedSecureRandom(toByteArray(c.get("seed").asString())));
var seed = toByteArray(c.get("seed").asString());
g.initialize(np, new FixedSecureRandom(seed));
var kp = g.generateKeyPair();
var pk = f.getKeySpec(kp.getPublic(), EncodedKeySpec.class).getEncoded();
var sk = f.getKeySpec(kp.getPrivate(), EncodedKeySpec.class).getEncoded();
Asserts.assertEqualsByteArray(toByteArray(c.get("pk").asString()), pk);
Asserts.assertEqualsByteArray(toByteArray(c.get("sk").asString()), sk);
Asserts.assertEqualsByteArray(toByteArray(c.get("sk").asString()),
ML_DSA_Impls.seedToExpanded(pname, seed));
}
System.out.println();
}
@ -106,7 +108,7 @@ public class ML_DSA_Test {
var sk = new PrivateKey() {
public String getAlgorithm() { return pname; }
public String getFormat() { return "RAW"; }
public byte[] getEncoded() { return toByteArray(c.get("sk").asString()); }
public byte[] getEncoded() { return oct(toByteArray(c.get("sk").asString())); }
};
var sr = new FixedSecureRandom(
det ? new byte[32] : toByteArray(c.get("rnd").asString()));
@ -119,6 +121,10 @@ public class ML_DSA_Test {
}
}
static byte[] oct(byte[] in) {
return new DerOutputStream().putOctetString(in).toByteArray();
}
static void sigVerTest(JSONValue kat, Provider p) throws Exception {
var s = p == null
? Signature.getInstance("ML-DSA")

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -20,9 +20,11 @@
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
import com.sun.crypto.provider.ML_KEM_Impls;
import jdk.test.lib.Asserts;
import jdk.test.lib.json.JSONValue;
import jdk.test.lib.security.FixedSecureRandom;
import sun.security.util.DerOutputStream;
import javax.crypto.KEM;
import java.security.*;
@ -65,13 +67,14 @@ public class ML_KEM_Test {
System.out.println(">> " + pname);
for (var c : t.get("tests").asArray()) {
System.out.print(c.get("tcId").asString() + " ");
g.initialize(np, new FixedSecureRandom(
toByteArray(c.get("d").asString()), toByteArray(c.get("z").asString())));
var seed = toByteArray(c.get("d").asString() + c.get("z").asString());
g.initialize(np, new FixedSecureRandom(seed));
var kp = g.generateKeyPair();
var pk = f.getKeySpec(kp.getPublic(), EncodedKeySpec.class).getEncoded();
var sk = f.getKeySpec(kp.getPrivate(), EncodedKeySpec.class).getEncoded();
Asserts.assertEqualsByteArray(toByteArray(c.get("ek").asString()), pk);
Asserts.assertEqualsByteArray(toByteArray(c.get("dk").asString()), sk);
Asserts.assertEqualsByteArray(
toByteArray(c.get("dk").asString()),
ML_KEM_Impls.seedToExpanded(pname, seed));
}
System.out.println();
}
@ -106,7 +109,7 @@ public class ML_KEM_Test {
var dk = new PrivateKey() {
public String getAlgorithm() { return pname; }
public String getFormat() { return "RAW"; }
public byte[] getEncoded() { return toByteArray(t.get("dk").asString()); }
public byte[] getEncoded() { return oct(toByteArray(t.get("dk").asString())); }
};
for (var c : t.get("tests").asArray()) {
System.out.print(c.get("tcId").asString() + " ");
@ -118,4 +121,8 @@ public class ML_KEM_Test {
}
}
}
static byte[] oct(byte[] in) {
return new DerOutputStream().putOctetString(in).toByteArray();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -23,11 +23,12 @@
/*
* @test
* @bug 8340327
* @bug 8340327 8347938
* @modules java.base/sun.security.ec.ed
* java.base/sun.security.ec.point
* java.base/sun.security.jca
* java.base/sun.security.provider
* java.base/sun.security.util
* @library /test/lib
*/
@ -40,7 +41,10 @@ import sun.security.jca.JCAUtil;
import sun.security.provider.NamedKeyFactory;
import sun.security.provider.NamedKeyPairGenerator;
import sun.security.provider.NamedSignature;
import sun.security.util.DerOutputStream;
import sun.security.util.DerValue;
import java.io.IOException;
import java.security.*;
import java.security.spec.EdDSAParameterSpec;
import java.security.spec.NamedParameterSpec;
@ -66,11 +70,11 @@ public class NamedEdDSA {
public static class EdDSASignature extends NamedSignature {
public EdDSASignature() {
super("EdDSA", "Ed25519", "Ed448");
super("EdDSA", new EdDSAKeyFactory());
}
protected EdDSASignature(String pname) {
super("EdDSA", pname);
super("EdDSA", new EdDSAKeyFactory(pname));
}
public static class Ed25519 extends EdDSASignature {
@ -86,22 +90,32 @@ public class NamedEdDSA {
}
@Override
public byte[] implSign(String name, byte[] sk, Object sk2, byte[] msg, SecureRandom sr) throws SignatureException {
return getOps(name).sign(plain, sk, msg);
public byte[] implSign(String pname, byte[] sk, Object sk2, byte[] msg, SecureRandom sr) {
return getOps(pname).sign(plain, sk, msg);
}
@Override
public boolean implVerify(String name, byte[] pk, Object pk2, byte[] msg, byte[] sig) throws SignatureException {
return getOps(name).verify(plain, (AffinePoint) pk2, pk, msg, sig);
public boolean implVerify(String pname, byte[] pk, Object pk2, byte[] msg, byte[] sig) throws SignatureException {
return getOps(pname).verify(plain, (AffinePoint) pk2, pk, msg, sig);
}
@Override
public Object implCheckPublicKey(String name, byte[] pk) throws InvalidKeyException {
return getOps(name).decodeAffinePoint(InvalidKeyException::new, pk);
public Object implCheckPublicKey(String pname, byte[] pk) throws InvalidKeyException {
return getOps(pname).decodeAffinePoint(InvalidKeyException::new, pk);
}
}
public static class EdDSAKeyFactory extends NamedKeyFactory {
@Override
protected byte[] implExpand(String pname, byte[] input)
throws InvalidKeyException {
try {
return new DerValue(input).getOctetString();
} catch (IOException e) {
throw new InvalidKeyException(e);
}
}
public EdDSAKeyFactory() {
super("EdDSA", "Ed25519", "Ed448");
}
@ -157,7 +171,10 @@ public class NamedEdDSA {
// set the high-order bit of the encoded point
byte msb = (byte) (point.isXOdd() ? 0x80 : 0);
encodedPoint[encodedPoint.length - 1] |= msb;
return new byte[][] { encodedPoint, sk };
return new byte[][] {
encodedPoint,
new DerOutputStream().putOctetString(sk).toByteArray(),
sk};
}
private static void swap(byte[] arr, int i, int j) {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2024, 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -23,7 +23,7 @@
/*
* @test
* @bug 8340327
* @bug 8340327 8347938
* @modules java.base/sun.security.x509
* java.base/sun.security.pkcs
* java.base/sun.security.provider
@ -41,10 +41,13 @@ import sun.security.x509.NamedX509Key;
import java.security.*;
import java.security.spec.*;
import java.util.Arrays;
public class NamedKeyFactoryTest {
private static final SeededSecureRandom RAND = SeededSecureRandom.one();
private static final byte[] RAW_SK = RAND.nBytes(16);
private static final byte[] RAW_PK = RAND.nBytes(16);
public static void main(String[] args) throws Exception {
Security.addProvider(new ProviderImpl());
@ -78,8 +81,8 @@ public class NamedKeyFactoryTest {
g.initialize(new NamedParameterSpec("ShA-256"));
checkKeyPair(g.generateKeyPair(), "SHA", "SHA-256");
var pk = new NamedX509Key("sHa", "ShA-256", RAND.nBytes(2));
var sk = new NamedPKCS8Key("sHa", "SHa-256", RAND.nBytes(2));
var pk = new NamedX509Key("sHa", "ShA-256", RAW_PK);
var sk = NamedPKCS8Key.internalCreate("sHa", "SHa-256", RAW_SK, null);
checkKey(pk, "sHa", "ShA-256");
checkKey(sk, "sHa", "SHa-256");
@ -134,25 +137,27 @@ public class NamedKeyFactoryTest {
Asserts.assertEquals("RAW", srk2.getFormat());
Asserts.assertEqualsByteArray(srk2.getEncoded(), sk.getRawBytes());
checkKey(kf2.generatePrivate(srk), "SHA", "SHA-256");
Asserts.assertEqualsByteArray(kf2.generatePrivate(srk).getEncoded(), sk.getEncoded());
Utils.runAndCheckException(() -> kf.generatePrivate(srk), InvalidKeySpecException.class); // no pname
checkKey(kf2.generatePrivate(srk), "SHA", "SHA-256");
Asserts.assertEqualsByteArray(kf2.generatePrivate(srk2).getEncoded(), sk.getEncoded());
Utils.runAndCheckException(() -> kf.generatePrivate(srk2), InvalidKeySpecException.class); // no pname
var pk1 = new PublicKey() {
public String getAlgorithm() { return "SHA"; }
public String getFormat() { return "RAW"; }
public byte[] getEncoded() { return RAND.nBytes(2); }
public byte[] getEncoded() { return RAW_PK; }
};
var pk2 = new PublicKey() {
public String getAlgorithm() { return "sHA-256"; }
public String getFormat() { return "RAW"; }
public byte[] getEncoded() { return RAND.nBytes(2); }
public byte[] getEncoded() { return RAW_PK; }
};
var pk3 = new PublicKey() {
public String getAlgorithm() { return "SHA"; }
public String getFormat() { return "RAW"; }
public byte[] getEncoded() { return RAND.nBytes(2); }
public byte[] getEncoded() { return RAW_PK; }
public AlgorithmParameterSpec getParams() { return new NamedParameterSpec("sHA-256"); }
};
@ -167,17 +172,17 @@ public class NamedKeyFactoryTest {
var sk1 = new PrivateKey() {
public String getAlgorithm() { return "SHA"; }
public String getFormat() { return "RAW"; }
public byte[] getEncoded() { return RAND.nBytes(2); }
public byte[] getEncoded() { return RAW_SK; }
};
var sk2 = new PrivateKey() {
public String getAlgorithm() { return "sHA-256"; }
public String getFormat() { return "RAW"; }
public byte[] getEncoded() { return RAND.nBytes(2); }
public byte[] getEncoded() { return RAW_SK; }
};
var sk3 = new PrivateKey() {
public String getAlgorithm() { return "SHA"; }
public String getFormat() { return "RAW"; }
public byte[] getEncoded() { return RAND.nBytes(2); }
public byte[] getEncoded() { return RAW_SK; }
public AlgorithmParameterSpec getParams() { return new NamedParameterSpec("sHA-256"); }
};
@ -201,6 +206,14 @@ public class NamedKeyFactoryTest {
if (k instanceof AsymmetricKey ak && ak.getParams() instanceof NamedParameterSpec nps) {
Asserts.assertEquals(pname, nps.getName());
}
if (k instanceof NamedPKCS8Key nsk) {
var raw = nsk.getRawBytes();
Asserts.assertEqualsByteArray(Arrays.copyOf(RAW_SK, raw.length), raw);
}
if (k instanceof NamedX509Key npk) {
var raw = npk.getRawBytes();
Asserts.assertEqualsByteArray(Arrays.copyOf(RAW_PK, raw.length), raw);
}
}
// Provider
@ -220,15 +233,24 @@ public class NamedKeyFactoryTest {
public KF() {
super("SHA", "SHA-256", "SHA-512");
}
}
public static class KF1 extends NamedKeyFactory {
public KF1() {
super("SHA", "SHA-256");
public KF(String name) {
super("SHA", name);
}
@Override
protected byte[] implExpand(String pname, byte[] input) throws InvalidKeyException {
return null;
}
}
public static class KF2 extends NamedKeyFactory {
public static class KF1 extends KF {
public KF1() {
super("SHA-256");
}
}
public static class KF2 extends KF {
public KF2() {
super("SHA", "SHA-512");
super("SHA-512");
}
}
public static class KPG extends NamedKeyPairGenerator {
@ -243,8 +265,8 @@ public class NamedKeyFactoryTest {
@Override
public byte[][] implGenerateKeyPair(String name, SecureRandom sr) {
var out = new byte[2][];
out[0] = RAND.nBytes(name.endsWith("256") ? 2 : 4);
out[1] = RAND.nBytes(name.endsWith("256") ? 2 : 4);
out[0] = name.endsWith("256") ? Arrays.copyOf(RAW_PK, 8) : RAW_PK;
out[1] = name.endsWith("256") ? Arrays.copyOf(RAW_SK, 8) : RAW_SK;
return out;
}
}

View File

@ -0,0 +1,103 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8347938
* @modules java.base/sun.security.pkcs
* java.base/sun.security.x509
* @library /test/lib
* @summary check the Named***Key behavior
*/
import jdk.test.lib.Asserts;
import jdk.test.lib.security.SeededSecureRandom;
import sun.security.pkcs.NamedPKCS8Key;
import sun.security.x509.NamedX509Key;
import java.util.Arrays;
public class NamedKeys {
public static void main(String[] args) throws Exception {
// This test uses fictional key algorithms SHA and SHA-256,
// simply because they look like a family name and parameter
// set name and SHA-256 already have its OID defined.
var r = SeededSecureRandom.one();
var raw = r.nBytes(32);
// Create a key using raw bytes
var sk = NamedPKCS8Key.internalCreate("SHA", "SHA-256", raw, null);
var enc = sk.getEncoded();
// The raw bytes array is re-used
Asserts.assertTrue(sk.getRawBytes() == sk.getRawBytes());
// but the encoding is different
Asserts.assertTrue(sk.getEncoded() != sk.getEncoded());
// When source change
Arrays.fill(raw, (byte)0);
// Internal raw bytes also changes
Asserts.assertEqualsByteArray(sk.getRawBytes(), new byte[32]);
// No guarantee on getEncoded() output, could be cached
// Create a key using encoding
var sk1 = new NamedPKCS8Key("SHA", enc, null);
var sk2 = new NamedPKCS8Key("SHA", enc, null);
var raw1 = sk1.getRawBytes();
Asserts.assertTrue(raw1 != sk2.getRawBytes());
Asserts.assertTrue(sk1.getEncoded() != sk2.getEncoded());
var encCopy = enc.clone(); // store a copy
Arrays.fill(enc, (byte)0); // clean the source and the key unchanged
Asserts.assertEqualsByteArray(encCopy, sk1.getEncoded());
// Same with public key
// Create a key using raw bytes
raw = r.nBytes(32);
var pk = new NamedX509Key("SHA", "SHA-256", raw);
enc = pk.getEncoded().clone();
// The raw bytes array is re-used
Asserts.assertTrue(pk.getRawBytes() == pk.getRawBytes());
// but the encoding is different
Asserts.assertTrue(pk.getEncoded() != pk.getEncoded());
// When source change
Arrays.fill(raw, (byte)0);
// Internal raw bytes also changes
Asserts.assertEqualsByteArray(pk.getRawBytes(), new byte[32]);
// No guarantee on getEncoded() output, could be cached
// Create a key using encoding
var pk1 = new NamedX509Key("SHA", enc);
var pk2 = new NamedX509Key("SHA", enc);
raw1 = pk1.getRawBytes();
Asserts.assertTrue(raw1 != pk2.getRawBytes());
Asserts.assertTrue(pk1.getEncoded() != pk2.getEncoded());
encCopy = enc.clone(); // store a copy
Arrays.fill(enc, (byte)0); // clean the source and the key unchanged
Asserts.assertEqualsByteArray(encCopy, pk1.getEncoded());
}
}

View File

@ -0,0 +1,227 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8347938
* @library /test/lib
* @summary ensure ML-KEM and ML-DSA encodings consistent with
* draft-ietf-lamps-kyber-certificates-11 and RFC 9881
* @modules java.base/com.sun.crypto.provider
* java.base/sun.security.pkcs
* java.base/sun.security.provider
* @run main/othervm PrivateKeyEncodings
*/
import com.sun.crypto.provider.ML_KEM_Impls;
import jdk.test.lib.Asserts;
import jdk.test.lib.security.RepositoryFileReader;
import jdk.test.lib.security.FixedSecureRandom;
import sun.security.pkcs.NamedPKCS8Key;
import sun.security.provider.ML_DSA_Impls;
import javax.crypto.KEM;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.cert.CertificateFactory;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.NamedParameterSpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.stream.Collectors;
public class PrivateKeyEncodings {
public static void main(String[] args) throws Exception {
// Example keys and certificates draft-ietf-lamps-kyber-certificates-11, Appendix B
// (https://datatracker.ietf.org/doc/html/draft-ietf-lamps-kyber-certificates-11#autoid-17)
// and RFC 9881, Appendix C.3
// (https://datatracker.ietf.org/doc/html/rfc9881#name-example-certificates)
//
// These data can be retrieved from the following GitHub releases:
// https://github.com/lamps-wg/kyber-certificates/releases/tag/draft-ietf-lamps-kyber-certificates-11
// https://github.com/lamps-wg/dilithium-certificates/releases/tag/draft-ietf-lamps-dilithium-certificates-13
//
// Although the release tags include "draft", these values are the
// same as those in the final RFC 9881.
try (var kemReader = RepositoryFileReader.of(RepositoryFileReader.KYBER_CERTIFICATES.class,
"kyber-certificates-draft-ietf-lamps-kyber-certificates-11/");
var dsaReader = RepositoryFileReader.of(RepositoryFileReader.DILITHIUM_CERTIFICATES.class,
"dilithium-certificates-draft-ietf-lamps-dilithium-certificates-13/")) {
good(kemReader, dsaReader);
badkem(kemReader);
baddsa(dsaReader);
}
}
static void badkem(RepositoryFileReader f) throws Exception {
var kf = KeyFactory.getInstance("ML-KEM");
// The first ML-KEM-512-PrivateKey example includes the both CHOICE,
// i.e., both seed and expandedKey are included. The seed and expanded
// values can be checked for inconsistencies.
Asserts.assertThrows(InvalidKeySpecException.class, () ->
kf.generatePrivate(new PKCS8EncodedKeySpec(
readData(f, "example/bad-ML-KEM-512-1.priv"))));
// The second ML-KEM-512-PrivateKey example includes only expandedKey.
// The expanded private key has a mutated s_0 and a valid public key hash,
// but a pairwise consistency check would find that the public key
// fails to match private.
var k2 = kf.generatePrivate(new PKCS8EncodedKeySpec(
readData(f, "example/bad-ML-KEM-512-2.priv")));
var pk2 = ML_KEM_Impls.privKeyToPubKey((NamedPKCS8Key) k2);
var enc = KEM.getInstance("ML-KEM").newEncapsulator(pk2).encapsulate();
var dk = KEM.getInstance("ML-KEM").newDecapsulator(k2).decapsulate(enc.encapsulation());
Asserts.assertNotEqualsByteArray(enc.key().getEncoded(), dk.getEncoded());
// The third ML-KEM-512-PrivateKey example includes only expandedKey.
// The expanded private key has a mutated H(ek); both a public key
// digest check and a pairwise consistency check should fail.
var k3 = kf.generatePrivate(new PKCS8EncodedKeySpec(
readData(f, "example/bad-ML-KEM-512-3.priv")));
Asserts.assertThrows(InvalidKeyException.class, () ->
KEM.getInstance("ML-KEM").newDecapsulator(k3));
// The fourth ML-KEM-512-PrivateKey example includes the both CHOICE,
// i.e., both seed and expandedKey are included. There is mismatch
// of the seed and expanded private key in only the z implicit rejection
// secret; here the private and public vectors match and the pairwise
// consistency check passes, but z is different.
Asserts.assertThrows(InvalidKeySpecException.class, () ->
kf.generatePrivate(new PKCS8EncodedKeySpec(
readData(f, "example/bad-ML-KEM-512-4.priv"))));
}
static void baddsa(RepositoryFileReader f) throws Exception {
var kf = KeyFactory.getInstance("ML-DSA");
// The first ML-DSA-PrivateKey example includes the both CHOICE, i.e.,
// both seed and expandedKey are included. The seed and expanded values
// can be checked for inconsistencies.
Asserts.assertThrows(InvalidKeySpecException.class, () ->
kf.generatePrivate(new PKCS8EncodedKeySpec(
readData(f, "examples/bad-ML-DSA-44-1.priv"))));
// The second ML-DSA-PrivateKey example includes only expandedKey.
// The public key fails to match the tr hash value in the private key.
var k2 = kf.generatePrivate(new PKCS8EncodedKeySpec(
readData(f, "examples/bad-ML-DSA-44-2.priv")));
Asserts.assertThrows(IllegalArgumentException.class, () ->
ML_DSA_Impls.privKeyToPubKey((NamedPKCS8Key) k2));
// The third ML-DSA-PrivateKey example also includes only expandedKey.
// The private s_1 and s_2 vectors imply a t vector whose private low
// bits do not match the t_0 vector portion of the private key
// (its high bits t_1 are the primary content of the public key).
var k3 = kf.generatePrivate(new PKCS8EncodedKeySpec(
readData(f, "examples/bad-ML-DSA-44-3.priv")));
Asserts.assertThrows(IllegalArgumentException.class, () ->
ML_DSA_Impls.privKeyToPubKey((NamedPKCS8Key) k3));
}
static void good(RepositoryFileReader kemReader, RepositoryFileReader dsaReader)
throws Exception {
var seed = new byte[64];
for (var i = 0; i < seed.length; i++) {
seed[i] = (byte) i;
}
var cf = CertificateFactory.getInstance("X.509");
var allPublicKeys = new HashMap<String, PublicKey>();
for (var pname: List.of("ML-DSA-44", "ML-DSA-65", "ML-DSA-87", // DSA first, will sign KEM
"ML-KEM-512", "ML-KEM-768", "ML-KEM-1024")) {
var isKem = pname.startsWith("ML-KEM");
KeyPairGenerator g = KeyPairGenerator.getInstance(isKem ? "ML-KEM" : "ML-DSA");
var prop = isKem ? "mlkem" : "mldsa";
var f = isKem ? kemReader : dsaReader;
var example = isKem ? "example/" : "examples/";
g.initialize(new NamedParameterSpec(pname), new FixedSecureRandom(seed));
var pk = g.generateKeyPair().getPublic();
allPublicKeys.put(pname, pk);
Asserts.assertEqualsByteArray(readData(f, example + pname + ".pub"), pk.getEncoded());
var in = new ByteArrayInputStream(readData(f, example + pname + ".crt"));
var c = cf.generateCertificate(in);
var signer = switch (pname) {
case "ML-KEM-512" -> allPublicKeys.get("ML-DSA-44");
case "ML-KEM-768" -> allPublicKeys.get("ML-DSA-65");
case "ML-KEM-1024" -> allPublicKeys.get("ML-DSA-87");
default -> c.getPublicKey();
};
c.verify(signer);
Asserts.assertEquals(c.getPublicKey(), pk);
for (var type : List.of("seed", "expandedkey", "both")) {
System.err.println(pname + " " + type);
System.setProperty("jdk." + prop + ".pkcs8.encoding", type);
g.initialize(new NamedParameterSpec(pname), new FixedSecureRandom(seed));
var sk = g.generateKeyPair().getPrivate();
if (type.equals("expandedkey")) type = "expanded";
Asserts.assertEqualsByteArray(
readData(f, example + pname + "-" + type + ".priv"), sk.getEncoded());
checkInterop(pk, sk);
}
}
}
// Ensures pk and sk interop with each other
static void checkInterop(PublicKey pk, PrivateKey sk) throws Exception {
if (pk.getAlgorithm().startsWith("ML-KEM")) {
var kem = KEM.getInstance("ML-KEM");
var enc = kem.newEncapsulator(pk).encapsulate();
var k = kem.newDecapsulator(sk).decapsulate(enc.encapsulation());
Asserts.assertEqualsByteArray(k.getEncoded(), enc.key().getEncoded());
} else {
var msg = "hello".getBytes(StandardCharsets.UTF_8);
var s = Signature.getInstance("ML-DSA");
s.initSign(sk);
s.update(msg);
var sig = s.sign();
s.initVerify(pk);
s.update(msg);
Asserts.assertTrue(s.verify(sig));
}
}
static byte[] readData(RepositoryFileReader f, String entry) throws Exception {
byte[] data = f.read(entry);
var pem = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(data)))
.lines()
.filter(s -> !s.contains("-----"))
.collect(Collectors.joining());
return Base64.getMimeDecoder().decode(pem);
}
}

View File

@ -0,0 +1,194 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
/*
* @test
* @bug 8347938
* @library /test/lib
* @modules java.base/com.sun.crypto.provider
* java.base/sun.security.pkcs
* java.base/sun.security.provider
* java.base/sun.security.util
* java.base/sun.security.x509
* @summary check key reading compatibility
* @run main/othervm SeedOrExpanded
*/
import com.sun.crypto.provider.ML_KEM_Impls;
import jdk.test.lib.Asserts;
import jdk.test.lib.security.DerUtils;
import jdk.test.lib.security.FixedSecureRandom;
import jdk.test.lib.security.SeededSecureRandom;
import sun.security.pkcs.NamedPKCS8Key;
import sun.security.provider.ML_DSA_Impls;
import sun.security.util.DerValue;
import javax.crypto.KEM;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.KeyFactory;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
public class SeedOrExpanded {
static final SeededSecureRandom RAND = SeededSecureRandom.one();
public static void main(String[] args) throws Exception {
test("mlkem", "ML-KEM-768");
test("mldsa", "ML-DSA-65");
}
static void test(String type, String alg) throws Exception {
var seed = RAND.nBytes(alg.contains("ML-KEM") ? 64 : 32);
var g = KeyPairGenerator.getInstance(alg);
// Generation
g.initialize(-1, new FixedSecureRandom(seed));
var kp = g.generateKeyPair();
var pk = kp.getPublic();
var kDefault = kp.getPrivate();
// Property value is case-insensitive
System.setProperty("jdk." + type + ".pkcs8.encoding", "SEED");
g.initialize(-1, new FixedSecureRandom(seed));
var kSeed = g.generateKeyPair().getPrivate();
System.setProperty("jdk." + type + ".pkcs8.encoding", "expandedkey");
g.initialize(-1, new FixedSecureRandom(seed));
var kExpanded = g.generateKeyPair().getPrivate();
System.setProperty("jdk." + type + ".pkcs8.encoding", "boTH");
g.initialize(-1, new FixedSecureRandom(seed));
var kBoth = g.generateKeyPair().getPrivate();
// Invalid property value
System.setProperty("jdk." + type + ".pkcs8.encoding", "bogus");
g.initialize(-1, new FixedSecureRandom(seed));
Asserts.assertThrows(IllegalArgumentException.class,
() -> g.generateKeyPair().getPrivate());
byte[] kExpandedEncoded = kExpanded.getEncoded();
byte[] kSeedEncoded = kSeed.getEncoded();
byte[] kBothEncoded = kBoth.getEncoded();
// Ensure tags match the CHOICE definition
Asserts.assertEquals((byte) 0x80, DerUtils.innerDerValue(kSeedEncoded, "2c").tag);
Asserts.assertEquals((byte) 0x04, DerUtils.innerDerValue(kExpandedEncoded, "2c").tag);
Asserts.assertEquals((byte) 0x30, DerUtils.innerDerValue(kBothEncoded, "2c").tag);
byte[] seedData = DerUtils.innerDerValue(kSeedEncoded, "2c")
.withTag(DerValue.tag_OctetString).getOctetString();
byte[] expandedData = DerUtils.innerDerValue(kExpandedEncoded, "2c").getOctetString();
Asserts.assertEqualsByteArray(seed, seedData);
Asserts.assertEqualsByteArray(
seedData,
DerUtils.innerDerValue(kBothEncoded, "2c0").getOctetString());
Asserts.assertEqualsByteArray(
expandedData,
DerUtils.innerDerValue(kBothEncoded, "2c1").getOctetString());
// Ensure seedToExpanded correctly called
if (alg.contains("ML-KEM")) {
Asserts.assertEqualsByteArray(expandedData,
ML_KEM_Impls.seedToExpanded(alg, seedData));
} else {
Asserts.assertEqualsByteArray(expandedData,
ML_DSA_Impls.seedToExpanded(alg, seedData));
}
test(alg, pk, kSeed);
test(alg, pk, kExpanded);
test(alg, pk, kBoth);
var kf = KeyFactory.getInstance(alg);
System.setProperty("jdk." + type + ".pkcs8.encoding", "seed");
Asserts.assertEqualsByteArray(
test(alg, pk, kf.translateKey(kBoth)).getEncoded(),
kSeedEncoded);
Asserts.assertTrue(kf.translateKey(kSeed) == kSeed);
Asserts.assertThrows(InvalidKeyException.class, () -> kf.translateKey(kExpanded));
System.setProperty("jdk." + type + ".pkcs8.encoding", "expandedkey");
Asserts.assertEqualsByteArray(
test(alg, pk, kf.translateKey(kBoth)).getEncoded(),
kExpandedEncoded);
Asserts.assertEqualsByteArray(
test(alg, pk, kf.translateKey(kSeed)).getEncoded(),
kExpandedEncoded);
Asserts.assertTrue(kf.translateKey(kExpanded) == kExpanded);
System.setProperty("jdk." + type + ".pkcs8.encoding", "both");
Asserts.assertTrue(kf.translateKey(kBoth) == kBoth);
Asserts.assertEqualsByteArray(
test(alg, pk, kf.translateKey(kSeed)).getEncoded(),
kBothEncoded);
Asserts.assertThrows(InvalidKeyException.class, () -> kf.translateKey(kExpanded));
// The following makes sure key is not mistakenly cleaned during
// translations.
var xk = new PrivateKey() {
public String getAlgorithm() { return alg; }
public String getFormat() { return "PKCS#8"; }
public byte[] getEncoded() { return kBothEncoded.clone(); }
};
test(alg, pk, xk);
var xk2 = (PrivateKey) kf.translateKey(xk);
test(alg, pk, xk2);
test(alg, pk, xk);
}
static PrivateKey test(String alg, PublicKey pk, Key k) throws Exception {
var sk = (PrivateKey) k;
if (alg.contains("ML-KEM")) {
var kem = KEM.getInstance("ML-KEM");
var e = kem.newEncapsulator(pk, RAND);
var enc = e.encapsulate();
var k1 = kem.newDecapsulator(sk).decapsulate(enc.encapsulation());
Asserts.assertEqualsByteArray(k1.getEncoded(), enc.key().getEncoded());
if (k instanceof NamedPKCS8Key npk) {
Asserts.assertEqualsByteArray(
ML_KEM_Impls.privKeyToPubKey(npk).getEncoded(), pk.getEncoded());
}
} else {
var s = Signature.getInstance("ML-DSA");
var rnd = RAND.nBytes(32); // randomness for signature generation
var msg = RAND.nBytes(20);
s.initSign(sk, new FixedSecureRandom(rnd));
s.update(msg);
var sig1 = s.sign();
s.initVerify(pk);
s.update(msg);
Asserts.assertTrue(s.verify(sig1));
if (k instanceof NamedPKCS8Key npk) {
Asserts.assertEqualsByteArray(
ML_DSA_Impls.privKeyToPubKey(npk).getEncoded(), pk.getEncoded());
}
}
return sk;
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2013, 2024, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2013, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@ -256,6 +256,15 @@ public class Proc {
}
}
}
String patchPath = System.getProperty("test.patch.path");
if (patchPath != null) {
try (var subs = Files.newDirectoryStream(Path.of(patchPath))) {
for (var sub : subs) {
var name = sub.getFileName();
cmd.add("--patch-module=" + name + "=" + sub);
}
}
}
var lcp = fullcp();
if (lcp != null) {

View File

@ -148,4 +148,13 @@ public sealed interface RepositoryFileReader extends AutoCloseable {
unpack = false)
public static class CMS_ML_DSA {
}
@Artifact(
organization = "jpg.tests.jdk.repos.lamps-wg",
name = "kyber-certificates",
revision = "29f3215",
extension = "zip",
unpack = false)
public static class KYBER_CERTIFICATES {
}
}