Repository: netty/netty
Issue: Better hash algorithm in FingerprintTrustManagerFactory

`FingerprintTrustManagerFactory` checks if a certificate is trusted by comparing its SHA-1 hash to a list of pre-configured ones:

https://github.com/netty/netty/blob/0cde4d9cb4d19ddc0ecafc5be7c5f7c781a1f6e9/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java#L85

Unfortunately, nowadays SHA-1 is considered insecure. The issue was reported by [LGTM](https://lgtm.com/projects/g/netty/netty/snapshot/090bf3d107cb3099317e1cd9e0d661ac0797c126/files/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java?sort=name&dir=ASC&mode=heatmap#xedd0183a9626d84c:1).

I see the following ways to fix it:

1. Update `FingerprintTrustManagerFactory` to use a stronger algorithm such as SHA-512. Unfortunately, most likely it'll break applications that pass a SHA-1 hash to the class.
1. Deprecate `FingerprintTrustManagerFactory` and add a new implementation that use a stronger algorithm. 
1. Updated `FingerprintTrustManagerFactory` to determine a hash algorithm based on a length of a hash passed to the constructor. If a caller passes a SHA-1 hash, the class can also print a warning.

Please let me know if one of the options above is fine (or, you see a better option), and I'll open a pull request.

Issue comments:
SHA-512 is an overkill and not useful for this purpose. SHA-256 or SHA-384 is the way to go.
@trustin @normanmaurer What do you think?
As far as I understand how `FingerprintTrustManagerFactory` is meant to be used, I think it's better to deprecate it, and add a new implementation that uses a stronger hash function, or allow the hash function to be configured. Maybe the configurable implementation can be extracted into a new super-class of `FingerprintTrustManagerFactory`.
I agree with @chrisvest here... 
I don't see a reason to introduce a new `TrustManagerFactory` implementation class. We could deprecate the public constructors in favor of builders and static factory methods. e.g.

```java
FingerprintTrustManagerFactory f =
  FingerprintTrustManagerFactory
    .builder("SHA256") // Always require the algorithm name
    .fingerprint("deadbeef...")
    .fingerprint("cafecafe...")
    .build();
```

The public constructors could call the internal constructor that passes the message digest function, e.g.

```java
@Deprecated
public FingerprintTrustManagerFactory(String... fingerprints) {
    this(FingerprintTrustManagerFactory::sha1Fingerprint, fingerprints);
}

// Called by the builder or the legacy public constructors.
// The example uses Java 8 Function but we can define a dedicated interface if necessary.
FingerprintTrustManagerFactory(
    Function<? super X509Certificate, byte[]> fingerprintFunc,
    String... fingerprints) {
    ...
}
```

We could also consider implementing more versatile `TrustManagerFactory` by extracting the verification logic out of the `TrustManagerFactory`, e.g.

```java
SomeTrustManagerFactory
  .builder()
  .add(CertificateMatchers.fingerprint("SHA256", "deadbeef..."))
  .add("bar.foo.com", CertificateMatchers.strict())
  .add("*.foo.com", CertificateMatchers.any());
  .build();
```
I like the idea with a builder and deprecating only the public constructor suggested by @trustin 

```java
FingerprintTrustManagerFactory f =
  FingerprintTrustManagerFactory
    .builder("SHA256") // Always require the algorithm name
    .fingerprint("deadbeef...")
    .fingerprint("cafecafe...")
    .build();
```

If no objections, I'll implement this logic then.

Pull request review comments:
Add method to add a List of `fingerprints`.
Also add `@deprecated` javadoc tag
also add `@deprecated` javadoc tag 
should we call `MessageDigest.getInstance(algorithm)` once in the constructor now to ensure it will not fail later on ? 
see above... I think it is very unlikely that the "constructing" thread will use the factory itself. so I think it would be better to not waste resources in the `FastThreadLocal`  and construct it directly.
`EmptyArrays.BYTES`
honestly this feels a bit like overkill.. Let's just keep it or use `String...`
Calling `public Builder fingerprint(String fingerprint)` every time is a little painful that's why we need something to add fingerprints in bulk. Both `List` and `String...` will do the job.
I disagree... what is the point of a builder if you will never call its methods multiple times ? 
That's a thing too. 
I think both points are valid. The builder may be used in multiple ways. For example, someone may want to hardcode fingerprints like the following:

```java
FingerprintTrustManagerFactory f =
  FingerprintTrustManagerFactory
    .builder("SHA256")
    .fingerprint("deadbeef...")
    .fingerprint("cafecafe...")
    .build();
```

But someone may want to load fingerprints from a file. In this case, `fingerprint()` would need to be called in a loop:

```java
FingerprintTrustManagerFactory.Builder b = FingerprintTrustManagerFactory.builder("SHA256");
for (String s : loadFingerprintsFrom(file)) {
    b.fingerprint(s);
}
```

Here `fingerprint(String...)` might make it look a bit nicer:

```java
FingerprintTrustManagerFactory f =
  FingerprintTrustManagerFactory
    .builder("SHA256")
    .fingerprint(loadFingerprintsFrom(file))
    .build();
```

Anyway, I am fine to keep it as-is, or updating `fingerprint()` to accept `String...`.
I am ok with `String...` but then call it fingerprints(...)
If I understand you correctly, you're suggesting getting rid of `FastThreadLocal`. I was not sure about it since I am new to the code. Okay, I can remove `FastThreadLocal` and create a `MessageDigest` directly in the constructor if it's fine.
If I am not missing something, that could not be used here because `EmptyArrays.EMPTY_BYTES` is `byte[]`.

https://github.com/netty/netty/blob/00afb19d7a37de21b35ce4f6cb3fa7f74809f2ab/common/src/main/java/io/netty/util/internal/EmptyArrays.java#L28
ah sorry I missed that it is `[][]`
no ... What I was saying that you should keep the `FastThreadLocal` but not access it in the constructor but better just create a new instance in the constructor and use it here. 
Okay, sure I can create a `MessageDigest` in the constructor as an early-check that the algorithm exists. The initialization of `FastThreadLocal` would still stay in the constructor since it now requires an algorithm identifier.
nit: include `algorithm` in the message as well. 
nit: include the actual length and the expected length in the message as well 
The wrapped exception usually mention the algorithm but nevertheless let's include it to the top message as well.
How about moving this method before the member fields in this class? i.e.

```
class MyClass {

    private static final ...;

    public static Builder builder() { ... }

    private final ...;

    public MyClass() { ... }
}
```
I'd prefer having this in a top-level class as `FingerprintTrustManagerFactoryBuilder`.
- How about accepting `CharSequence...` and then converting it to a `String`?
- Could we also add the version that accepts `Iterable<? extends CharSequence>`?
What would be the expected behavior when `fingerprints` is empty? Throwing an `IllegalStateException` in `build()`?
The way this works means the `FingerprintTrustManagerFactory` cannot be shared between multiple threads, because only the constructing thread has initialised their thread-local, and there's not enough information in the factory instance for other threads to initialise their thread-locals as well.
@chrisvest huh... can you explain to me why is this ? 
@chrisvest was right here... you need to override `initialValue()` here 
nevermind... https://github.com/netty/netty/pull/10683#discussion_r506221085
Since `FingerprintTrustManagerFactory` is now configured with a hash algorithm, `tlmd` needs to be initialized in the constructor. It used to be before. Then, `initialValue()` was removed because we wanted to create an instance of `MessageDigest` earlier in the constructor to check if the algorithm exists. If I understand correctly, we just want to revert the logic to the previous version. I am not sure if we need to create another `MessageDigest` in the constructor. Please let me know if so.
@chrisvest Thanks for pointing it out!
> * How about accepting `CharSequence...` and then converting it to a `String`?

Let me try.

> * Could we also add the version that accepts `Iterable<? extends CharSequence>`?

We've discussed adding `fingerprings(List)` but decided not to do that

https://github.com/netty/netty/pull/10683#discussion_r504560104

I am fine with adding `fingerprints(Iterable<? extends CharSequence>)` as long as other are fine.
> I am not sure if we need to create another MessageDigest in the constructor. Please let me know if so.

Well, looks like there is no harm if we do that. So, I'll add such a check if no objections.
Since `tlmd` is an instance field, you can create the `FastThreadLocal` with an `initialValue` method that has all the logic necessary to create `MessageDigest` instances. I'd still check that the algorithm exists before creating the FTL, though, because each FTL instance will claim some static resources that we'd prefer not to leak.
Currently, no fingerprints are allowed. That would mean that no certificate is going to be trusted. In theory, it's okay but it looks unlikely that an application may want to use such a paranoid `TrustManager`. Therefore, I think it would be fine to throw an exception if no fingerprints are provided. I'd put this check in to a constructor since all the checks are currently done there, and `FingerprintTrustManagerFactory` may be still created by calling the deprecated constructors.
I think it was more about `fingerprint(String)` and `fingerprints(String...)`. We do need `fingerprints(Iterable<? extends CharSequence>)` for the users who want to feed the fingerprints from a collection.
Sure, no problem. I've added `fingerprints(Iterable<? extends CharSequence>)`.
How about moving this validation to `build()` and raising `IllegalStateException`? Otherwise, a user will get `IllegalArgumentException` on `build()`, which does not make sense. We could add an assertion here instead.
If we would like to prohibit creating a `TrustManager` with no fingerprints, then I think this check should be in the constructor because `FingerprintTrustManagerFactory` can still be created by calling the deprecated constructors.

If I understand you correctly, you mean that `IllegalArgumentException` may confuse users whey they call `build()` that has no arguments itself. We can add an extra check in `build()` that throws `IllegalStateException` if the list of fingerprints is empty. I'll update the code then. Please let me know if I missed something.
```suggestion
 *   https://www.apache.org/licenses/LICENSE-2.0
```
```suggestion
 *   https://www.apache.org/licenses/LICENSE-2.0
```
Thanks for fixing this @normanmaurer !

Patch that resolved the issue:
diff --git a/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java b/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java
index 8a023d476e4..ff6772a695a 100644
--- a/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java
+++ b/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactory.java
@@ -39,7 +39,7 @@
 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 @@
  * 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 X509Certificate[] getAcceptedIssuers() {
     /**
      * 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 @@ private static byte[][] toFingerprintArray(Iterable<String> fingerprints) {
                 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));
         }
diff --git a/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactoryBuilder.java b/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactoryBuilder.java
new file mode 100644
index 00000000000..38182661d8a
--- /dev/null
+++ b/handler/src/main/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactoryBuilder.java
@@ -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));
+    }
+}


Test patch:
diff --git a/handler/src/test/java/io/netty/handler/ssl/Java8SslTestUtils.java b/handler/src/test/java/io/netty/handler/ssl/Java8SslTestUtils.java
index 50fb935d9dc..dc3a2774bfe 100644
--- a/handler/src/test/java/io/netty/handler/ssl/Java8SslTestUtils.java
+++ b/handler/src/test/java/io/netty/handler/ssl/Java8SslTestUtils.java
@@ -22,11 +22,16 @@
 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 @@ static SSLEngine wrapSSLEngineForTesting(SSLEngine engine) {
         }
         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;
+    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java b/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java
index 04b6a8e08b4..6f209e0757b 100644
--- a/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java
+++ b/handler/src/test/java/io/netty/handler/ssl/SslContextTrustManagerTest.java
@@ -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 @@ private static X509TrustManager getTrustManager(String[] resourceNames)
         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;
-    }
 }
diff --git a/handler/src/test/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactoryTest.java b/handler/src/test/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactoryTest.java
new file mode 100644
index 00000000000..12c0d894deb
--- /dev/null
+++ b/handler/src/test/java/io/netty/handler/ssl/util/FingerprintTrustManagerFactoryTest.java
@@ -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");
+    }
+
+}