View Javadoc
1   /*
2    * Copyright 2023 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;
18  
19  import io.netty.util.internal.SuppressJava6Requirement;
20  
21  import java.net.Socket;
22  import java.security.cert.CertificateException;
23  import java.security.cert.X509Certificate;
24  import java.util.Collection;
25  import java.util.List;
26  import javax.naming.ldap.LdapName;
27  import javax.naming.ldap.Rdn;
28  import javax.net.ssl.ExtendedSSLSession;
29  import javax.net.ssl.SNIHostName;
30  import javax.net.ssl.SNIServerName;
31  import javax.net.ssl.SSLEngine;
32  import javax.net.ssl.SSLSession;
33  import javax.net.ssl.SSLSocket;
34  import javax.net.ssl.X509ExtendedTrustManager;
35  import javax.net.ssl.X509TrustManager;
36  import javax.security.auth.x500.X500Principal;
37  
38  /**
39   * Wraps an existing {@link X509ExtendedTrustManager} and enhances the {@link CertificateException} that is thrown
40   * because of hostname validation.
41   */
42  @SuppressJava6Requirement(reason = "Usage guarded by java version check")
43  final class EnhancingX509ExtendedTrustManager extends X509ExtendedTrustManager {
44  
45      // Constants for subject alt names of type DNS and IP. See X509Certificate#getSubjectAlternativeNames() javadocs.
46      static final int ALTNAME_DNS = 2;
47      static final int ALTNAME_URI = 6;
48      static final int ALTNAME_IP = 7;
49      private static final String SEPARATOR = ", ";
50  
51      private final X509ExtendedTrustManager wrapped;
52  
53      EnhancingX509ExtendedTrustManager(X509TrustManager wrapped) {
54          this.wrapped = (X509ExtendedTrustManager) wrapped;
55      }
56  
57      @Override
58      public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket)
59              throws CertificateException {
60          wrapped.checkClientTrusted(chain, authType, socket);
61      }
62  
63      @Override
64      public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket)
65              throws CertificateException {
66          try {
67              wrapped.checkServerTrusted(chain, authType, socket);
68          } catch (CertificateException e) {
69              throwEnhancedCertificateException(e, chain,
70                      socket instanceof SSLSocket ? ((SSLSocket) socket).getHandshakeSession() : null);
71          }
72      }
73  
74      @Override
75      public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
76              throws CertificateException {
77          wrapped.checkClientTrusted(chain, authType, engine);
78      }
79  
80      @Override
81      public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine)
82              throws CertificateException {
83          try {
84              wrapped.checkServerTrusted(chain, authType, engine);
85          } catch (CertificateException e) {
86              throwEnhancedCertificateException(e, chain, engine != null ? engine.getHandshakeSession() : null);
87          }
88      }
89  
90      @Override
91      public void checkClientTrusted(X509Certificate[] chain, String authType)
92              throws CertificateException {
93          wrapped.checkClientTrusted(chain, authType);
94      }
95  
96      @Override
97      public void checkServerTrusted(X509Certificate[] chain, String authType)
98              throws CertificateException {
99          try {
100             wrapped.checkServerTrusted(chain, authType);
101         } catch (CertificateException e) {
102             throwEnhancedCertificateException(e, chain, null);
103         }
104     }
105 
106     @Override
107     public X509Certificate[] getAcceptedIssuers() {
108         return wrapped.getAcceptedIssuers();
109     }
110 
111     private static void throwEnhancedCertificateException(CertificateException e, X509Certificate[] chain,
112                                                           SSLSession session) throws CertificateException {
113         // Matching the message is the best we can do sadly.
114         String message = e.getMessage();
115         if (message != null &&
116                 (message.startsWith("No subject alternative") || message.startsWith("No name matching"))) {
117             StringBuilder sb = new StringBuilder(128);
118             sb.append(message);
119             // Some exception messages from sun.security.util.HostnameChecker may end with a dot that we don't need
120             if (message.charAt(message.length() - 1) == '.') {
121                 sb.setLength(sb.length() - 1);
122             }
123             if (session != null) {
124                 sb.append(" for SNIHostName=").append(getSNIHostName(session))
125                         .append(" and peerHost=").append(session.getPeerHost());
126             }
127             sb.append(" in the chain of ").append(chain.length).append(" certificate(s):");
128             for (int i = 0; i < chain.length; i++) {
129                 X509Certificate cert = chain[i];
130                 Collection<List<?>> collection = cert.getSubjectAlternativeNames();
131                 sb.append(' ').append(i + 1).append(". subjectAlternativeNames=[");
132                 if (collection != null) {
133                     boolean hasNames = false;
134                     for (List<?> altNames : collection) {
135                         if (altNames.size() < 2) {
136                             // We expect at least a pair of 'nameType:value' in that list.
137                             continue;
138                         }
139                         final int nameType = ((Integer) altNames.get(0)).intValue();
140                         if (nameType == ALTNAME_DNS) {
141                             sb.append("DNS");
142                         } else if (nameType == ALTNAME_IP) {
143                             sb.append("IP");
144                         } else if (nameType == ALTNAME_URI) {
145                             // URI names are common in some environments with gRPC services that use SPIFFEs.
146                             // Though the hostname matcher won't be looking at them, having them there can help
147                             // debugging cases where hostname verification was enabled when it shouldn't be.
148                             sb.append("URI");
149                         } else {
150                             continue;
151                         }
152                         sb.append(':').append((String) altNames.get(1)).append(SEPARATOR);
153                         hasNames = true;
154                     }
155                     if (hasNames) {
156                         // Strip of the last separator
157                         sb.setLength(sb.length() - SEPARATOR.length());
158                     }
159                 }
160                 sb.append("], CN=").append(getCommonName(cert)).append('.');
161             }
162             throw new CertificateException(sb.toString(), e);
163         }
164         throw e;
165     }
166 
167     private static String getSNIHostName(SSLSession session) {
168         if (!(session instanceof ExtendedSSLSession)) {
169             return null;
170         }
171         List<SNIServerName> names = ((ExtendedSSLSession) session).getRequestedServerNames();
172         for (SNIServerName sni : names) {
173             if (sni instanceof SNIHostName) {
174                 SNIHostName hostName = (SNIHostName) sni;
175                 return hostName.getAsciiName();
176             }
177         }
178         return null;
179     }
180 
181     private static String getCommonName(X509Certificate cert) {
182         try {
183             // 1. Get the X500Principal (better than getSubjectDN which is implementation dependent and deprecated)
184             X500Principal principal = cert.getSubjectX500Principal();
185             // 2. Parse the DN using LdapName
186             LdapName ldapName = new LdapName(principal.getName());
187             // 3. Iterate over the Relative Distinguished Names (RDNs) to find CN
188             for (Rdn rdn : ldapName.getRdns()) {
189                 if (rdn.getType().equalsIgnoreCase("CN")) {
190                     return rdn.getValue().toString();
191                 }
192             }
193         } catch (Exception ignore) {
194             // ignore
195         }
196         return "null";
197     }
198 }