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.dnsmessage.DnsMessage; 014import org.minidns.dnsname.DnsName; 015import org.minidns.dnssec.DnssecClient; 016import org.minidns.dnssec.DnssecQueryResult; 017import org.minidns.dnssec.DnssecUnverifiedReason; 018import org.minidns.record.Data; 019import org.minidns.record.Record; 020import org.minidns.record.TLSA; 021 022import javax.net.ssl.HttpsURLConnection; 023import javax.net.ssl.SSLContext; 024import javax.net.ssl.SSLPeerUnverifiedException; 025import javax.net.ssl.SSLSession; 026import javax.net.ssl.SSLSocket; 027import javax.net.ssl.TrustManager; 028import javax.net.ssl.X509TrustManager; 029 030import java.io.IOException; 031import java.security.KeyManagementException; 032import java.security.MessageDigest; 033import java.security.NoSuchAlgorithmException; 034import java.security.cert.Certificate; 035import java.security.cert.CertificateException; 036import java.security.cert.X509Certificate; 037import java.util.ArrayList; 038import java.util.LinkedList; 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 private final DnssecClient client; 049 050 public DaneVerifier() { 051 this(new DnssecClient()); 052 } 053 054 public DaneVerifier(DnssecClient client) { 055 this.client = client; 056 } 057 058 /** 059 * Verifies the certificate chain in an active {@link SSLSocket}. The socket must be connected. 060 * 061 * @param socket A connected {@link SSLSocket} whose certificate chain shall be verified using DANE. 062 * @return Whether the DANE verification is the only requirement according to the TLSA record. 063 * If this method returns {@code false}, additional PKIX validation is required. 064 * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE. 065 */ 066 public boolean verify(SSLSocket socket) throws CertificateException { 067 if (!socket.isConnected()) { 068 throw new IllegalStateException("Socket not yet connected."); 069 } 070 return verify(socket.getSession()); 071 } 072 073 /** 074 * Verifies the certificate chain in an active {@link SSLSession}. 075 * 076 * @param session An active {@link SSLSession} whose certificate chain shall be verified using DANE. 077 * @return Whether the DANE verification is the only requirement according to the TLSA record. 078 * If this method returns {@code false}, additional PKIX validation is required. 079 * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE. 080 */ 081 public boolean verify(SSLSession session) throws CertificateException { 082 try { 083 return verifyCertificateChain(convert(session.getPeerCertificates()), session.getPeerHost(), session.getPeerPort()); 084 } catch (SSLPeerUnverifiedException e) { 085 throw new CertificateException("Peer not verified", e); 086 } 087 } 088 089 /** 090 * Verifies a certificate chain to be valid when used with the given connection details using DANE. 091 * 092 * @param chain A certificate chain that should be verified using DANE. 093 * @param hostName The DNS name of the host this certificate chain belongs to. 094 * @param port The port number that was used to reach the server providing the certificate chain in question. 095 * @return Whether the DANE verification is the only requirement according to the TLSA record. 096 * If this method returns {@code false}, additional PKIX validation is required. 097 * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE. 098 */ 099 public boolean verifyCertificateChain(X509Certificate[] chain, String hostName, int port) throws CertificateException { 100 DnsName req = DnsName.from("_" + port + "._tcp." + hostName); 101 DnssecQueryResult result; 102 try { 103 result = client.queryDnssec(req, Record.TYPE.TLSA); 104 } catch (IOException e) { 105 throw new RuntimeException(e); 106 } 107 DnsMessage res = result.dnsQueryResult.response; 108 // TODO: We previously used the AD bit here. This allowed non-DNSSEC aware clients to be plugged into 109 // DaneVerifier, which, in turn, allows to use a trusted forward as DNSSEC validator. Is this a good idea? 110 if (!result.isAuthenticData()) { 111 String msg = "Got TLSA response from DNS server, but was not signed properly."; 112 msg += " Reasons:"; 113 for (DnssecUnverifiedReason reason : result.getUnverifiedReasons()) { 114 msg += " " + reason; 115 } 116 LOGGER.info(msg); 117 return false; 118 } 119 120 List<DaneCertificateException.CertificateMismatch> certificateMismatchExceptions = new LinkedList<>(); 121 boolean verified = false; 122 for (Record<? extends Data> record : res.answerSection) { 123 if (record.type == Record.TYPE.TLSA && record.name.equals(req)) { 124 TLSA tlsa = (TLSA) record.payloadData; 125 try { 126 verified |= checkCertificateMatches(chain[0], tlsa, hostName); 127 } catch (DaneCertificateException.CertificateMismatch certificateMismatchException) { 128 // Record the mismatch and only throw an exception if no 129 // TLSA RR is able to verify the cert. This allows for TLSA 130 // certificate rollover. 131 certificateMismatchExceptions.add(certificateMismatchException); 132 } 133 if (verified) break; 134 } 135 } 136 137 if (!verified && !certificateMismatchExceptions.isEmpty()) { 138 throw new DaneCertificateException.MultipleCertificateMismatchExceptions(certificateMismatchExceptions); 139 } 140 141 return verified; 142 } 143 144 private static boolean checkCertificateMatches(X509Certificate cert, TLSA tlsa, String hostName) throws CertificateException { 145 if (tlsa.certUsage == null) { 146 LOGGER.warning("TLSA certificate usage byte " + tlsa.certUsageByte + " is not supported while verifying " + hostName); 147 return false; 148 } 149 150 switch (tlsa.certUsage) { 151 case serviceCertificateConstraint: // PKIX-EE 152 case domainIssuedCertificate: // DANE-EE 153 break; 154 case caConstraint: // PKIX-TA 155 case trustAnchorAssertion: // DANE-TA 156 default: 157 LOGGER.warning("TLSA certificate usage " + tlsa.certUsage + " (" + tlsa.certUsageByte + ") not supported while verifying " + hostName); 158 return false; 159 } 160 161 if (tlsa.selector == null) { 162 LOGGER.warning("TLSA selector byte " + tlsa.selectorByte + " is not supported while verifying " + hostName); 163 return false; 164 } 165 166 byte[] comp = null; 167 switch (tlsa.selector) { 168 case fullCertificate: 169 comp = cert.getEncoded(); 170 break; 171 case subjectPublicKeyInfo: 172 comp = cert.getPublicKey().getEncoded(); 173 break; 174 default: 175 LOGGER.warning("TLSA selector " + tlsa.selector + " (" + tlsa.selectorByte + ") not supported while verifying " + hostName); 176 return false; 177 } 178 179 if (tlsa.matchingType == null) { 180 LOGGER.warning("TLSA matching type byte " + tlsa.matchingTypeByte + " is not supported while verifying " + hostName); 181 return false; 182 } 183 184 switch (tlsa.matchingType) { 185 case noHash: 186 break; 187 case sha256: 188 try { 189 comp = MessageDigest.getInstance("SHA-256").digest(comp); 190 } catch (NoSuchAlgorithmException e) { 191 throw new CertificateException("Verification using TLSA failed: could not SHA-256 for matching", e); 192 } 193 break; 194 case sha512: 195 try { 196 comp = MessageDigest.getInstance("SHA-512").digest(comp); 197 } catch (NoSuchAlgorithmException e) { 198 throw new CertificateException("Verification using TLSA failed: could not SHA-512 for matching", e); 199 } 200 break; 201 default: 202 LOGGER.warning("TLSA matching type " + tlsa.matchingType + " not supported while verifying " + hostName); 203 return false; 204 } 205 206 boolean matches = tlsa.certificateAssociationEquals(comp); 207 if (!matches) { 208 throw new DaneCertificateException.CertificateMismatch(tlsa, comp); 209 } 210 211 // domain issued certificate does not require further verification, 212 // service certificate constraint does. 213 return tlsa.certUsage == TLSA.CertUsage.domainIssuedCertificate; 214 } 215 216 /** 217 * Invokes {@link HttpsURLConnection#connect()} in a DANE verified fashion. 218 * This method must be called before {@link HttpsURLConnection#connect()} is invoked. 219 * 220 * If a SSLSocketFactory was set on this HttpsURLConnection, it will be ignored. You can use 221 * {@link #verifiedConnect(HttpsURLConnection, X509TrustManager)} to inject a custom {@link TrustManager}. 222 * 223 * @param conn connection to be connected. 224 * @return The {@link HttpsURLConnection} after being connected. 225 * @throws IOException when the connection could not be established. 226 * @throws CertificateException if there was an exception while verifying the certificate. 227 */ 228 public HttpsURLConnection verifiedConnect(HttpsURLConnection conn) throws IOException, CertificateException { 229 return verifiedConnect(conn, null); 230 } 231 232 /** 233 * Invokes {@link HttpsURLConnection#connect()} in a DANE verified fashion. 234 * This method must be called before {@link HttpsURLConnection#connect()} is invoked. 235 * 236 * If a SSLSocketFactory was set on this HttpsURLConnection, it will be ignored. 237 * 238 * @param conn connection to be connected. 239 * @param trustManager A non-default {@link TrustManager} to be used. 240 * @return The {@link HttpsURLConnection} after being connected. 241 * @throws IOException when the connection could not be established. 242 * @throws CertificateException if there was an exception while verifying the certificate. 243 */ 244 public HttpsURLConnection verifiedConnect(HttpsURLConnection conn, X509TrustManager trustManager) throws IOException, CertificateException { 245 try { 246 SSLContext context = SSLContext.getInstance("TLS"); 247 ExpectingTrustManager expectingTrustManager = new ExpectingTrustManager(trustManager); 248 context.init(null, new TrustManager[] {expectingTrustManager}, null); 249 conn.setSSLSocketFactory(context.getSocketFactory()); 250 conn.connect(); 251 boolean fullyVerified = verifyCertificateChain(convert(conn.getServerCertificates()), conn.getURL().getHost(), 252 conn.getURL().getPort() < 0 ? conn.getURL().getDefaultPort() : conn.getURL().getPort()); 253 // If fullyVerified is true then it's the DANE verification performed by verifiyCertificateChain() is 254 // sufficient to verify the certificate and we ignore possible pending exceptions of ExpectingTrustManager. 255 if (!fullyVerified && expectingTrustManager.hasException()) { 256 throw new IOException("Peer verification failed using PKIX", expectingTrustManager.getException()); 257 } 258 return conn; 259 } catch (NoSuchAlgorithmException | KeyManagementException e) { 260 throw new RuntimeException(e); 261 } 262 } 263 264 private static X509Certificate[] convert(Certificate[] certificates) { 265 List<X509Certificate> certs = new ArrayList<>(); 266 for (Certificate certificate : certificates) { 267 if (certificate instanceof X509Certificate) { 268 certs.add((X509Certificate) certificate); 269 } 270 } 271 return certs.toArray(new X509Certificate[certs.size()]); 272 } 273}