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.netty.handler.ssl.util;
18  
19  import io.netty.buffer.ByteBufUtil;
20  import io.netty.buffer.Unpooled;
21  import io.netty.util.concurrent.FastThreadLocal;
22  import io.netty.util.internal.EmptyArrays;
23  import io.netty.util.internal.ObjectUtil;
24  import io.netty.util.internal.StringUtil;
25  
26  import javax.net.ssl.ManagerFactoryParameters;
27  import javax.net.ssl.TrustManager;
28  import javax.net.ssl.TrustManagerFactory;
29  import javax.net.ssl.X509TrustManager;
30  import java.security.KeyStore;
31  import java.security.MessageDigest;
32  import java.security.NoSuchAlgorithmException;
33  import java.security.cert.CertificateEncodingException;
34  import java.security.cert.CertificateException;
35  import java.security.cert.X509Certificate;
36  import java.util.ArrayList;
37  import java.util.Arrays;
38  import java.util.List;
39  import java.util.regex.Pattern;
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      * @deprecated This deprecated constructor uses SHA-1 that is considered insecure.
146      *      It is recommended to specify a stronger hash algorithm, such as SHA-256,
147      *      by calling {@link FingerprintTrustManagerFactory#builder(String)} method.
148      *
149      * @param fingerprints a list of SHA1 fingerprints in hexadecimal form
150      */
151     @Deprecated
152     public FingerprintTrustManagerFactory(Iterable<String> fingerprints) {
153         this("SHA1", toFingerprintArray(fingerprints));
154     }
155 
156     /**
157      * Creates a new instance.
158      *
159      * @deprecated This deprecated constructor uses SHA-1 that is considered insecure.
160      *      It is recommended to specify a stronger hash algorithm, such as SHA-256,
161      *      by calling {@link FingerprintTrustManagerFactory#builder(String)} method.
162      *
163      * @param fingerprints a list of SHA1 fingerprints in hexadecimal form
164      */
165     @Deprecated
166     public FingerprintTrustManagerFactory(String... fingerprints) {
167         this("SHA1", toFingerprintArray(Arrays.asList(fingerprints)));
168     }
169 
170     /**
171      * Creates a new instance.
172      *
173      * @deprecated This deprecated constructor uses SHA-1 that is considered insecure.
174      *      It is recommended to specify a stronger hash algorithm, such as SHA-256,
175      *      by calling {@link FingerprintTrustManagerFactory#builder(String)} method.
176      *
177      * @param fingerprints a list of SHA1 fingerprints
178      */
179     @Deprecated
180     public FingerprintTrustManagerFactory(byte[]... fingerprints) {
181         this("SHA1", fingerprints);
182     }
183 
184     /**
185      * Creates a new instance.
186      *
187      * @param algorithm a hash algorithm
188      * @param fingerprints a list of fingerprints
189      */
190     FingerprintTrustManagerFactory(final String algorithm, byte[][] fingerprints) {
191         ObjectUtil.checkNotNull(algorithm, "algorithm");
192         ObjectUtil.checkNotNull(fingerprints, "fingerprints");
193 
194         if (fingerprints.length == 0) {
195             throw new IllegalArgumentException("No fingerprints provided");
196         }
197 
198         // check early if the hash algorithm is available
199         final MessageDigest md;
200         try {
201             md = MessageDigest.getInstance(algorithm);
202         } catch (NoSuchAlgorithmException e) {
203             throw new IllegalArgumentException(
204                     String.format("Unsupported hash algorithm: %s", algorithm), e);
205         }
206 
207         int hashLength = md.getDigestLength();
208         List<byte[]> list = new ArrayList<byte[]>(fingerprints.length);
209         for (byte[] f: fingerprints) {
210             if (f == null) {
211                 break;
212             }
213             if (f.length != hashLength) {
214                 throw new IllegalArgumentException(
215                         String.format("malformed fingerprint (length is %d but expected %d): %s",
216                                       f.length, hashLength, ByteBufUtil.hexDump(Unpooled.wrappedBuffer(f))));
217             }
218             list.add(f.clone());
219         }
220 
221         this.tlmd = new FastThreadLocal<MessageDigest>() {
222 
223             @Override
224             protected MessageDigest initialValue() {
225                 try {
226                     return MessageDigest.getInstance(algorithm);
227                 } catch (NoSuchAlgorithmException e) {
228                     throw new IllegalArgumentException(
229                             String.format("Unsupported hash algorithm: %s", algorithm), e);
230                 }
231             }
232         };
233 
234         this.fingerprints = list.toArray(new byte[0][]);
235     }
236 
237     static byte[][] toFingerprintArray(Iterable<String> fingerprints) {
238         ObjectUtil.checkNotNull(fingerprints, "fingerprints");
239 
240         List<byte[]> list = new ArrayList<byte[]>();
241         for (String f: fingerprints) {
242             if (f == null) {
243                 break;
244             }
245 
246             if (!FINGERPRINT_PATTERN.matcher(f).matches()) {
247                 throw new IllegalArgumentException("malformed fingerprint: " + f);
248             }
249             f = FINGERPRINT_STRIP_PATTERN.matcher(f).replaceAll("");
250 
251             list.add(StringUtil.decodeHexDump(f));
252         }
253 
254         return list.toArray(new byte[0][]);
255     }
256 
257     @Override
258     protected void engineInit(KeyStore keyStore) throws Exception { }
259 
260     @Override
261     protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws Exception { }
262 
263     @Override
264     protected TrustManager[] engineGetTrustManagers() {
265         return new TrustManager[] { tm };
266     }
267 }