View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty5.handler.ssl.util;
18  
19  import io.netty5.buffer.BufferUtil;
20  import io.netty5.util.concurrent.FastThreadLocal;
21  import io.netty5.util.internal.EmptyArrays;
22  import io.netty5.util.internal.StringUtil;
23  
24  import javax.net.ssl.ManagerFactoryParameters;
25  import javax.net.ssl.TrustManager;
26  import javax.net.ssl.TrustManagerFactory;
27  import javax.net.ssl.X509TrustManager;
28  import java.security.KeyStore;
29  import java.security.MessageDigest;
30  import java.security.NoSuchAlgorithmException;
31  import java.security.cert.CertificateEncodingException;
32  import java.security.cert.CertificateException;
33  import java.security.cert.X509Certificate;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.List;
37  import java.util.regex.Pattern;
38  
39  import static java.util.Objects.requireNonNull;
40  
41  /**
42   * An {@link TrustManagerFactory} that trusts an X.509 certificate whose hash matches.
43   * <p>
44   * <strong>NOTE:</strong> It is recommended to verify certificates and their chain to prevent
45   * <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">Man-in-the-middle attacks</a>.
46   * This {@link TrustManagerFactory} will <strong>only</strong> verify that the fingerprint of certificates match one
47   * of the given fingerprints. This procedure is called
48   * <a href="https://en.wikipedia.org/wiki/Transport_Layer_Security#Certificate_pinning">certificate pinning</a> and
49   * is an effective protection. For maximum security one should verify that the whole certificate chain is as expected.
50   * It is worth mentioning that certain firewalls, proxies or other appliances found in corporate environments,
51   * actually perform Man-in-the-middle attacks and thus present a different certificate fingerprint.
52   * </p>
53   * <p>
54   * The hash of an X.509 certificate is calculated from its DER encoded format.  You can get the fingerprint of
55   * an X.509 certificate using the {@code openssl} command.  For example:
56   *
57   * <pre>
58   * $ openssl x509 -fingerprint -sha256 -in my_certificate.crt
59   * 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
60   * -----BEGIN CERTIFICATE-----
61   * MIIC/jCCAeagAwIBAgIIIMONxElm0AIwDQYJKoZIhvcNAQELBQAwPjE8MDoGA1UE
62   * AwwzZThhYzAyZmEwZDY1YTg0MjE5MDE2MDQ1ZGI4YjA1YzQ4NWI0ZWNkZi5uZXR0
63   * eS50ZXN0MCAXDTEzMDgwMjA3NTEzNloYDzk5OTkxMjMxMjM1OTU5WjA+MTwwOgYD
64   * VQQDDDNlOGFjMDJmYTBkNjVhODQyMTkwMTYwNDVkYjhiMDVjNDg1YjRlY2RmLm5l
65   * dHR5LnRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDb+HBO3C0U
66   * RBKvDUgJHbhIlBye8X/cbNH3lDq3XOOFBz7L4XZKLDIXS+FeQqSAUMo2otmU+Vkj
67   * 0KorshMjbUXfE1KkTijTMJlaga2M2xVVt21fRIkJNWbIL0dWFLWyRq7OXdygyFkI
68   * iW9b2/LYaePBgET22kbtHSCAEj+BlSf265+1rNxyAXBGGGccCKzEbcqASBKHOgVp
69   * 6pLqlQAfuSy6g/OzGzces3zXRrGu1N3pBIzAIwCW429n52ZlYfYR0nr+REKDnRrP
70   * IIDsWASmEHhBezTD+v0qCJRyLz2usFgWY+7agUJE2yHHI2mTu2RAFngBilJXlMCt
71   * VwT0xGuQxkbHAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAEv8N7Xm8qaY2FgrOc6P
72   * a1GTgA+AOb3aU33TGwAR86f+nLf6BSPaohcQfOeJid7FkFuYInuXl+oqs+RqM/j8
73   * R0E5BuGYY2wOKpL/PbFi1yf/Kyvft7KVh8e1IUUec/i1DdYTDB0lNWvXXxjfMKGL
74   * ct3GMbEHKvLfHx42Iwz/+fva6LUrO4u2TDfv0ycHuR7UZEuC1DJ4xtFhbpq/QRAj
75   * CyfNx3cDc7L2EtJWnCmivTFA9l8MF1ZPMDSVd4ecQ7B0xZIFQ5cSSFt7WGaJCsGM
76   * zYkU4Fp4IykQcWxdlNX7wJZRwQ2TZJFFglpTiFZdeq6I6Ad9An1Encpz5W8UJ4tv
77   * hmw=
78   * -----END CERTIFICATE-----
79   * </pre>
80   * </p>
81   */
82  public final class FingerprintTrustManagerFactory extends SimpleTrustManagerFactory {
83  
84      private static final Pattern FINGERPRINT_PATTERN = Pattern.compile("^[0-9a-fA-F:]+$");
85      private static final Pattern FINGERPRINT_STRIP_PATTERN = Pattern.compile(":");
86  
87      /**
88       * Creates a builder for {@link FingerprintTrustManagerFactory}.
89       *
90       * @param algorithm a hash algorithm
91       * @return a builder
92       */
93      public static FingerprintTrustManagerFactoryBuilder builder(String algorithm) {
94          return new FingerprintTrustManagerFactoryBuilder(algorithm);
95      }
96  
97      private final FastThreadLocal<MessageDigest> tlmd;
98  
99      private final TrustManager tm = new X509TrustManager() {
100 
101         @Override
102         public void checkClientTrusted(X509Certificate[] chain, String s) throws CertificateException {
103             checkTrusted("client", chain);
104         }
105 
106         @Override
107         public void checkServerTrusted(X509Certificate[] chain, String s) throws CertificateException {
108             checkTrusted("server", chain);
109         }
110 
111         private void checkTrusted(String type, X509Certificate[] chain) throws CertificateException {
112             X509Certificate cert = chain[0];
113             byte[] fingerprint = fingerprint(cert);
114             boolean found = false;
115             for (byte[] allowedFingerprint: fingerprints) {
116                 if (Arrays.equals(fingerprint, allowedFingerprint)) {
117                     found = true;
118                     break;
119                 }
120             }
121 
122             if (!found) {
123                 throw new CertificateException(
124                         type + " certificate with unknown fingerprint: " + cert.getSubjectDN());
125             }
126         }
127 
128         private byte[] fingerprint(X509Certificate cert) throws CertificateEncodingException {
129             MessageDigest md = tlmd.get();
130             md.reset();
131             return md.digest(cert.getEncoded());
132         }
133 
134         @Override
135         public X509Certificate[] getAcceptedIssuers() {
136             return EmptyArrays.EMPTY_X509_CERTIFICATES;
137         }
138     };
139 
140     private final byte[][] fingerprints;
141 
142     /**
143      * Creates a new instance.
144      *
145      * @param algorithm a hash algorithm
146      * @param fingerprints a list of fingerprints
147      */
148     FingerprintTrustManagerFactory(final String algorithm, byte[][] fingerprints) {
149         requireNonNull(algorithm, "algorithm");
150         requireNonNull(fingerprints, "fingerprints");
151 
152         if (fingerprints.length == 0) {
153             throw new IllegalArgumentException("No fingerprints provided");
154         }
155 
156         // check early if the hash algorithm is available
157         final MessageDigest md;
158         try {
159             md = MessageDigest.getInstance(algorithm);
160         } catch (NoSuchAlgorithmException e) {
161             throw new IllegalArgumentException(
162                     String.format("Unsupported hash algorithm: %s", algorithm), e);
163         }
164 
165         int hashLength = md.getDigestLength();
166         List<byte[]> list = new ArrayList<>(fingerprints.length);
167         for (byte[] f: fingerprints) {
168             if (f == null) {
169                 break;
170             }
171             if (f.length != hashLength) {
172                 throw new IllegalArgumentException(
173                         String.format("malformed fingerprint (length is %d but expected %d): %s",
174                                       f.length, hashLength, BufferUtil.hexDump(f)));
175             }
176             list.add(f.clone());
177         }
178 
179         tlmd = new FastThreadLocal<>() {
180 
181             @Override
182             protected MessageDigest initialValue() {
183                 try {
184                     return MessageDigest.getInstance(algorithm);
185                 } catch (NoSuchAlgorithmException e) {
186                     throw new IllegalArgumentException(
187                             String.format("Unsupported hash algorithm: %s", algorithm), e);
188                 }
189             }
190         };
191 
192         this.fingerprints = list.toArray(EmptyArrays.EMPTY_BYTES_BYTES);
193     }
194 
195     static byte[][] toFingerprintArray(Iterable<String> fingerprints) {
196         requireNonNull(fingerprints, "fingerprints");
197 
198         List<byte[]> list = new ArrayList<>();
199         for (String f: fingerprints) {
200             if (f == null) {
201                 break;
202             }
203 
204             if (!FINGERPRINT_PATTERN.matcher(f).matches()) {
205                 throw new IllegalArgumentException("malformed fingerprint: " + f);
206             }
207             f = FINGERPRINT_STRIP_PATTERN.matcher(f).replaceAll("");
208 
209             list.add(StringUtil.decodeHexDump(f));
210         }
211 
212         return list.toArray(EmptyArrays.EMPTY_BYTES_BYTES);
213     }
214 
215     @Override
216     protected void engineInit(KeyStore keyStore) throws Exception { }
217 
218     @Override
219     protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws Exception { }
220 
221     @Override
222     protected TrustManager[] engineGetTrustManagers() {
223         return new TrustManager[] { tm };
224     }
225 }