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    *   http://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.internal.EmptyArrays;
22  import io.netty.util.concurrent.FastThreadLocal;
23  import io.netty.util.internal.StringUtil;
24  
25  import javax.net.ssl.ManagerFactoryParameters;
26  import javax.net.ssl.TrustManager;
27  import javax.net.ssl.TrustManagerFactory;
28  import javax.net.ssl.X509TrustManager;
29  import java.security.KeyStore;
30  import java.security.MessageDigest;
31  import java.security.NoSuchAlgorithmException;
32  import java.security.cert.CertificateEncodingException;
33  import java.security.cert.CertificateException;
34  import java.security.cert.X509Certificate;
35  import java.util.ArrayList;
36  import java.util.Arrays;
37  import java.util.List;
38  import java.util.regex.Pattern;
39  
40  /**
41   * An {@link TrustManagerFactory} that trusts an X.509 certificate whose SHA1 checksum matches.
42   * <p>
43   * <strong>NOTE:</strong> It is recommended to verify certificates and their chain to prevent
44   * <a href="https://en.wikipedia.org/wiki/Man-in-the-middle_attack">Man-in-the-middle attacks</a>.
45   * This {@link TrustManagerFactory} will <strong>only</strong> verify that the fingerprint of certificates match one
46   * of the given fingerprints. This procedure is called
47   * <a href="https://en.wikipedia.org/wiki/Transport_Layer_Security#Certificate_pinning">certificate pinning</a> and
48   * is an effective protection. For maximum security one should verify that the whole certificate chain is as expected.
49   * It is worth mentioning that certain firewalls, proxies or other appliances found in corporate environments,
50   * actually perform Man-in-the-middle attacks and thus present a different certificate fingerprint.
51   * </p>
52   * <p>
53   * The SHA1 checksum of an X.509 certificate is calculated from its DER encoded format.  You can get the fingerprint of
54   * an X.509 certificate using the {@code openssl} command.  For example:
55   *
56   * <pre>
57   * $ openssl x509 -fingerprint -sha1 -in my_certificate.crt
58   * SHA1 Fingerprint=4E:85:10:55:BC:7B:12:08:D1:EA:0A:12:C9:72:EE:F3:AA:B2:C7:CB
59   * -----BEGIN CERTIFICATE-----
60   * MIIBqjCCAROgAwIBAgIJALiT3Nvp0kvmMA0GCSqGSIb3DQEBBQUAMBYxFDASBgNV
61   * BAMTC2V4YW1wbGUuY29tMCAXDTcwMDEwMTAwMDAwMFoYDzk5OTkxMjMxMjM1OTU5
62   * WjAWMRQwEgYDVQQDEwtleGFtcGxlLmNvbTCBnzANBgkqhkiG9w0BAQEFAAOBjQAw
63   * gYkCgYEAnadvODG0QCiHhaFZlLHtr5gLIkDQS8ErZ//KfqeCHTC/KJsl3xYFk0zG
64   * aCv2FcmkOlokm77qV8qOW2DZdND7WuYzX6nLVuLb+GYxZ7b45iMAbAajvGh8jc9U
65   * o07fUIahGqTDAIAGCWsoLUOQ9nMzO/8GRHcXJAeQ2MGY2VpCcv0CAwEAATANBgkq
66   * hkiG9w0BAQUFAAOBgQBpRCnmjmNM0D7yrpkUJpBTNiqinhKLbeOvPWm+YmdInUUs
67   * LoMu0mZ1IANemLwqbwJJ76fknngeB+YuVAj46SurvVCV6ekwHcbgpW1u063IRwKk
68   * tQhOBO0HQxldUS4+4MYv/kuvnKkbjfgh5qfWw89Kx4kD+cycpP4yPtgDGk8ZMA==
69   * -----END CERTIFICATE-----
70   * </pre>
71   * </p>
72   */
73  public final class FingerprintTrustManagerFactory extends SimpleTrustManagerFactory {
74  
75      private static final Pattern FINGERPRINT_PATTERN = Pattern.compile("^[0-9a-fA-F:]+$");
76      private static final Pattern FINGERPRINT_STRIP_PATTERN = Pattern.compile(":");
77      private static final int SHA1_BYTE_LEN = 20;
78      private static final int SHA1_HEX_LEN = SHA1_BYTE_LEN * 2;
79  
80      private static final FastThreadLocal<MessageDigest> tlmd = new FastThreadLocal<MessageDigest>() {
81          @Override
82          protected MessageDigest initialValue() {
83              try {
84                  return MessageDigest.getInstance("SHA1");
85              } catch (NoSuchAlgorithmException e) {
86                  // All Java implementation must have SHA1 digest algorithm.
87                  throw new Error(e);
88              }
89          }
90      };
91  
92      private final TrustManager tm = new X509TrustManager() {
93  
94          @Override
95          public void checkClientTrusted(X509Certificate[] chain, String s) throws CertificateException {
96              checkTrusted("client", chain);
97          }
98  
99          @Override
100         public void checkServerTrusted(X509Certificate[] chain, String s) throws CertificateException {
101             checkTrusted("server", chain);
102         }
103 
104         private void checkTrusted(String type, X509Certificate[] chain) throws CertificateException {
105             X509Certificate cert = chain[0];
106             byte[] fingerprint = fingerprint(cert);
107             boolean found = false;
108             for (byte[] allowedFingerprint: fingerprints) {
109                 if (Arrays.equals(fingerprint, allowedFingerprint)) {
110                     found = true;
111                     break;
112                 }
113             }
114 
115             if (!found) {
116                 throw new CertificateException(
117                         type + " certificate with unknown fingerprint: " + cert.getSubjectDN());
118             }
119         }
120 
121         private byte[] fingerprint(X509Certificate cert) throws CertificateEncodingException {
122             MessageDigest md = tlmd.get();
123             md.reset();
124             return md.digest(cert.getEncoded());
125         }
126 
127         @Override
128         public X509Certificate[] getAcceptedIssuers() {
129             return EmptyArrays.EMPTY_X509_CERTIFICATES;
130         }
131     };
132 
133     private final byte[][] fingerprints;
134 
135     /**
136      * Creates a new instance.
137      *
138      * @param fingerprints a list of SHA1 fingerprints in hexadecimal form
139      */
140     public FingerprintTrustManagerFactory(Iterable<String> fingerprints) {
141         this(toFingerprintArray(fingerprints));
142     }
143 
144     /**
145      * Creates a new instance.
146      *
147      * @param fingerprints a list of SHA1 fingerprints in hexadecimal form
148      */
149     public FingerprintTrustManagerFactory(String... fingerprints) {
150         this(toFingerprintArray(Arrays.asList(fingerprints)));
151     }
152 
153     /**
154      * Creates a new instance.
155      *
156      * @param fingerprints a list of SHA1 fingerprints
157      */
158     public FingerprintTrustManagerFactory(byte[]... fingerprints) {
159         if (fingerprints == null) {
160             throw new NullPointerException("fingerprints");
161         }
162 
163         List<byte[]> list = new ArrayList<byte[]>(fingerprints.length);
164         for (byte[] f: fingerprints) {
165             if (f == null) {
166                 break;
167             }
168             if (f.length != SHA1_BYTE_LEN) {
169                 throw new IllegalArgumentException("malformed fingerprint: " +
170                         ByteBufUtil.hexDump(Unpooled.wrappedBuffer(f)) + " (expected: SHA1)");
171             }
172             list.add(f.clone());
173         }
174 
175         this.fingerprints = list.toArray(new byte[list.size()][]);
176     }
177 
178     private static byte[][] toFingerprintArray(Iterable<String> fingerprints) {
179         if (fingerprints == null) {
180             throw new NullPointerException("fingerprints");
181         }
182 
183         List<byte[]> list = new ArrayList<byte[]>();
184         for (String f: fingerprints) {
185             if (f == null) {
186                 break;
187             }
188 
189             if (!FINGERPRINT_PATTERN.matcher(f).matches()) {
190                 throw new IllegalArgumentException("malformed fingerprint: " + f);
191             }
192             f = FINGERPRINT_STRIP_PATTERN.matcher(f).replaceAll("");
193             if (f.length() != SHA1_HEX_LEN) {
194                 throw new IllegalArgumentException("malformed fingerprint: " + f + " (expected: SHA1)");
195             }
196 
197             list.add(StringUtil.decodeHexDump(f));
198         }
199 
200         return list.toArray(new byte[list.size()][]);
201     }
202 
203     @Override
204     protected void engineInit(KeyStore keyStore) throws Exception { }
205 
206     @Override
207     protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws Exception { }
208 
209     @Override
210     protected TrustManager[] engineGetTrustManagers() {
211         return new TrustManager[] { tm };
212     }
213 }