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}