001/* 002 * Copyright 2015-2024 the original author or authors 003 * 004 * This software is licensed under the Apache License, Version 2.0, 005 * the GNU Lesser General Public License version 2 or later ("LGPL") 006 * and the WTFPL. 007 * You may choose either license to govern your use of this software only 008 * upon the condition that you accept all of the terms of either 009 * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. 010 */ 011package org.minidns.dane; 012 013import org.minidns.dnsname.DnsName; 014import org.minidns.dnssec.DnssecClient; 015import org.minidns.dnssec.DnssecQueryResult; 016import org.minidns.dnssec.DnssecUnverifiedReason; 017import org.minidns.dnssec.DnssecValidationFailedException; 018import org.minidns.record.CNAME; 019import org.minidns.record.Data; 020import org.minidns.record.Record; 021import org.minidns.record.TLSA; 022 023import javax.net.ssl.HttpsURLConnection; 024import javax.net.ssl.SSLContext; 025import javax.net.ssl.SSLPeerUnverifiedException; 026import javax.net.ssl.SSLSession; 027import javax.net.ssl.SSLSocket; 028import javax.net.ssl.TrustManager; 029import javax.net.ssl.X509TrustManager; 030 031import java.io.IOException; 032import java.security.KeyManagementException; 033import java.security.MessageDigest; 034import java.security.NoSuchAlgorithmException; 035import java.security.cert.Certificate; 036import java.security.cert.CertificateException; 037import java.security.cert.X509Certificate; 038import java.util.ArrayList; 039import java.util.List; 040import java.util.logging.Logger; 041 042/** 043 * A helper class to validate the usage of TLSA records. 044 */ 045public class DaneVerifier { 046 private static final Logger LOGGER = Logger.getLogger(DaneVerifier.class.getName()); 047 048 /** 049 * The maximum number of CNAMEs followed when looking up the TLSA RRs. This also prevents looping on CNAME loops. 050 */ 051 private static final int MAX_CNAME_CHAIN_LENGTH = 16; 052 053 private final DnssecClient client; 054 055 public DaneVerifier() { 056 this(new DnssecClient()); 057 } 058 059 public DaneVerifier(DnssecClient client) { 060 this.client = client; 061 } 062 063 /** 064 * Verifies the certificate chain in an active {@link SSLSocket}. The socket must be connected. 065 * 066 * @param socket A connected {@link SSLSocket} whose certificate chain shall be verified using DANE. 067 * @return Whether the DANE verification is the only requirement according to the TLSA record. 068 * If this method returns {@code false}, additional PKIX validation is required. 069 * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE, or if 070 * the DNSSEC validation of the TLSA response failed. 071 * @see #daneVerify(SSLSocket) 072 */ 073 public boolean verify(SSLSocket socket) throws CertificateException { 074 return daneVerify(socket).isFullyVerified(); 075 } 076 077 /** 078 * Verifies the certificate chain in an active {@link SSLSocket}. The socket must be connected. 079 * 080 * @param socket A connected {@link SSLSocket} whose certificate chain shall be verified using DANE. 081 * @return The result of the DANE verification. 082 * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE, or if 083 * the DNSSEC validation of the TLSA response failed. 084 */ 085 public DaneVerificationResult daneVerify(SSLSocket socket) throws CertificateException { 086 if (!socket.isConnected()) { 087 throw new IllegalStateException("Socket not yet connected."); 088 } 089 return daneVerify(socket.getSession()); 090 } 091 092 /** 093 * Verifies the certificate chain in an active {@link SSLSession}. 094 * 095 * @param session An active {@link SSLSession} whose certificate chain shall be verified using DANE. 096 * @return Whether the DANE verification is the only requirement according to the TLSA record. 097 * If this method returns {@code false}, additional PKIX validation is required. 098 * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE, or if 099 * the DNSSEC validation of the TLSA response failed. 100 * @see #daneVerify(SSLSession) 101 */ 102 public boolean verify(SSLSession session) throws CertificateException { 103 return daneVerify(session).isFullyVerified(); 104 } 105 106 /** 107 * Verifies the certificate chain in an active {@link SSLSession}. 108 * 109 * @param session An active {@link SSLSession} whose certificate chain shall be verified using DANE. 110 * @return The result of the DANE verification. 111 * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE, or if 112 * the DNSSEC validation of the TLSA response failed. 113 */ 114 public DaneVerificationResult daneVerify(SSLSession session) throws CertificateException { 115 try { 116 return daneVerify(convert(session.getPeerCertificates()), session.getPeerHost(), session.getPeerPort()); 117 } catch (SSLPeerUnverifiedException e) { 118 throw new CertificateException("Peer not verified", e); 119 } 120 } 121 122 /** 123 * Verifies a certificate chain to be valid when used with the given connection details using DANE. 124 * 125 * @param chain A certificate chain that should be verified using DANE. 126 * @param hostName The DNS name of the host this certificate chain belongs to. 127 * @param port The port number that was used to reach the server providing the certificate chain in question. 128 * @return Whether the DANE verification is the only requirement according to the TLSA record. 129 * If this method returns {@code false}, additional PKIX validation is required. 130 * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE, or if 131 * the DNSSEC validation of the TLSA response failed. 132 * @see #daneVerify(X509Certificate[], String, int) 133 */ 134 public boolean verifyCertificateChain(X509Certificate[] chain, String hostName, int port) throws CertificateException { 135 return daneVerify(chain, hostName, port).isFullyVerified(); 136 } 137 138 /** 139 * Verifies a certificate chain to be valid when used with the given connection details using DANE. 140 * <p> 141 * Unlike {@link #verifyCertificateChain(X509Certificate[], String, int)}, this method allows the caller to 142 * distinguish between DANE not being applicable and a TLSA record matching the certificate while still requiring 143 * PKIX validation, as it is the case for the certificate usage PKIX-EE (1). 144 * </p> 145 * 146 * @param chain A certificate chain that should be verified using DANE. 147 * @param hostName The DNS name of the host this certificate chain belongs to. 148 * @param port The port number that was used to reach the server providing the certificate chain in question. 149 * @return The result of the DANE verification. 150 * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE, or if 151 * the DNSSEC validation of the TLSA response failed. 152 */ 153 public DaneVerificationResult daneVerify(X509Certificate[] chain, String hostName, int port) throws CertificateException { 154 DnsName req = DnsName.from("_" + port + "._tcp." + hostName); 155 DnsName tlsaName = req; 156 List<Record<? extends Data>> answers; 157 int remainingCnames = MAX_CNAME_CHAIN_LENGTH; 158 while (true) { 159 DnssecQueryResult result; 160 try { 161 result = client.queryDnssec(tlsaName, Record.TYPE.TLSA); 162 } catch (DnssecValidationFailedException e) { 163 // A bogus TLSA response must cause the TLS connection to be aborted (RFC 6698 § 4.1). 164 throw new DaneCertificateException.DnssecValidationFailed(e); 165 } catch (IOException e) { 166 throw new RuntimeException(e); 167 } 168 // TODO: We previously used the AD bit here. This allowed non-DNSSEC aware clients to be plugged into 169 // DaneVerifier, which, in turn, allows to use a trusted forward as DNSSEC validator. Is this a good idea? 170 if (!result.isAuthenticData()) { 171 String msg = "Got TLSA response from DNS server, but was not signed properly."; 172 msg += " Reasons:"; 173 for (DnssecUnverifiedReason reason : result.getUnverifiedReasons()) { 174 msg += " " + reason; 175 } 176 LOGGER.info(msg); 177 return DaneVerificationResult.notAuthenticated; 178 } 179 answers = result.dnsQueryResult.response.answerSection; 180 181 // The TLSA RRs may be provisioned via CNAMEs (RFC 6698 § A.2.1). Since the response is authentic, all 182 // CNAME RRs in the answer section have been validated, so we can de-alias the queried name. 183 DnsName queriedName = tlsaName; 184 while (true) { 185 DnsName target = findCnameTarget(tlsaName, answers); 186 if (target == null) break; 187 if (remainingCnames-- == 0) { 188 LOGGER.warning("CNAME chain for " + req + " exceeds " + MAX_CNAME_CHAIN_LENGTH + " CNAMEs, not using TLSA RRs"); 189 return DaneVerificationResult.noUsableTlsaRecords; 190 } 191 tlsaName = target; 192 } 193 194 // If the response did not include the RRs of the CNAME target, e.g., because the target is in another 195 // zone, then query the target. 196 if (tlsaName.equals(queriedName) || containsTlsa(tlsaName, answers)) { 197 break; 198 } 199 } 200 201 List<DaneCertificateException.CertificateMismatch> certificateMismatchExceptions = new ArrayList<>(); 202 DaneVerificationResult verificationResult = DaneVerificationResult.noUsableTlsaRecords; 203 for (Record<? extends Data> record : answers) { 204 if (record.type == Record.TYPE.TLSA && record.name.equals(tlsaName)) { 205 TLSA tlsa = (TLSA) record.payloadData; 206 DaneVerificationResult recordResult; 207 try { 208 recordResult = checkCertificateMatches(chain[0], tlsa, hostName); 209 } catch (DaneCertificateException.CertificateMismatch certificateMismatchException) { 210 // Record the mismatch and only throw an exception if no 211 // TLSA RR is able to verify the cert. This allows for TLSA 212 // certificate rollover. 213 certificateMismatchExceptions.add(certificateMismatchException); 214 continue; 215 } 216 217 if (recordResult == DaneVerificationResult.verified) { 218 return recordResult; 219 } 220 if (recordResult == DaneVerificationResult.matchedPkixRequired) { 221 // Keep looking, as another TLSA RR may make PKIX validation unnecessary. 222 verificationResult = recordResult; 223 } 224 } 225 } 226 227 if (!verificationResult.isDaneMatch() && !certificateMismatchExceptions.isEmpty()) { 228 throw new DaneCertificateException.MultipleCertificateMismatchExceptions(certificateMismatchExceptions); 229 } 230 231 return verificationResult; 232 } 233 234 private static DnsName findCnameTarget(DnsName name, List<Record<? extends Data>> records) { 235 for (Record<? extends Data> record : records) { 236 if (record.type == Record.TYPE.CNAME && record.name.equals(name)) { 237 return ((CNAME) record.payloadData).target; 238 } 239 } 240 return null; 241 } 242 243 private static boolean containsTlsa(DnsName name, List<Record<? extends Data>> records) { 244 for (Record<? extends Data> record : records) { 245 if (record.type == Record.TYPE.TLSA && record.name.equals(name)) { 246 return true; 247 } 248 } 249 return false; 250 } 251 252 /** 253 * Check if the given certificate matches the given TLSA record. 254 * 255 * @param cert The certificate to check. 256 * @param tlsa The TLSA record to check the certificate against. 257 * @param hostName The DNS name of the host the certificate belongs to, used for logging. 258 * @return {@link DaneVerificationResult#verified} or {@link DaneVerificationResult#matchedPkixRequired} if the 259 * certificate matches, or {@link DaneVerificationResult#noUsableTlsaRecords} if the TLSA record is not usable. 260 * @throws DaneCertificateException.CertificateMismatch if the TLSA record is usable but does not match. 261 */ 262 private static DaneVerificationResult checkCertificateMatches(X509Certificate cert, TLSA tlsa, String hostName) throws CertificateException { 263 if (tlsa.certUsage == null) { 264 LOGGER.warning("TLSA certificate usage byte " + tlsa.certUsageByte + " is not supported while verifying " + hostName); 265 return DaneVerificationResult.noUsableTlsaRecords; 266 } 267 268 switch (tlsa.certUsage) { 269 case serviceCertificateConstraint: // PKIX-EE 270 case domainIssuedCertificate: // DANE-EE 271 break; 272 case caConstraint: // PKIX-TA 273 case trustAnchorAssertion: // DANE-TA 274 default: 275 LOGGER.warning("TLSA certificate usage " + tlsa.certUsage + " (" + tlsa.certUsageByte + ") not supported while verifying " + hostName); 276 return DaneVerificationResult.noUsableTlsaRecords; 277 } 278 279 if (tlsa.selector == null) { 280 LOGGER.warning("TLSA selector byte " + tlsa.selectorByte + " is not supported while verifying " + hostName); 281 return DaneVerificationResult.noUsableTlsaRecords; 282 } 283 284 byte[] comp = null; 285 switch (tlsa.selector) { 286 case fullCertificate: 287 comp = cert.getEncoded(); 288 break; 289 case subjectPublicKeyInfo: 290 comp = cert.getPublicKey().getEncoded(); 291 break; 292 default: 293 LOGGER.warning("TLSA selector " + tlsa.selector + " (" + tlsa.selectorByte + ") not supported while verifying " + hostName); 294 return DaneVerificationResult.noUsableTlsaRecords; 295 } 296 297 if (tlsa.matchingType == null) { 298 LOGGER.warning("TLSA matching type byte " + tlsa.matchingTypeByte + " is not supported while verifying " + hostName); 299 return DaneVerificationResult.noUsableTlsaRecords; 300 } 301 302 switch (tlsa.matchingType) { 303 case noHash: 304 break; 305 case sha256: 306 try { 307 comp = MessageDigest.getInstance("SHA-256").digest(comp); 308 } catch (NoSuchAlgorithmException e) { 309 throw new CertificateException("Verification using TLSA failed: could not SHA-256 for matching", e); 310 } 311 break; 312 case sha512: 313 try { 314 comp = MessageDigest.getInstance("SHA-512").digest(comp); 315 } catch (NoSuchAlgorithmException e) { 316 throw new CertificateException("Verification using TLSA failed: could not SHA-512 for matching", e); 317 } 318 break; 319 default: 320 LOGGER.warning("TLSA matching type " + tlsa.matchingType + " not supported while verifying " + hostName); 321 return DaneVerificationResult.noUsableTlsaRecords; 322 } 323 324 boolean matches = tlsa.certificateAssociationEquals(comp); 325 if (!matches) { 326 throw new DaneCertificateException.CertificateMismatch(tlsa, comp); 327 } 328 329 // domain issued certificate does not require further verification, 330 // service certificate constraint does. 331 if (tlsa.certUsage == TLSA.CertUsage.domainIssuedCertificate) { 332 return DaneVerificationResult.verified; 333 } 334 return DaneVerificationResult.matchedPkixRequired; 335 } 336 337 /** 338 * Invokes {@link HttpsURLConnection#connect()} in a DANE verified fashion. 339 * This method must be called before {@link HttpsURLConnection#connect()} is invoked. 340 * 341 * If a SSLSocketFactory was set on this HttpsURLConnection, it will be ignored. You can use 342 * {@link #verifiedConnect(HttpsURLConnection, X509TrustManager)} to inject a custom {@link TrustManager}. 343 * 344 * @param conn connection to be connected. 345 * @return The {@link HttpsURLConnection} after being connected. 346 * @throws IOException when the connection could not be established. 347 * @throws CertificateException if there was an exception while verifying the certificate. 348 */ 349 public HttpsURLConnection verifiedConnect(HttpsURLConnection conn) throws IOException, CertificateException { 350 return verifiedConnect(conn, null); 351 } 352 353 /** 354 * Invokes {@link HttpsURLConnection#connect()} in a DANE verified fashion. 355 * This method must be called before {@link HttpsURLConnection#connect()} is invoked. 356 * 357 * If a SSLSocketFactory was set on this HttpsURLConnection, it will be ignored. 358 * 359 * @param conn connection to be connected. 360 * @param trustManager A non-default {@link TrustManager} to be used. 361 * @return The {@link HttpsURLConnection} after being connected. 362 * @throws IOException when the connection could not be established. 363 * @throws CertificateException if there was an exception while verifying the certificate. 364 */ 365 public HttpsURLConnection verifiedConnect(HttpsURLConnection conn, X509TrustManager trustManager) throws IOException, CertificateException { 366 try { 367 SSLContext context = SSLContext.getInstance("TLS"); 368 ExpectingTrustManager expectingTrustManager = new ExpectingTrustManager(trustManager); 369 context.init(null, new TrustManager[] {expectingTrustManager}, null); 370 conn.setSSLSocketFactory(context.getSocketFactory()); 371 conn.connect(); 372 boolean fullyVerified = verifyCertificateChain(convert(conn.getServerCertificates()), conn.getURL().getHost(), 373 conn.getURL().getPort() < 0 ? conn.getURL().getDefaultPort() : conn.getURL().getPort()); 374 // If fullyVerified is true then it's the DANE verification performed by verifiyCertificateChain() is 375 // sufficient to verify the certificate and we ignore possible pending exceptions of ExpectingTrustManager. 376 if (!fullyVerified && expectingTrustManager.hasException()) { 377 throw new IOException("Peer verification failed using PKIX", expectingTrustManager.getException()); 378 } 379 return conn; 380 } catch (NoSuchAlgorithmException | KeyManagementException e) { 381 throw new RuntimeException(e); 382 } 383 } 384 385 private static X509Certificate[] convert(Certificate[] certificates) { 386 List<X509Certificate> certs = new ArrayList<>(); 387 for (Certificate certificate : certificates) { 388 if (certificate instanceof X509Certificate) { 389 certs.add((X509Certificate) certificate); 390 } 391 } 392 return certs.toArray(new X509Certificate[certs.size()]); 393 } 394}