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}