Better hash algorithm in FingerprintTrustManagerFactory (#10683)

Motivation:

FingerprintTrustManagerFactory can only use SHA-1 that is considered
insecure.

Modifications:

- Updated FingerprintTrustManagerFactory to accept a stronger hash algorithm.
- Deprecated the constructors that still use SHA-1.
- Added a test for FingerprintTrustManagerFactory.

Result:

A user can now configure FingerprintTrustManagerFactory to use a
stronger hash algorithm.
The compiler shows a warning if the code still uses one of the
unsafe constructors.

Co-authored-by: Norman Maurer <norman_maurer@apple.com>
This commit is contained in:
Artem Smotrakov 2020-10-26 14:29:26 +01:00 committed by GitHub
parent 03aafb9cff
commit 065c39611e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 329 additions and 68 deletions

View File

@ -39,7 +39,7 @@ import java.util.List;
import java.util.regex.Pattern;
/**
* An {@link TrustManagerFactory} that trusts an X.509 certificate whose SHA1 checksum matches.
* An {@link TrustManagerFactory} that trusts an X.509 certificate whose hash matches.
* <p>
* <strong>NOTE:</strong> It is recommended to verify certificates and their chain to prevent
* <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">Man-in-the-middle attacks</a>.
@ -51,22 +51,30 @@ import java.util.regex.Pattern;
* actually perform Man-in-the-middle attacks and thus present a different certificate fingerprint.
* </p>
* <p>
* The SHA1 checksum of an X.509 certificate is calculated from its DER encoded format. You can get the fingerprint of
* The hash of an X.509 certificate is calculated from its DER encoded format. You can get the fingerprint of
* an X.509 certificate using the {@code openssl} command. For example:
*
* <pre>
* $ openssl x509 -fingerprint -sha1 -in my_certificate.crt
* SHA1 Fingerprint=4E:85:10:55:BC:7B:12:08:D1:EA:0A:12:C9:72:EE:F3:AA:B2:C7:CB
* $ openssl x509 -fingerprint -sha256 -in my_certificate.crt
* SHA256 Fingerprint=1C:53:0E:6B:FF:93:F0:DE:C2:E6:E7:9D:10:53:58:FF:DD:8E:68:CD:82:D9:C9:36:9B:43:EE:B3:DC:13:68:FB
* -----BEGIN CERTIFICATE-----
* MIIBqjCCAROgAwIBAgIJALiT3Nvp0kvmMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV
* BAMTC2V4YW1wbGUuY29tMCAXDTcwMDEwMTAwMDAwMFoYDzk5OTkxMjMxMjM1OTU5
* WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
* gYkCgYEAnadvODG0QCiHhaFZlLHtr5gLIkDQS8ErZ//KfqeCHTC/KJsl3xYFk0zG
* aCv2FcmkOlokm77qV8qOW2DZdND7WuYzX6nLVuLb+GYxZ7b45iMAbAajvGh8jc9U
* o07fUIahGqTDAIAGCWsoLUOQ9nMzO/8GRHcXJAeQ2MGY2VpCcv0CAwEAATANBgkq
* hkiG9w0BAQUFAAOBgQBpRCnmjmNM0D7yrpkUJpBTNiqinhKLbeOvPWm+YmdInUUs
* LoMu0mZ1IANemLwqbwJJ76fknngeB+YuVAj46SurvVCV6ekwHcbgpW1u063IRwKk
* tQhOBO0HQxldUS4+4MYv/kuvnKkbjfgh5qfWw89Kx4kD+cycpP4yPtgDGk8ZMA==
* MIIC/jCCAeagAwIBAgIIIMONxElm0AIwDQYJKoZIhvcNAQELBQAwPjE8MDoGA1UE
* AwwzZThhYzAyZmEwZDY1YTg0MjE5MDE2MDQ1ZGI4YjA1YzQ4NWI0ZWNkZi5uZXR0
* eS50ZXN0MCAXDTEzMDgwMjA3NTEzNloYDzk5OTkxMjMxMjM1OTU5WjA+MTwwOgYD
* VQQDDDNlOGFjMDJmYTBkNjVhODQyMTkwMTYwNDVkYjhiMDVjNDg1YjRlY2RmLm5l
* dHR5LnRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDb+HBO3C0U
* RBKvDUgJHbhIlBye8X/cbNH3lDq3XOOFBz7L4XZKLDIXS+FeQqSAUMo2otmU+Vkj
* 0KorshMjbUXfE1KkTijTMJlaga2M2xVVt21fRIkJNWbIL0dWFLWyRq7OXdygyFkI
* iW9b2/LYaePBgET22kbtHSCAEj+BlSf265+1rNxyAXBGGGccCKzEbcqASBKHOgVp
* 6pLqlQAfuSy6g/OzGzces3zXRrGu1N3pBIzAIwCW429n52ZlYfYR0nr+REKDnRrP
* IIDsWASmEHhBezTD+v0qCJRyLz2usFgWY+7agUJE2yHHI2mTu2RAFngBilJXlMCt
* VwT0xGuQxkbHAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEv8N7Xm8qaY2FgrOc6P
* a1GTgA+AOb3aU33TGwAR86f+nLf6BSPaohcQfOeJid7FkFuYInuXl+oqs+RqM/j8
* R0E5BuGYY2wOKpL/PbFi1yf/Kyvft7KVh8e1IUUec/i1DdYTDB0lNWvXXxjfMKGL
* ct3GMbEHKvLfHx42Iwz/+fva6LUrO4u2TDfv0ycHuR7UZEuC1DJ4xtFhbpq/QRAj
* CyfNx3cDc7L2EtJWnCmivTFA9l8MF1ZPMDSVd4ecQ7B0xZIFQ5cSSFt7WGaJCsGM
* zYkU4Fp4IykQcWxdlNX7wJZRwQ2TZJFFglpTiFZdeq6I6Ad9An1Encpz5W8UJ4tv
* hmw=
* -----END CERTIFICATE-----
* </pre>
* </p>
@ -75,20 +83,18 @@ public final class FingerprintTrustManagerFactory extends SimpleTrustManagerFact
private static final Pattern FINGERPRINT_PATTERN = Pattern.compile("^[0-9a-fA-F:]+$");
private static final Pattern FINGERPRINT_STRIP_PATTERN = Pattern.compile(":");
private static final int SHA1_BYTE_LEN = 20;
private static final int SHA1_HEX_LEN = SHA1_BYTE_LEN * 2;
private static final FastThreadLocal<MessageDigest> tlmd = new FastThreadLocal<MessageDigest>() {
@Override
protected MessageDigest initialValue() {
try {
return MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
// All Java implementation must have SHA1 digest algorithm.
throw new Error(e);
}
}
};
/**
* Creates a builder for {@link FingerprintTrustManagerFactory}.
*
* @param algorithm a hash algorithm
* @return a builder
*/
public static FingerprintTrustManagerFactoryBuilder builder(String algorithm) {
return new FingerprintTrustManagerFactoryBuilder(algorithm);
}
private final FastThreadLocal<MessageDigest> tlmd;
private final TrustManager tm = new X509TrustManager() {
@ -136,45 +142,99 @@ public final class FingerprintTrustManagerFactory extends SimpleTrustManagerFact
/**
* Creates a new instance.
*
* @deprecated This deprecated constructor uses SHA-1 that is considered insecure.
* It is recommended to specify a stronger hash algorithm, such as SHA-256,
* by calling {@link FingerprintTrustManagerFactory#builder(String)} method.
*
* @param fingerprints a list of SHA1 fingerprints in hexadecimal form
*/
@Deprecated
public FingerprintTrustManagerFactory(Iterable<String> fingerprints) {
this(toFingerprintArray(fingerprints));
this("SHA1", toFingerprintArray(fingerprints));
}
/**
* Creates a new instance.
*
* @deprecated This deprecated constructor uses SHA-1 that is considered insecure.
* It is recommended to specify a stronger hash algorithm, such as SHA-256,
* by calling {@link FingerprintTrustManagerFactory#builder(String)} method.
*
* @param fingerprints a list of SHA1 fingerprints in hexadecimal form
*/
@Deprecated
public FingerprintTrustManagerFactory(String... fingerprints) {
this(toFingerprintArray(Arrays.asList(fingerprints)));
this("SHA1", toFingerprintArray(Arrays.asList(fingerprints)));
}
/**
* Creates a new instance.
*
* @deprecated This deprecated constructor uses SHA-1 that is considered insecure.
* It is recommended to specify a stronger hash algorithm, such as SHA-256,
* by calling {@link FingerprintTrustManagerFactory#builder(String)} method.
*
* @param fingerprints a list of SHA1 fingerprints
*/
@Deprecated
public FingerprintTrustManagerFactory(byte[]... fingerprints) {
this("SHA1", fingerprints);
}
/**
* Creates a new instance.
*
* @param algorithm a hash algorithm
* @param fingerprints a list of fingerprints
*/
FingerprintTrustManagerFactory(final String algorithm, byte[][] fingerprints) {
ObjectUtil.checkNotNull(algorithm, "algorithm");
ObjectUtil.checkNotNull(fingerprints, "fingerprints");
if (fingerprints.length == 0) {
throw new IllegalArgumentException("No fingerprints provided");
}
// check early if the hash algorithm is available
final MessageDigest md;
try {
md = MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(
String.format("Unsupported hash algorithm: %s", algorithm), e);
}
int hashLength = md.getDigestLength();
List<byte[]> list = new ArrayList<byte[]>(fingerprints.length);
for (byte[] f: fingerprints) {
if (f == null) {
break;
}
if (f.length != SHA1_BYTE_LEN) {
throw new IllegalArgumentException("malformed fingerprint: " +
ByteBufUtil.hexDump(Unpooled.wrappedBuffer(f)) + " (expected: SHA1)");
if (f.length != hashLength) {
throw new IllegalArgumentException(
String.format("malformed fingerprint (length is %d but expected %d): %s",
f.length, hashLength, ByteBufUtil.hexDump(Unpooled.wrappedBuffer(f))));
}
list.add(f.clone());
}
this.tlmd = new FastThreadLocal<MessageDigest>() {
@Override
protected MessageDigest initialValue() {
try {
return MessageDigest.getInstance(algorithm);
} catch (NoSuchAlgorithmException e) {
throw new IllegalArgumentException(
String.format("Unsupported hash algorithm: %s", algorithm), e);
}
}
};
this.fingerprints = list.toArray(new byte[0][]);
}
private static byte[][] toFingerprintArray(Iterable<String> fingerprints) {
static byte[][] toFingerprintArray(Iterable<String> fingerprints) {
ObjectUtil.checkNotNull(fingerprints, "fingerprints");
List<byte[]> list = new ArrayList<byte[]>();
@ -187,9 +247,6 @@ public final class FingerprintTrustManagerFactory extends SimpleTrustManagerFact
throw new IllegalArgumentException("malformed fingerprint: " + f);
}
f = FINGERPRINT_STRIP_PATTERN.matcher(f).replaceAll("");
if (f.length() != SHA1_HEX_LEN) {
throw new IllegalArgumentException("malformed fingerprint: " + f + " (expected: SHA1)");
}
list.add(StringUtil.decodeHexDump(f));
}

View File

@ -0,0 +1,89 @@
/*
* Copyright 2020 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.ssl.util;
import io.netty.util.internal.ObjectUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* A builder for creating {@link FingerprintTrustManagerFactory}.
*/
public final class FingerprintTrustManagerFactoryBuilder {
/**
* A hash algorithm for fingerprints.
*/
private final String algorithm;
/**
* A list of fingerprints.
*/
private final List<String> fingerprints = new ArrayList<String>();
/**
* Creates a builder.
*
* @param algorithm a hash algorithm
*/
FingerprintTrustManagerFactoryBuilder(String algorithm) {
this.algorithm = ObjectUtil.checkNotNull(algorithm, "algorithm");
}
/**
* Adds fingerprints.
*
* @param fingerprints a number of fingerprints
* @return the same builder
*/
public FingerprintTrustManagerFactoryBuilder fingerprints(CharSequence... fingerprints) {
ObjectUtil.checkNotNull(fingerprints, "fingerprints");
return fingerprints(Arrays.asList(fingerprints));
}
/**
* Adds fingerprints.
*
* @param fingerprints a number of fingerprints
* @return the same builder
*/
public FingerprintTrustManagerFactoryBuilder fingerprints(Iterable<? extends CharSequence> fingerprints) {
ObjectUtil.checkNotNull(fingerprints, "fingerprints");
for (CharSequence fingerprint : fingerprints) {
if (fingerprint == null) {
throw new IllegalArgumentException("One of the fingerprints is null");
}
this.fingerprints.add(fingerprint.toString());
}
return this;
}
/**
* Creates a {@link FingerprintTrustManagerFactory}.
*
* @return a new {@link FingerprintTrustManagerFactory}
*/
public FingerprintTrustManagerFactory build() {
if (fingerprints.isEmpty()) {
throw new IllegalStateException("No fingerprints provided");
}
return new FingerprintTrustManagerFactory(this.algorithm,
FingerprintTrustManagerFactory.toFingerprintArray(this.fingerprints));
}
}

View File

@ -22,11 +22,16 @@ import javax.net.ssl.SNIMatcher;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import java.io.InputStream;
import java.security.Provider;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.Collections;
final class Java8SslTestUtils {
import static org.junit.Assert.assertNotNull;
public final class Java8SslTestUtils {
private Java8SslTestUtils() { }
@ -53,4 +58,27 @@ final class Java8SslTestUtils {
}
return engine;
}
public static X509Certificate[] loadCertCollection(String... resourceNames)
throws Exception {
CertificateFactory certFactory = CertificateFactory
.getInstance("X.509");
X509Certificate[] certCollection = new X509Certificate[resourceNames.length];
for (int i = 0; i < resourceNames.length; i++) {
String resourceName = resourceNames[i];
InputStream is = null;
try {
is = SslContextTest.class.getResourceAsStream(resourceName);
assertNotNull("Cannot find " + resourceName, is);
certCollection[i] = (X509Certificate) certFactory
.generateCertificate(is);
} finally {
if (is != null) {
is.close();
}
}
}
return certCollection;
}
}

View File

@ -15,20 +15,18 @@
*/
package io.netty.handler.ssl;
import static org.junit.Assert.fail;
import static org.junit.Assert.assertNotNull;
import java.io.InputStream;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import org.junit.Test;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import org.junit.Test;
import static io.netty.handler.ssl.Java8SslTestUtils.loadCertCollection;
import static org.junit.Assert.fail;
import static org.junit.Assert.assertNotNull;
public class SslContextTrustManagerTest {
@Test
@ -121,27 +119,4 @@ public class SslContextTrustManagerTest {
throw new Exception(
"Unable to find any X509TrustManager from this factory.");
}
private static X509Certificate[] loadCertCollection(String[] resourceNames)
throws Exception {
CertificateFactory certFactory = CertificateFactory
.getInstance("X.509");
X509Certificate[] certCollection = new X509Certificate[resourceNames.length];
for (int i = 0; i < resourceNames.length; i++) {
String resourceName = resourceNames[i];
InputStream is = null;
try {
is = SslContextTest.class.getResourceAsStream(resourceName);
assertNotNull("Cannot find " + resourceName, is);
certCollection[i] = (X509Certificate) certFactory
.generateCertificate(is);
} finally {
if (is != null) {
is.close();
}
}
}
return certCollection;
}
}

View File

@ -0,0 +1,112 @@
/*
* Copyright 2020 The Netty Project
*
* The Netty Project licenses this file to you under the Apache License,
* version 2.0 (the "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at:
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
package io.netty.handler.ssl.util;
import org.junit.Test;
import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import static io.netty.handler.ssl.Java8SslTestUtils.loadCertCollection;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
public class FingerprintTrustManagerFactoryTest {
private static final String FIRST_CERT_SHA1_FINGERPRINT
= "18:C7:C2:76:1F:DF:72:3B:2A:A7:BB:2C:B0:30:D4:C0:C0:72:AD:84";
private static final String FIRST_CERT_SHA256_FINGERPRINT
= "1C:53:0E:6B:FF:93:F0:DE:C2:E6:E7:9D:10:53:58:FF:" +
"DD:8E:68:CD:82:D9:C9:36:9B:43:EE:B3:DC:13:68:FB";
private static final X509Certificate[] FIRST_CHAIN;
private static final X509Certificate[] SECOND_CHAIN;
static {
try {
FIRST_CHAIN = loadCertCollection("test.crt");
SECOND_CHAIN = loadCertCollection("test2.crt");
} catch (Exception e) {
throw new Error(e);
}
}
@Test(expected = IllegalArgumentException.class)
public void testFingerprintWithInvalidLength() {
FingerprintTrustManagerFactory.builder("SHA-256").fingerprints("00:00:00").build();
}
@Test(expected = IllegalArgumentException.class)
public void testFingerprintWithUnexpectedCharacters() {
FingerprintTrustManagerFactory.builder("SHA-256").fingerprints("00:00:00\n").build();
}
@Test(expected = IllegalStateException.class)
public void testWithNoFingerprints() {
FingerprintTrustManagerFactory.builder("SHA-256").build();
}
@Test(expected = IllegalArgumentException.class)
public void testWithNullFingerprint() {
FingerprintTrustManagerFactory
.builder("SHA-256")
.fingerprints(FIRST_CERT_SHA256_FINGERPRINT, null)
.build();
}
@Test
public void testValidSHA1Fingerprint() throws Exception {
FingerprintTrustManagerFactory factory = new FingerprintTrustManagerFactory(FIRST_CERT_SHA1_FINGERPRINT);
assertTrue(factory.engineGetTrustManagers().length > 0);
assertTrue(factory.engineGetTrustManagers()[0] instanceof X509TrustManager);
X509TrustManager tm = (X509TrustManager) factory.engineGetTrustManagers()[0];
tm.checkClientTrusted(FIRST_CHAIN, "test");
}
@Test
public void testTrustedCertificateWithSHA256Fingerprint() throws Exception {
FingerprintTrustManagerFactory factory = FingerprintTrustManagerFactory
.builder("SHA-256")
.fingerprints(FIRST_CERT_SHA256_FINGERPRINT)
.build();
X509Certificate[] keyCertChain = loadCertCollection("test.crt");
assertNotNull(keyCertChain);
assertTrue(factory.engineGetTrustManagers().length > 0);
assertTrue(factory.engineGetTrustManagers()[0] instanceof X509TrustManager);
X509TrustManager tm = (X509TrustManager) factory.engineGetTrustManagers()[0];
tm.checkClientTrusted(keyCertChain, "test");
}
@Test(expected = CertificateException.class)
public void testUntrustedCertificateWithSHA256Fingerprint() throws Exception {
FingerprintTrustManagerFactory factory = FingerprintTrustManagerFactory
.builder("SHA-256")
.fingerprints(FIRST_CERT_SHA256_FINGERPRINT)
.build();
assertTrue(factory.engineGetTrustManagers().length > 0);
assertTrue(factory.engineGetTrustManagers()[0] instanceof X509TrustManager);
X509TrustManager tm = (X509TrustManager) factory.engineGetTrustManagers()[0];
tm.checkClientTrusted(SECOND_CHAIN, "test");
}
}