9fd855ed47
Reviewed-by: mullan, valeriep
511 lines
21 KiB
Java
511 lines
21 KiB
Java
/*
|
|
* Copyright (c) 2023, 2024, 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 8297878
|
|
* @summary RSA_KEM example
|
|
* @modules java.base/sun.security.jca
|
|
* java.base/sun.security.rsa
|
|
* java.base/sun.security.util
|
|
* java.base/javax.crypto:+open
|
|
*/
|
|
import sun.security.jca.JCAUtil;
|
|
import sun.security.rsa.RSACore;
|
|
import sun.security.util.*;
|
|
|
|
import javax.crypto.*;
|
|
import javax.crypto.spec.*;
|
|
import java.io.ByteArrayOutputStream;
|
|
import java.io.IOException;
|
|
import java.math.BigInteger;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.security.*;
|
|
import java.security.interfaces.RSAPrivateCrtKey;
|
|
import java.security.interfaces.RSAPrivateKey;
|
|
import java.security.interfaces.RSAPublicKey;
|
|
import java.security.spec.AlgorithmParameterSpec;
|
|
import java.security.spec.InvalidParameterSpecException;
|
|
import java.util.Arrays;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Objects;
|
|
|
|
// This test implements RSA-KEM as described in RFC 5990. In this KEM, the
|
|
// sender configures the encapsulator with an RSAKEMParameterSpec object.
|
|
// This object is encoded as a byte array and included in the Encapsulated
|
|
// output. The receiver is then able to recover the same RSAKEMParameterSpec
|
|
// object from the encoding using an AlgorithmParameters implementation
|
|
// and use the object to configure the decapsulator.
|
|
public class RSA_KEM {
|
|
public static void main(String[] args) throws Exception {
|
|
Provider p = new ProviderImpl();
|
|
RSAKEMParameterSpec[] kspecs = new RSAKEMParameterSpec[] {
|
|
RSAKEMParameterSpec.kdf1("SHA-256", "AES_128/KW/NoPadding"),
|
|
RSAKEMParameterSpec.kdf1("SHA-512", "AES_256/KW/NoPadding"),
|
|
RSAKEMParameterSpec.kdf2("SHA-256", "AES_128/KW/NoPadding"),
|
|
RSAKEMParameterSpec.kdf2("SHA-512", "AES_256/KW/NoPadding"),
|
|
RSAKEMParameterSpec.kdf3("SHA-256", new byte[10], "AES_128/KW/NoPadding"),
|
|
RSAKEMParameterSpec.kdf3("SHA-256", new byte[0], "AES_128/KW/NoPadding"),
|
|
RSAKEMParameterSpec.kdf3("SHA-512", new byte[0], "AES_128/KW/NoPadding"),
|
|
};
|
|
for (RSAKEMParameterSpec kspec : kspecs) {
|
|
System.err.println("---------");
|
|
System.err.println(kspec);
|
|
AlgorithmParameters d = AlgorithmParameters.getInstance("RSA-KEM", p);
|
|
d.init(kspec);
|
|
AlgorithmParameters s = AlgorithmParameters.getInstance("RSA-KEM", p);
|
|
s.init(d.getEncoded());
|
|
AlgorithmParameterSpec spec = s.getParameterSpec(AlgorithmParameterSpec.class);
|
|
if (!spec.toString().equals(kspec.toString())) {
|
|
throw new RuntimeException(spec.toString());
|
|
}
|
|
}
|
|
byte[] msg = "hello".getBytes(StandardCharsets.UTF_8);
|
|
byte[] iv = new byte[16];
|
|
for (int size : List.of(1024, 2048)) {
|
|
KeyPairGenerator g = KeyPairGenerator.getInstance("RSA");
|
|
g.initialize(size);
|
|
KeyPair kp = g.generateKeyPair();
|
|
for (RSAKEMParameterSpec kspec : kspecs) {
|
|
SecretKey cek = KeyGenerator.getInstance("AES").generateKey();
|
|
KEM kem1 = getKemImpl(p);
|
|
Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
c.init(Cipher.ENCRYPT_MODE, cek, new IvParameterSpec(iv));
|
|
byte[] ciphertext = c.doFinal(msg);
|
|
|
|
KEM.Encapsulator e = kem1.newEncapsulator(kp.getPublic(), kspec, null);
|
|
KEM.Encapsulated enc = e.encapsulate(0, e.secretSize(), "AES");
|
|
Cipher c2 = Cipher.getInstance(kspec.encAlg);
|
|
c2.init(Cipher.WRAP_MODE, enc.key());
|
|
byte[] ek = c2.wrap(cek);
|
|
|
|
AlgorithmParameters a = AlgorithmParameters.getInstance("RSA-KEM", p);
|
|
a.init(enc.params());
|
|
KEM kem2 = getKemImpl(p);
|
|
KEM.Decapsulator d = kem2.newDecapsulator(kp.getPrivate(), a.getParameterSpec(AlgorithmParameterSpec.class));
|
|
SecretKey k = d.decapsulate(enc.encapsulation(), 0, d.secretSize(), "AES");
|
|
Cipher c3 = Cipher.getInstance(kspec.encAlg);
|
|
c3.init(Cipher.UNWRAP_MODE, k);
|
|
cek = (SecretKey) c3.unwrap(ek, "AES", Cipher.SECRET_KEY);
|
|
Cipher c4 = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
|
c4.init(Cipher.DECRYPT_MODE, cek, new IvParameterSpec(iv));
|
|
byte[] cleartext = c4.doFinal(ciphertext);
|
|
|
|
if (!Arrays.equals(cleartext, msg)) {
|
|
throw new RuntimeException();
|
|
}
|
|
System.out.printf("%4d %20s - %11d %11d %11d %11d %s\n",
|
|
size, kspec,
|
|
e.secretSize(), e.encapsulationSize(),
|
|
d.secretSize(), d.encapsulationSize(), k.getAlgorithm());
|
|
}
|
|
}
|
|
}
|
|
|
|
// To bypass the JCE security provider signature check
|
|
private static KEM getKemImpl(Provider p) throws Exception {
|
|
var ctor = KEM.class.getDeclaredConstructor(
|
|
String.class, KEMSpi.class, Provider.class);
|
|
ctor.setAccessible(true);
|
|
return ctor.newInstance("RSA-KEM", new KEMImpl(), p);
|
|
}
|
|
|
|
static final String RSA_KEM = "1.2.840.113549.1.9.16.3.14";
|
|
static final String KEM_RSA = "1.0.18033.2.2.4";
|
|
|
|
public static class ProviderImpl extends Provider {
|
|
public ProviderImpl() {
|
|
super("MYKEM", "1", "RSA-KEM");
|
|
List<String> alias = List.of(RSA_KEM, "OID." + RSA_KEM);
|
|
Map<String, String> attrs = Map.of(
|
|
"SupportedKeyClasses", "java.security.interfaces.RSAKey");
|
|
putService(new Service(this, "KEM", "RSA-KEM",
|
|
"RSA_KEM$KEMImpl", alias, attrs));
|
|
putService(new Service(this, "AlgorithmParameters", "RSA-KEM",
|
|
"RSA_KEM$AlgorithmParametersImpl", alias, attrs));
|
|
}
|
|
}
|
|
|
|
public static class AlgorithmParametersImpl extends AlgorithmParametersSpi {
|
|
RSAKEMParameterSpec spec;
|
|
@Override
|
|
protected void engineInit(AlgorithmParameterSpec paramSpec)
|
|
throws InvalidParameterSpecException {
|
|
if (paramSpec instanceof RSAKEMParameterSpec rspec) {
|
|
spec = rspec;
|
|
} else {
|
|
throw new InvalidParameterSpecException();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void engineInit(byte[] params) throws IOException {
|
|
spec = decode(params);
|
|
}
|
|
|
|
@Override
|
|
protected void engineInit(byte[] params, String format) throws IOException {
|
|
spec = decode(params);
|
|
}
|
|
|
|
@Override
|
|
protected <T extends AlgorithmParameterSpec> T engineGetParameterSpec(
|
|
Class<T> paramSpec) throws InvalidParameterSpecException {
|
|
if (paramSpec.isAssignableFrom(RSAKEMParameterSpec.class)) {
|
|
return paramSpec.cast(spec);
|
|
} else {
|
|
throw new InvalidParameterSpecException();
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected byte[] engineGetEncoded() {
|
|
return encode(spec);
|
|
}
|
|
|
|
@Override
|
|
protected byte[] engineGetEncoded(String format) {
|
|
return encode(spec);
|
|
}
|
|
|
|
@Override
|
|
protected String engineToString() {
|
|
return spec == null ? "<null>" : spec.toString();
|
|
}
|
|
|
|
static final ObjectIdentifier id_rsa_kem;
|
|
static final ObjectIdentifier id_kem_rsa;
|
|
static final ObjectIdentifier id_kdf1;
|
|
static final ObjectIdentifier id_kdf2;
|
|
static final ObjectIdentifier id_kdf3;
|
|
|
|
static {
|
|
try {
|
|
id_rsa_kem = ObjectIdentifier.of("1.2.840.113549.1.9.16.3.14");
|
|
id_kem_rsa = ObjectIdentifier.of("1.0.18033.2.2.4");
|
|
id_kdf1 = ObjectIdentifier.of("1.3.133.16.840.9.44.1.0"); // fake
|
|
id_kdf2 = ObjectIdentifier.of("1.3.133.16.840.9.44.1.1");
|
|
id_kdf3 = ObjectIdentifier.of("1.3.133.16.840.9.44.1.2");
|
|
} catch (IOException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
}
|
|
|
|
static byte[] encode(RSAKEMParameterSpec spec) {
|
|
DerOutputStream kdf = new DerOutputStream()
|
|
.write(DerValue.tag_Sequence, new DerOutputStream()
|
|
.putOID(oid4(spec.kdfAlg))
|
|
.write(DerValue.tag_Sequence, new DerOutputStream()
|
|
.putOID(oid4(spec.hashAlg))))
|
|
.putInteger(spec.kdfLen());
|
|
// The next line is not in RFC 5990
|
|
if (spec.fixedInfo != null) {
|
|
kdf.putOctetString(spec.fixedInfo);
|
|
}
|
|
return new DerOutputStream()
|
|
.write(DerValue.tag_Sequence, new DerOutputStream()
|
|
.write(DerValue.tag_Sequence, new DerOutputStream()
|
|
.putOID(id_kem_rsa)
|
|
.write(DerValue.tag_Sequence, kdf))
|
|
.write(DerValue.tag_Sequence, new DerOutputStream()
|
|
.putOID(oid4(spec.encAlg)))).toByteArray();
|
|
}
|
|
|
|
static RSAKEMParameterSpec decode(byte[] der) throws IOException {
|
|
String kdfAlg, encAlg, hashAlg;
|
|
int kdfLen;
|
|
byte[] fixedInfo;
|
|
DerInputStream d2 = new DerValue(der).toDerInputStream();
|
|
DerInputStream d3 = d2.getDerValue().toDerInputStream();
|
|
if (!d3.getOID().equals(id_kem_rsa)) {
|
|
throw new IOException("not id_kem_rsa");
|
|
}
|
|
DerInputStream d4 = d3.getDerValue().toDerInputStream();
|
|
DerInputStream d5 = d4.getDerValue().toDerInputStream();
|
|
kdfLen = d4.getInteger();
|
|
fixedInfo = d4.available() > 0 ? d4.getOctetString() : null;
|
|
d4.atEnd();
|
|
ObjectIdentifier kdfOid = d5.getOID();
|
|
if (kdfOid.equals(id_kdf1)) {
|
|
kdfAlg = "kdf1";
|
|
} else if (kdfOid.equals(id_kdf2)) {
|
|
kdfAlg = "kdf2";
|
|
} else if (kdfOid.equals(id_kdf3)) {
|
|
kdfAlg = "kdf3";
|
|
} else {
|
|
throw new IOException("unknown kdf");
|
|
}
|
|
DerInputStream d6 = d5.getDerValue().toDerInputStream();
|
|
String hashOID = d6.getOID().toString();
|
|
KnownOIDs k = KnownOIDs.findMatch(hashOID);
|
|
hashAlg = k == null ? hashOID : k.stdName();
|
|
d6.atEnd();
|
|
d5.atEnd();
|
|
|
|
d3.atEnd();
|
|
DerInputStream d7 = d2.getDerValue().toDerInputStream();
|
|
String encOID = d7.getOID().toString();
|
|
KnownOIDs e = KnownOIDs.findMatch(encOID);
|
|
encAlg = e == null ? encOID : e.stdName();
|
|
d7.atEnd();
|
|
d2.atEnd();
|
|
if (kdfLen != RSAKEMParameterSpec.kdfLen(encAlg)) {
|
|
throw new IOException("kdfLen does not match encAlg");
|
|
}
|
|
return new RSAKEMParameterSpec(kdfAlg, hashAlg, fixedInfo, encAlg);
|
|
}
|
|
|
|
static ObjectIdentifier oid4(String s) {
|
|
return switch (s) {
|
|
case "kdf1" -> id_kdf1;
|
|
case "kdf2" -> id_kdf2;
|
|
case "kdf3" -> id_kdf3;
|
|
default -> {
|
|
KnownOIDs k = KnownOIDs.findMatch(s);
|
|
if (k == null) throw new UnsupportedOperationException();
|
|
yield ObjectIdentifier.of(k);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
|
|
public static class RSAKEMParameterSpec implements AlgorithmParameterSpec {
|
|
private final String kdfAlg;
|
|
private final String hashAlg;
|
|
private final byte[] fixedInfo;
|
|
private final String encAlg;
|
|
|
|
private RSAKEMParameterSpec(String kdfAlg, String hashAlg, byte[] fixedInfo, String encAlg) {
|
|
this.hashAlg = hashAlg;
|
|
this.kdfAlg = kdfAlg;
|
|
this.fixedInfo = fixedInfo == null ? null : fixedInfo.clone();
|
|
this.encAlg = encAlg;
|
|
}
|
|
|
|
public static RSAKEMParameterSpec kdf1(String hashAlg, String encAlg) {
|
|
return new RSAKEMParameterSpec("kdf1", hashAlg, null, encAlg);
|
|
}
|
|
public static RSAKEMParameterSpec kdf2(String hashAlg, String encAlg) {
|
|
return new RSAKEMParameterSpec("kdf2", hashAlg, null, encAlg);
|
|
}
|
|
public static RSAKEMParameterSpec kdf3(String hashAlg, byte[] fixedInfo, String encAlg) {
|
|
return new RSAKEMParameterSpec("kdf3", hashAlg, fixedInfo, encAlg);
|
|
}
|
|
|
|
public int kdfLen() {
|
|
return RSAKEMParameterSpec.kdfLen(encAlg);
|
|
}
|
|
|
|
public static int kdfLen(String encAlg) {
|
|
return Integer.parseInt(encAlg, 4, 7, 10) / 8;
|
|
}
|
|
|
|
public String hashAlgorithm() {
|
|
return hashAlg;
|
|
}
|
|
public String kdfAlgorithm() {
|
|
return kdfAlg;
|
|
}
|
|
public byte[] fixedInfo() {
|
|
return fixedInfo == null ? null : fixedInfo.clone();
|
|
}
|
|
|
|
public String getEncAlg() {
|
|
return encAlg;
|
|
}
|
|
|
|
@Override
|
|
public String toString() {
|
|
return String.format("[%s,%s,%s]", kdfAlg, hashAlg, encAlg);
|
|
}
|
|
}
|
|
|
|
public static class KEMImpl implements KEMSpi {
|
|
|
|
@Override
|
|
public KEMSpi.EncapsulatorSpi engineNewEncapsulator(
|
|
PublicKey pk, AlgorithmParameterSpec spec, SecureRandom secureRandom)
|
|
throws InvalidAlgorithmParameterException, InvalidKeyException {
|
|
if (!(pk instanceof RSAPublicKey rpk)) {
|
|
throw new InvalidKeyException("Not an RSA key");
|
|
}
|
|
return Handler.newEncapsulator(spec, rpk, secureRandom);
|
|
}
|
|
|
|
@Override
|
|
public KEMSpi.DecapsulatorSpi engineNewDecapsulator(
|
|
PrivateKey sk, AlgorithmParameterSpec spec)
|
|
throws InvalidAlgorithmParameterException, InvalidKeyException {
|
|
if (!(sk instanceof RSAPrivateCrtKey rsk)) {
|
|
throw new InvalidKeyException("Not an RSA key");
|
|
}
|
|
return Handler.newDecapsulator(spec, rsk);
|
|
}
|
|
|
|
static class Handler implements KEMSpi.EncapsulatorSpi, KEMSpi.DecapsulatorSpi {
|
|
|
|
private final RSAPublicKey rpk; // not null for encapsulator
|
|
private final RSAPrivateKey rsk; // not null for decapsulator
|
|
private final RSAKEMParameterSpec kspec; // not null
|
|
private final SecureRandom sr; // not null for encapsulator
|
|
|
|
Handler(AlgorithmParameterSpec spec, RSAPublicKey rpk, RSAPrivateCrtKey rsk, SecureRandom sr)
|
|
throws InvalidAlgorithmParameterException {
|
|
this.rpk = rpk;
|
|
this.rsk = rsk;
|
|
this.sr = sr;
|
|
if (spec != null) {
|
|
if (spec instanceof RSAKEMParameterSpec rs) {
|
|
this.kspec = rs;
|
|
} else {
|
|
throw new InvalidAlgorithmParameterException();
|
|
}
|
|
} else {
|
|
this.kspec = RSAKEMParameterSpec
|
|
.kdf2("SHA-256", "AES_256/KW/NoPadding");
|
|
}
|
|
}
|
|
|
|
static Handler newEncapsulator(AlgorithmParameterSpec spec, RSAPublicKey rpk, SecureRandom sr)
|
|
throws InvalidAlgorithmParameterException {
|
|
if (sr == null) {
|
|
sr = JCAUtil.getDefSecureRandom();
|
|
}
|
|
return new Handler(spec, rpk, null, sr);
|
|
}
|
|
|
|
static Handler newDecapsulator(AlgorithmParameterSpec spec, RSAPrivateCrtKey rsk)
|
|
throws InvalidAlgorithmParameterException {
|
|
return new Handler(spec, null, rsk, null);
|
|
}
|
|
|
|
@Override
|
|
public SecretKey engineDecapsulate(byte[] encapsulation,
|
|
int from, int to, String algorithm)
|
|
throws DecapsulateException {
|
|
Objects.checkFromToIndex(from, to, kspec.kdfLen());
|
|
Objects.requireNonNull(algorithm, "null algorithm");
|
|
Objects.requireNonNull(encapsulation, "null encapsulation");
|
|
if (encapsulation.length != KeyUtil.getKeySize(rsk) / 8) {
|
|
throw new DecapsulateException("incorrect encapsulation size");
|
|
}
|
|
try {
|
|
byte[] Z = RSACore.rsa(encapsulation, rsk, false);
|
|
return new SecretKeySpec(kdf(Z), from, to - from, algorithm);
|
|
} catch (BadPaddingException e) {
|
|
throw new DecapsulateException("cannot decrypt", e);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public KEM.Encapsulated engineEncapsulate(int from, int to, String algorithm) {
|
|
Objects.checkFromToIndex(from, to, kspec.kdfLen());
|
|
Objects.requireNonNull(algorithm, "null algorithm");
|
|
int nLen = rpk.getModulus().bitLength();
|
|
int nSize = (nLen + 7) / 8;
|
|
BigInteger z;
|
|
int tried = 0;
|
|
while (true) {
|
|
z = new BigInteger(nLen, sr);
|
|
if (z.compareTo(rpk.getModulus()) < 0) {
|
|
break;
|
|
}
|
|
if (tried++ > 20) {
|
|
throw new ProviderException("Cannot get good random number");
|
|
}
|
|
}
|
|
byte[] Z = z.toByteArray();
|
|
if (Z.length > nSize) {
|
|
Z = Arrays.copyOfRange(Z, Z.length - nSize, Z.length);
|
|
} else if (Z.length < nSize) {
|
|
byte[] tmp = new byte[nSize];
|
|
System.arraycopy(Z, 0, tmp, nSize - Z.length, Z.length);
|
|
Z = tmp;
|
|
}
|
|
byte[] c;
|
|
try {
|
|
c = RSACore.rsa(Z, rpk);
|
|
} catch (BadPaddingException e) {
|
|
throw new AssertionError(e);
|
|
}
|
|
return new KEM.Encapsulated(
|
|
new SecretKeySpec(kdf(Z), from, to - from, algorithm),
|
|
c, AlgorithmParametersImpl.encode(kspec));
|
|
}
|
|
|
|
byte[] kdf(byte[] input) {
|
|
String hashAlg = kspec.hashAlgorithm();
|
|
MessageDigest md;
|
|
try {
|
|
md = MessageDigest.getInstance(hashAlg);
|
|
} catch (NoSuchAlgorithmException e) {
|
|
throw new ProviderException(e);
|
|
}
|
|
String kdfAlg = kspec.kdfAlgorithm();
|
|
byte[] fixedInput = kspec.fixedInfo();
|
|
int length = kspec.kdfLen();
|
|
|
|
ByteArrayOutputStream bout = new ByteArrayOutputStream();
|
|
int n = kdfAlg.equals("kdf1") ? 0 : 1;
|
|
while (true) {
|
|
switch (kdfAlg) {
|
|
case "kdf1", "kdf2" -> {
|
|
md.update(input);
|
|
md.update(u32str(n));
|
|
}
|
|
case "kdf3" -> {
|
|
md.update(u32str(n));
|
|
md.update(input);
|
|
md.update(fixedInput);
|
|
}
|
|
default -> throw new ProviderException();
|
|
}
|
|
bout.writeBytes(md.digest());
|
|
if (bout.size() > length) break;
|
|
n++;
|
|
}
|
|
byte[] result = bout.toByteArray();
|
|
return result.length == length
|
|
? result
|
|
: Arrays.copyOf(result, length);
|
|
}
|
|
|
|
@Override
|
|
public int engineSecretSize() {
|
|
return kspec.kdfLen();
|
|
}
|
|
|
|
@Override
|
|
public int engineEncapsulationSize() {
|
|
return KeyUtil.getKeySize(rsk == null ? rpk : rsk) / 8;
|
|
}
|
|
}
|
|
}
|
|
|
|
static byte[] u32str(int i) {
|
|
return new byte[] {
|
|
(byte)(i >> 24), (byte)(i >> 16), (byte)(i >> 8), (byte)i };
|
|
}
|
|
}
|