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