Skip to content

Commit

Permalink
Fix java 15-18 ECDSA vulnerability.
Browse files Browse the repository at this point in the history
Add missing check of signatures's R and S after successful signature
verification.
Applies only to DTLS, not TLS. TLS requires a fixed JCE!
The fixed x509 certificate path validation is executed by the
CertPathUtil.
If other implementations are used, the
Asn1DerDecoder.checkCertificateChain must be called there.

Signed-off-by: Achim Kraus <achim.kraus@bosch.io>
  • Loading branch information
Achim Kraus committed Apr 24, 2022
1 parent 60f7b2e commit 022565f
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 14 deletions.
8 changes: 8 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,11 @@ For more details, please look at https://www.eclipse.org/security/.
| ------------------- | ---------- | ---------------- | ----- | -------------
| < 3.3 <br/> < 2.7.2 | com.upokecenter.cbor | 4.0 - 4.5.0 | cf-oscore <br/> demo-apps | [GHSA-fj2w-wfgv-mwq6](https://github.com/peteroupc/CBOR-Java/security/advisories/GHSA-fj2w-wfgv-mwq6)
| < 3.2 <br/> < 2.7.1 | ch.qos.logback.logback-classic | < 1.2.9 | demo-apps | [CVE-2021-42550](https://cve.report/CVE-2021-42550)

## Known Vulnerabilities Of Runtime Dependencies

| Californium Version | Dependency | Affected Version | Usage | Vulnerability
| ------------------- | ---------- | ---------------- | ----- | -------------
| < 3.5 | JDK / JCE | <= 15.0.2? <br/> <= 16.0.2? <br/> < 17.0.3 <br/> < 18.0.1 | execution environment | ECDSA [CVE-2022-21449](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-21449)


Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.CertPathValidatorException;
import java.security.cert.X509Certificate;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
Expand All @@ -34,6 +36,7 @@
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.List;

/**
* ASN.1 DER decoder for SEQUENCEs and OIDs.
Expand Down Expand Up @@ -365,6 +368,8 @@ public class Asn1DerDecoder {
private static final OidEntityDefinition OID = new OidEntityDefinition();
/**
* ASN.1 entity definition for INTEGER.
*
* Converts values up to 4 bytes into {@code int}.
*/
private static final IntegerEntityDefinition INTEGER = new IntegerEntityDefinition();
/**
Expand Down Expand Up @@ -917,6 +922,88 @@ public static Keys readEdDsaPrivateKeyV2(final byte[] data) throws GeneralSecuri
return keys;
}

/**
* Checks, if chain contains a vulnerable ECDSA signature.
*
* @param chain certificate chain to check.
* @param trust trusted certificate
* @param last number of certificates to check in chain.
* @throws CertPathValidatorException if signature contains INTEGER values
* not in range {@code [1, N-1]}.
* @see #checkEcDsaSignature(byte[], PublicKey)
* @since 3.5
*/
public static void checkCertificateChain(List<X509Certificate> chain, X509Certificate trust, int last)
throws CertPathValidatorException {
try {
for (int index = 0; index < last; ++index) {
X509Certificate certificate = chain.get(index);
String signatureAlgorithm = certificate.getSigAlgName();
if (signatureAlgorithm.endsWith("withECDSA") || signatureAlgorithm.endsWith("WITHECDSA")) {
X509Certificate issuerCertificate;
if (index + 1 < chain.size()) {
issuerCertificate = chain.get(index + 1);
} else {
issuerCertificate = trust;
}
Asn1DerDecoder.checkEcDsaSignature(certificate.getSignature(), issuerCertificate.getPublicKey());
}
}
} catch (GeneralSecurityException ex) {
throw new CertPathValidatorException(ex.getMessage());
}
}

/**
* Check, if provided ECDSA signature is vulnerable.
*
* Some java JCE versions 15 to 18 fail to check the signature for 0 and n.
* This method adds that check.
*
* @param signature received signature.
* @param publicKey public key to read the order (N)
* @throws GeneralSecurityException if signature contains INTEGER values not
* in range {@code [1, N-1]}.
* @see JceProviderUtil#isEcdsaVulnerable()
* @see <a href=
* "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-21449"
* target="_blank">CVE-2022-21449</a>
* @since 3.5
*/
public static void checkEcDsaSignature(byte[] signature, PublicKey publicKey) throws GeneralSecurityException {
DatagramReader reader = new DatagramReader(signature, false);
reader = SEQUENCE.createRangeReader(reader, false);
byte[] valueR = INTEGER.read(reader, false);
byte[] valueS = INTEGER.read(reader, false);
BigInteger order = ((ECPublicKey) publicKey).getParams().getOrder();
checkSignatureInteger("R", valueR, order);
checkSignatureInteger("S", valueS, order);
}

/**
* Checks, if the provided ASN.1 INTEGER is valid for a signature.
*
* @param name name of signature parameter
* @param value byte value of signature parameter
* @param order order of the public key (N)
* @throws GeneralSecurityException if the signature parameter is not in
* range {@code [1, N-1]}.
* @since 3.5
*/
private static void checkSignatureInteger(String name, byte[] value, BigInteger order)
throws GeneralSecurityException {
if (value.length == 0) {
throw new GeneralSecurityException("ECDSA signature " + name + " is 0!");
}
BigInteger big = new BigInteger(value);
if (big.compareTo(BigInteger.ONE) < 0) {
throw new GeneralSecurityException("ECDSA signature " + name + " is less than 1!");
}
if (big.compareTo(order) >= 0) {
throw new GeneralSecurityException("ECDSA signature " + name + " is not less than N!");
}
}

/**
* Check for equal key algorithm synonyms.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,9 @@ public static CertPath validateCertificatePathWithIssuer(boolean truncateCertifi
// TODO: implement alternative means of revocation checking
params.setRevocationEnabled(false);
validator.validate(verifyCertPath, params);
if (JceProviderUtil.isEcdsaVulnerable()) {
Asn1DerDecoder.checkCertificateChain(chain, trust, last);
}
if (truncated || add) {
if (add) {
if (!truncated) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public interface JceNames {
public String OID_X448 = "OID.1.3.101.111";

/**
* Name of environment variable to specify JCE,.
* Name of environment variable to specify the JCE.
*/
public String CALIFORNIUM_JCE_PROVIDER = "CALIFORNIUM_JCE_PROVIDER";
/**
Expand Down Expand Up @@ -140,5 +140,18 @@ public interface JceNames {
* for EdDSA.
*/
public String JCE_PROVIDER_NET_I2P_CRYPTO = "I2P";
/**
* Name of environment variable to specify, if the used JCE is tested for
* the ECDSA vulnerability
* <a href= "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-21449"
* target="_blank">ECDSA vulnerability, CVE-2022-21449</a>.
*
* The default is to test it. If the value of this environment variable is
* set to {@code false}, the test is suppressed and no additional checks for
* such signatures are done.
*
* @since 3.5
*/
public String CALIFORNIUM_JCE_ECDSA_FIX = "CALIFORNIUM_JCE_ECDSA_FIX";

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,19 @@

import java.lang.reflect.Method;
import java.security.AccessController;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;
import java.security.Signature;
import java.security.SignatureException;
import java.security.spec.ECGenParameterSpec;

import javax.crypto.Cipher;

Expand Down Expand Up @@ -145,6 +152,7 @@ public class JceProviderUtil {
private final boolean ed25519;
private final boolean ed448;
private final boolean strongEncryption;
private final boolean ecdsaVulnerable;
private final String providerVersion;

static {
Expand Down Expand Up @@ -408,6 +416,7 @@ private static void setupJce() {
}
boolean ec = false;
boolean rsa = false;
boolean ecdsaVulnerable = false;
String aesPermission = "not supported";
int aesMaxAllowedKeyLength = 0;
try {
Expand All @@ -432,6 +441,28 @@ private static void setupJce() {
} catch (NoSuchAlgorithmException e) {
}
LOGGER.debug("EC: {}", ec);
if (ec) {
String ecdsaFix = StringUtil.getConfiguration(JceNames.CALIFORNIUM_JCE_ECDSA_FIX);
if (ecdsaFix == null || !ecdsaFix.equalsIgnoreCase("false")) {
ecdsaVulnerable = true;
try {
Signature signature = Signature.getInstance("SHA256withECDSA");
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC");
keyPairGenerator.initialize(new ECGenParameterSpec("secp256r1"));
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// malicious signature
byte[] ghost = StringUtil.hex2ByteArray("3006020100020100");
signature.initVerify(keyPair.getPublic());
signature.update(ghost);
ecdsaVulnerable = signature.verify(ghost);
} catch (NoSuchAlgorithmException e) {
} catch (InvalidAlgorithmParameterException e) {
} catch (InvalidKeyException e) {
} catch (SignatureException e) {
}
LOGGER.debug("ECDSA {}vulnerable.", ecdsaVulnerable ? "" : "not ");
}
}
if (!LOGGER.isDebugEnabled()) {
LOGGER.info("RSA: {}, EC: {}, AES: {}", rsa, ec, aesPermission);
}
Expand Down Expand Up @@ -463,7 +494,7 @@ private static void setupJce() {
LOGGER.info("EdDSA not supported!");
}
JceProviderUtil newSupport = new JceProviderUtil(isBouncyCastle(provider), rsa, ec, ed25519, ed448,
aesMaxAllowedKeyLength >= 256, version);
aesMaxAllowedKeyLength >= 256, ecdsaVulnerable, version);
if (!newSupport.equals(features)) {
features = newSupport;
}
Expand Down Expand Up @@ -507,6 +538,23 @@ public static boolean hasStrongEncryption() {
return features.strongEncryption;
}

/**
* Checks, if the JCE is affected by the ECDSA vulnerability.
*
* Some java JCE versions 15 to 18 fail to check the signature for 0 and n.
*
* @return {@code true}, if the JCE has the ECDSA vulnerability,
* {@code false}, otherwise. signature received signature.
* @see <a href=
* "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-21449"
* target="_blank">CVE-2022-21449</a>
* @see JceNames#CALIFORNIUM_JCE_ECDSA_FIX
* @since 3.5
*/
public static boolean isEcdsaVulnerable() {
return features.ecdsaVulnerable;
}

/**
* Check, if key algorithm is supported.
*
Expand Down Expand Up @@ -586,13 +634,14 @@ public static String getProviderVersion() {
}

private JceProviderUtil(boolean useBc, boolean rsa, boolean ec, boolean ed25519, boolean ed448,
boolean strongEncryption, String providerVersion) {
boolean strongEncryption, boolean ecdsaVulnerable, String providerVersion) {
this.useBc = useBc;
this.rsa = rsa;
this.ec = ec;
this.ed25519 = ed25519;
this.ed448 = ed448;
this.strongEncryption = strongEncryption;
this.ecdsaVulnerable = ecdsaVulnerable;
this.providerVersion = providerVersion;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@
import static org.junit.Assume.assumeTrue;

import java.io.IOException;
import java.math.BigInteger;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.interfaces.ECPublicKey;
import java.util.Arrays;

import org.eclipse.californium.elements.util.Asn1DerDecoder.Keys;
Expand Down Expand Up @@ -289,6 +293,85 @@ public void testReadPrivateKeyEcV2() throws IOException, GeneralSecurityExceptio
TestCertificatesTools.assertSigning("asn.1", keys.getPrivateKey(), keys.getPublicKey(), "SHA256withECDSA");
}

@Test
public void testBrokenEcdsa() throws IOException, GeneralSecurityException {
String version = System.getProperty("java.version");
byte[] data = Base64.decode(EC_PRIVATE_KEY_V2_BASE64);
Keys keys = Asn1DerDecoder.readPrivateKey(data);
assertThat(keys, is(notNullValue()));
assertThat(keys.getPrivateKey(), is(notNullValue()));
assertThat(keys.getPublicKey(), is(notNullValue()));
try {
boolean broken = false;
SecureRandom random = new SecureRandom();
Signature signature = Signature.getInstance("SHA256withECDSA");
byte[] message = Bytes.createBytes(random, 1024);
signature.initSign(keys.getPrivateKey());
signature.update(message);
byte[] sign = signature.sign();

// verify the proper signature
signature.initVerify(keys.getPublicKey());
signature.update(message);
if (!signature.verify(sign)) {
fail("verify failed!");
}
Asn1DerDecoder.checkEcDsaSignature(sign, keys.getPublicKey());

// check the ghost signature with R := 0 and L := 0
byte[] ghost = StringUtil.hex2ByteArray("3006020100020100");
signature.initVerify(keys.getPublicKey());
signature.update(message);
boolean valid = signature.verify(ghost);
if (valid) {
broken = true;
System.err.println("Java JCE " + version + " is vulnerable for ECDSA R := 0, CVE-2022-21449!");
} else {
System.out.println("Java JCE " + version + " is not vulnerable for ECDSA R := 0, CVE-2022-21449!");
}
try {
Asn1DerDecoder.checkEcDsaSignature(ghost, keys.getPublicKey());
fail("Failed to detect R := 0 ECDSA signature!");
} catch (GeneralSecurityException ex) {
assertThat(ex.getMessage(), is("ECDSA signature R is less than 1!"));
}

// check the ghost signature with R := N and L := N
BigInteger order = ((ECPublicKey) keys.getPublicKey()).getParams().getOrder();
byte[] number = order.toByteArray();
if (number[0] < 0) {
number = Bytes.concatenate(new byte[] { 0 }, number);
}
number = Bytes.concatenate(new byte[] { 0x02, (byte) number.length }, number);

byte[] ghost2 = Bytes.concatenate(new byte[] { 0x30, (byte) (number.length * 2) }, number);
ghost2 = Bytes.concatenate(ghost2, number);

signature.initVerify(keys.getPublicKey());
signature.update(message);
valid = signature.verify(ghost2);
if (valid) {
broken = true;
System.err.println("Java JCE " + version + " is vulnerable for ECDSA R := N, CVE-2022-21449!");
} else {
System.out.println("Java JCE " + version + " is not vulnerable for ECDSA R := N, CVE-2022-21449!");
}
try {
Asn1DerDecoder.checkEcDsaSignature(ghost2, keys.getPublicKey());
fail("failed to detect R := N ECDSA signature!");
} catch (GeneralSecurityException ex) {
assertThat(ex.getMessage(), is("ECDSA signature R is not less than N!"));
}
assertThat("detect broken ECDSA failed!", JceProviderUtil.isEcdsaVulnerable(), is(broken));
} catch (GeneralSecurityException e) {
e.printStackTrace();
fail("Failed with " + e);
} catch (RuntimeException e) {
e.printStackTrace();
fail("Failed with " + e);
}
}

/**
* Test, if decoder throws NoSuchAlgorithmException when reading EdDSA v2 private key.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -473,11 +473,11 @@ public static void assertSigning(String message, PrivateKey privateKey, PublicKe
}
byte[] data = Bytes.createBytes(random, len);
signature.initSign(privateKey);
signature.update(data, 0, len);
signature.update(data);
byte[] sign = signature.sign();

signature.initVerify(pulbicKey);
signature.update(data, 0, len);
signature.update(data);
if (!signature.verify(sign)) {
fail(message + ":" + algorithm + " failed!");
}
Expand Down
Loading

0 comments on commit 022565f

Please # to comment.