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 }