diff --git a/app/build.gradle b/app/build.gradle index fe8a91cbc3..2d71889483 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -524,7 +524,7 @@ configurations.configureEach { exclude group: "com.atlassian.commonmark", module: "commonmark" // https://github.com/MiniDNS/minidns/issues/139 - //exclude group: "org.minidns", module: "minidns-dnssec" + exclude group: "org.minidns", module: "minidns-dnssec" } configurations.configureEach { @@ -590,7 +590,7 @@ dependencies { def jsonpath_version = "2.9.0" def css_version = "0.9.30" def jax_version = "2.3.0-jaxb-1.0.6" - def minidns_version = "1.1.1" + def minidns_version = "1.0.5" def openpgp_version = "12.0" def badge_version = "1.1.22" def bugsnag_version = "6.7.0" diff --git a/app/src/main/java/org/minidns/dane/DaneCertificateException.java b/app/src/main/java/org/minidns/dane/DaneCertificateException.java new file mode 100644 index 0000000000..3fd4bef550 --- /dev/null +++ b/app/src/main/java/org/minidns/dane/DaneCertificateException.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dane; + +import java.security.cert.CertificateException; +import java.util.Collections; +import java.util.List; + +import org.minidns.record.TLSA; + +public abstract class DaneCertificateException extends CertificateException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + protected DaneCertificateException() { + } + + protected DaneCertificateException(String message) { + super(message); + } + + public static class CertificateMismatch extends DaneCertificateException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public final TLSA tlsa; + public final byte[] computed; + + public CertificateMismatch(TLSA tlsa, byte[] computed) { + super("The TLSA RR does not match the certificate"); + this.tlsa = tlsa; + this.computed = computed; + } + } + + public static class MultipleCertificateMismatchExceptions extends DaneCertificateException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public final List certificateMismatchExceptions; + + public MultipleCertificateMismatchExceptions(List certificateMismatchExceptions) { + super("There where multiple CertificateMismatch exceptions because none of the TLSA RR does match the certificate"); + assert !certificateMismatchExceptions.isEmpty(); + this.certificateMismatchExceptions = Collections.unmodifiableList(certificateMismatchExceptions); + } + } +} diff --git a/app/src/main/java/org/minidns/dane/DaneVerifier.java b/app/src/main/java/org/minidns/dane/DaneVerifier.java new file mode 100644 index 0000000000..7d57da802a --- /dev/null +++ b/app/src/main/java/org/minidns/dane/DaneVerifier.java @@ -0,0 +1,280 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dane; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsname.DnsName; +import org.minidns.dnssec.DnssecClient; +import org.minidns.dnssec.DnssecQueryResult; +import org.minidns.dnssec.DnssecUnverifiedReason; +import org.minidns.record.Data; +import org.minidns.record.Record; +import org.minidns.record.CNAME; +import org.minidns.record.TLSA; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Logger; + +/** + * A helper class to validate the usage of TLSA records. + */ +public class DaneVerifier { + private static final Logger LOGGER = Logger.getLogger(DaneVerifier.class.getName()); + + private final DnssecClient client; + + public DaneVerifier() { + this(new DnssecClient()); + } + + public DaneVerifier(DnssecClient client) { + this.client = client; + } + + /** + * Verifies the certificate chain in an active {@link SSLSocket}. The socket must be connected. + * + * @param socket A connected {@link SSLSocket} whose certificate chain shall be verified using DANE. + * @return Whether the DANE verification is the only requirement according to the TLSA record. + * If this method returns {@code false}, additional PKIX validation is required. + * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE. + */ + public boolean verify(SSLSocket socket) throws CertificateException { + if (!socket.isConnected()) { + throw new IllegalStateException("Socket not yet connected."); + } + return verify(socket.getSession()); + } + + /** + * Verifies the certificate chain in an active {@link SSLSession}. + * + * @param session An active {@link SSLSession} whose certificate chain shall be verified using DANE. + * @return Whether the DANE verification is the only requirement according to the TLSA record. + * If this method returns {@code false}, additional PKIX validation is required. + * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE. + */ + public boolean verify(SSLSession session) throws CertificateException { + try { + return verifyCertificateChain(convert(session.getPeerCertificates()), session.getPeerHost(), session.getPeerPort()); + } catch (SSLPeerUnverifiedException e) { + throw new CertificateException("Peer not verified", e); + } + } + + /** + * Verifies a certificate chain to be valid when used with the given connection details using DANE. + * + * @param chain A certificate chain that should be verified using DANE. + * @param hostName The DNS name of the host this certificate chain belongs to. + * @param port The port number that was used to reach the server providing the certificate chain in question. + * @return Whether the DANE verification is the only requirement according to the TLSA record. + * If this method returns {@code false}, additional PKIX validation is required. + * @throws CertificateException if the certificate chain provided differs from the one enforced using DANE. + */ + public boolean verifyCertificateChain(X509Certificate[] chain, String hostName, int port) throws CertificateException { + DnsName req = DnsName.from("_" + port + "._tcp." + hostName); + DnssecQueryResult result; + try { + result = client.queryDnssec(req, Record.TYPE.TLSA); + } catch (IOException e) { + throw new RuntimeException(e); + } + DnsMessage res = result.dnsQueryResult.response; + // TODO: We previously used the AD bit here. This allowed non-DNSSEC aware clients to be plugged into + // DaneVerifier, which, in turn, allows to use a trusted forward as DNSSEC validator. Is this a good idea? + if (!result.isAuthenticData()) { + String msg = "Got TLSA response from DNS server, but was not signed properly."; + msg += " Reasons:"; + for (DnssecUnverifiedReason reason : result.getUnverifiedReasons()) { + msg += " " + reason; + } + LOGGER.info(msg); + return false; + } + + List certificateMismatchExceptions = new LinkedList<>(); + boolean verified = false; + for (Record record : res.answerSection) { + if (record.name.equals(req)) { + if (record.type == Record.TYPE.TLSA) { + TLSA tlsa = (TLSA) record.payloadData; + try { + verified |= checkCertificateMatches(chain[0], tlsa, hostName); + } catch (DaneCertificateException.CertificateMismatch certificateMismatchException) { + // Record the mismatch and only throw an exception if no + // TLSA RR is able to verify the cert. This allows for TLSA + // certificate rollover. + certificateMismatchExceptions.add(certificateMismatchException); + } + if (verified) break; + } + // https://github.com/MiniDNS/minidns/issues/140 + else if (record.type == Record.TYPE.CNAME) { + req = ((CNAME) record.payloadData).target; + } + } + } + + if (!verified && !certificateMismatchExceptions.isEmpty()) { + throw new DaneCertificateException.MultipleCertificateMismatchExceptions(certificateMismatchExceptions); + } + + return verified; + } + + private static boolean checkCertificateMatches(X509Certificate cert, TLSA tlsa, String hostName) throws CertificateException { + if (tlsa.certUsage == null) { + LOGGER.warning("TLSA certificate usage byte " + tlsa.certUsageByte + " is not supported while verifying " + hostName); + return false; + } + + switch (tlsa.certUsage) { + case serviceCertificateConstraint: // PKIX-EE + case domainIssuedCertificate: // DANE-EE + break; + case caConstraint: // PKIX-TA + case trustAnchorAssertion: // DANE-TA + default: + LOGGER.warning("TLSA certificate usage " + tlsa.certUsage + " (" + tlsa.certUsageByte + ") not supported while verifying " + hostName); + return false; + } + + if (tlsa.selector == null) { + LOGGER.warning("TLSA selector byte " + tlsa.selectorByte + " is not supported while verifying " + hostName); + return false; + } + + byte[] comp = null; + switch (tlsa.selector) { + case fullCertificate: + comp = cert.getEncoded(); + break; + case subjectPublicKeyInfo: + comp = cert.getPublicKey().getEncoded(); + break; + default: + LOGGER.warning("TLSA selector " + tlsa.selector + " (" + tlsa.selectorByte + ") not supported while verifying " + hostName); + return false; + } + + if (tlsa.matchingType == null) { + LOGGER.warning("TLSA matching type byte " + tlsa.matchingTypeByte + " is not supported while verifying " + hostName); + return false; + } + + switch (tlsa.matchingType) { + case noHash: + break; + case sha256: + try { + comp = MessageDigest.getInstance("SHA-256").digest(comp); + } catch (NoSuchAlgorithmException e) { + throw new CertificateException("Verification using TLSA failed: could not SHA-256 for matching", e); + } + break; + case sha512: + try { + comp = MessageDigest.getInstance("SHA-512").digest(comp); + } catch (NoSuchAlgorithmException e) { + throw new CertificateException("Verification using TLSA failed: could not SHA-512 for matching", e); + } + break; + default: + LOGGER.warning("TLSA matching type " + tlsa.matchingType + " not supported while verifying " + hostName); + return false; + } + + boolean matches = tlsa.certificateAssociationEquals(comp); + if (!matches) { + throw new DaneCertificateException.CertificateMismatch(tlsa, comp); + } + + // domain issued certificate does not require further verification, + // service certificate constraint does. + return tlsa.certUsage == TLSA.CertUsage.domainIssuedCertificate; + } + + /** + * Invokes {@link HttpsURLConnection#connect()} in a DANE verified fashion. + * This method must be called before {@link HttpsURLConnection#connect()} is invoked. + * + * If a SSLSocketFactory was set on this HttpsURLConnection, it will be ignored. You can use + * {@link #verifiedConnect(HttpsURLConnection, X509TrustManager)} to inject a custom {@link TrustManager}. + * + * @param conn connection to be connected. + * @return The {@link HttpsURLConnection} after being connected. + * @throws IOException when the connection could not be established. + * @throws CertificateException if there was an exception while verifying the certificate. + */ + public HttpsURLConnection verifiedConnect(HttpsURLConnection conn) throws IOException, CertificateException { + return verifiedConnect(conn, null); + } + + /** + * Invokes {@link HttpsURLConnection#connect()} in a DANE verified fashion. + * This method must be called before {@link HttpsURLConnection#connect()} is invoked. + * + * If a SSLSocketFactory was set on this HttpsURLConnection, it will be ignored. + * + * @param conn connection to be connected. + * @param trustManager A non-default {@link TrustManager} to be used. + * @return The {@link HttpsURLConnection} after being connected. + * @throws IOException when the connection could not be established. + * @throws CertificateException if there was an exception while verifying the certificate. + */ + public HttpsURLConnection verifiedConnect(HttpsURLConnection conn, X509TrustManager trustManager) throws IOException, CertificateException { + try { + SSLContext context = SSLContext.getInstance("TLS"); + ExpectingTrustManager expectingTrustManager = new ExpectingTrustManager(trustManager); + context.init(null, new TrustManager[] {expectingTrustManager}, null); + conn.setSSLSocketFactory(context.getSocketFactory()); + conn.connect(); + boolean fullyVerified = verifyCertificateChain(convert(conn.getServerCertificates()), conn.getURL().getHost(), + conn.getURL().getPort() < 0 ? conn.getURL().getDefaultPort() : conn.getURL().getPort()); + // If fullyVerified is true then it's the DANE verification performed by verifiyCertificateChain() is + // sufficient to verify the certificate and we ignore possible pending exceptions of ExpectingTrustManager. + if (!fullyVerified && expectingTrustManager.hasException()) { + throw new IOException("Peer verification failed using PKIX", expectingTrustManager.getException()); + } + return conn; + } catch (NoSuchAlgorithmException | KeyManagementException e) { + throw new RuntimeException(e); + } + } + + private static X509Certificate[] convert(Certificate[] certificates) { + List certs = new ArrayList<>(); + for (Certificate certificate : certificates) { + if (certificate instanceof X509Certificate) { + certs.add((X509Certificate) certificate); + } + } + return certs.toArray(new X509Certificate[certs.size()]); + } +} diff --git a/app/src/main/java/org/minidns/dane/ExpectingTrustManager.java b/app/src/main/java/org/minidns/dane/ExpectingTrustManager.java new file mode 100644 index 0000000000..699bafd9ba --- /dev/null +++ b/app/src/main/java/org/minidns/dane/ExpectingTrustManager.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dane; + +import javax.net.ssl.X509TrustManager; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +public class ExpectingTrustManager implements X509TrustManager { + private CertificateException exception; + private final X509TrustManager trustManager; + + /** + * Creates a new instance of ExpectingTrustManager. + * + * @param trustManager The {@link X509TrustManager} to be used for verification. + * {@code null} to use the system default. + */ + public ExpectingTrustManager(X509TrustManager trustManager) { + this.trustManager = trustManager == null ? X509TrustManagerUtil.getDefault() : trustManager; + } + + public boolean hasException() { + return exception != null; + } + + public CertificateException getException() { + CertificateException e = exception; + exception = null; + return e; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + trustManager.checkClientTrusted(chain, authType); + } catch (CertificateException e) { + exception = e; + } + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + try { + trustManager.checkServerTrusted(chain, authType); + } catch (CertificateException e) { + exception = e; + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return trustManager.getAcceptedIssuers(); + } +} diff --git a/app/src/main/java/org/minidns/dane/X509TrustManagerUtil.java b/app/src/main/java/org/minidns/dane/X509TrustManagerUtil.java new file mode 100644 index 0000000000..d09a9cb8d8 --- /dev/null +++ b/app/src/main/java/org/minidns/dane/X509TrustManagerUtil.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dane; + +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; + +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +public class X509TrustManagerUtil { + + public static X509TrustManager getDefault() { + return getDefault(null); + } + + public static X509TrustManager getDefault(KeyStore keyStore) { + String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm(); + TrustManagerFactory trustManagerFactory; + try { + trustManagerFactory = TrustManagerFactory.getInstance(defaultAlgorithm); + trustManagerFactory.init(keyStore); + } catch (NoSuchAlgorithmException | KeyStoreException e) { + throw new AssertionError(e); + } + + for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) { + if (trustManager instanceof X509TrustManager) { + return (X509TrustManager) trustManager; + } + } + throw new AssertionError("No trust manager for the default algorithm " + defaultAlgorithm + " found"); + } +} diff --git a/app/src/main/java/org/minidns/dnssec/DigestCalculator.java b/app/src/main/java/org/minidns/dnssec/DigestCalculator.java new file mode 100644 index 0000000000..af3ad42189 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/DigestCalculator.java @@ -0,0 +1,15 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +public interface DigestCalculator { + byte[] digest(byte[] bytes); +} diff --git a/app/src/main/java/org/minidns/dnssec/DnssecClient.java b/app/src/main/java/org/minidns/dnssec/DnssecClient.java new file mode 100644 index 0000000000..216541cfa9 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/DnssecClient.java @@ -0,0 +1,573 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import org.minidns.DnsCache; +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.dnssec.DnssecUnverifiedReason.NoActiveSignaturesReason; +import org.minidns.dnssec.DnssecUnverifiedReason.NoSecureEntryPointReason; +import org.minidns.dnssec.DnssecUnverifiedReason.NoSignaturesReason; +import org.minidns.dnssec.DnssecUnverifiedReason.NoTrustAnchorReason; +import org.minidns.dnssec.DnssecValidationFailedException.AuthorityDoesNotContainSoa; +import org.minidns.iterative.ReliableDnsClient; +import org.minidns.record.DLV; +import org.minidns.record.DNSKEY; +import org.minidns.record.DS; +import org.minidns.record.Data; +import org.minidns.record.DelegatingDnssecRR; +import org.minidns.record.NSEC; +import org.minidns.record.NSEC3; +import org.minidns.record.RRSIG; +import org.minidns.record.Record; +import org.minidns.record.Record.CLASS; +import org.minidns.record.Record.TYPE; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Date; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class DnssecClient extends ReliableDnsClient { + + /** + * The root zone's KSK. + * The ID of the current key is "Klajeyz", and the key tag value is "20326". + */ + private static final BigInteger rootEntryKey = new BigInteger("1628686155461064465348252249725010996177649738666492500572664444461532807739744536029771810659241049343994038053541290419968870563183856865780916376571550372513476957870843322273120879361960335192976656756972171258658400305760429696147778001233984421619267530978084631948434496468785021389956803104620471232008587410372348519229650742022804219634190734272506220018657920136902014393834092648785514548876370028925405557661759399901378816916683122474038734912535425670533237815676134840739565610963796427401855723026687073600445461090736240030247906095053875491225879656640052743394090544036297390104110989318819106653199917493"); + + private static final DnsName DEFAULT_DLV = DnsName.from("dlv.isc.org"); + + /** + * Create a new DNSSEC aware DNS client using the global default cache. + */ + public DnssecClient() { + this(DEFAULT_CACHE); + } + + /** + * Create a new DNSSEC aware DNS client with the given DNS cache. + * + * @param cache The backend DNS cache. + */ + public DnssecClient(DnsCache cache) { + super(cache); + addSecureEntryPoint(DnsName.ROOT, rootEntryKey.toByteArray()); + } + + /** + * Known secure entry points (SEPs). + */ + private final Map knownSeps = new ConcurrentHashMap<>(); + + private boolean stripSignatureRecords = true; + + /** + * The active DNSSEC Look-aside Validation Registry. May be null. + */ + private DnsName dlv; + + @Override + public DnsQueryResult query(Question q) throws IOException { + DnssecQueryResult dnssecQueryResult = queryDnssec(q); + if (!dnssecQueryResult.isAuthenticData()) { + // TODO: Refine exception. + throw new IOException(); + } + return dnssecQueryResult.dnsQueryResult; + } + + public DnssecQueryResult queryDnssec(CharSequence name, TYPE type) throws IOException { + Question q = new Question(name, type, CLASS.IN); + return queryDnssec(q); + } + + public DnssecQueryResult queryDnssec(Question q) throws IOException { + DnsQueryResult dnsQueryResult = super.query(q); + DnssecQueryResult dnssecQueryResult = performVerification(dnsQueryResult); + return dnssecQueryResult; + } + + private DnssecQueryResult performVerification(DnsQueryResult dnsQueryResult) throws IOException { + if (dnsQueryResult == null) return null; + + DnsMessage dnsMessage = dnsQueryResult.response; + DnsMessage.Builder messageBuilder = dnsMessage.asBuilder(); + + Set unverifiedReasons = verify(dnsMessage); + + messageBuilder.setAuthenticData(unverifiedReasons.isEmpty()); + + List> answers = dnsMessage.answerSection; + List> nameserverRecords = dnsMessage.authoritySection; + List> additionalResourceRecords = dnsMessage.additionalSection; + Set> signatures = new HashSet<>(); + Record.filter(signatures, RRSIG.class, answers); + Record.filter(signatures, RRSIG.class, nameserverRecords); + Record.filter(signatures, RRSIG.class, additionalResourceRecords); + + if (stripSignatureRecords) { + messageBuilder.setAnswers(stripSignatureRecords(answers)); + messageBuilder.setNameserverRecords(stripSignatureRecords(nameserverRecords)); + messageBuilder.setAdditionalResourceRecords(stripSignatureRecords(additionalResourceRecords)); + } + + return new DnssecQueryResult(messageBuilder.build(), dnsQueryResult, signatures, unverifiedReasons); + } + + private static List> stripSignatureRecords(List> records) { + if (records.isEmpty()) return records; + List> recordList = new ArrayList<>(records.size()); + for (Record record : records) { + if (record.type != TYPE.RRSIG) { + recordList.add(record); + } + } + return recordList; + } + + private Set verify(DnsMessage dnsMessage) throws IOException { + if (!dnsMessage.answerSection.isEmpty()) { + return verifyAnswer(dnsMessage); + } else { + return verifyNsec(dnsMessage); + } + } + + private Set verifyAnswer(DnsMessage dnsMessage) throws IOException { + Question q = dnsMessage.questions.get(0); + List> answers = dnsMessage.answerSection; + List> toBeVerified = dnsMessage.copyAnswers(); + VerifySignaturesResult verifiedSignatures = verifySignatures(q, answers, toBeVerified); + Set result = verifiedSignatures.reasons; + if (!result.isEmpty()) { + return result; + } + + // Keep SEPs separated, we only need one valid SEP. + boolean sepSignatureValid = false; + Set sepReasons = new HashSet<>(); + for (Iterator> iterator = toBeVerified.iterator(); iterator.hasNext(); ) { + Record record = iterator.next().ifPossibleAs(DNSKEY.class); + if (record == null) { + continue; + } + + // Verify all DNSKEYs as if it was a SEP. If we find a single SEP we are safe. + Set reasons = verifySecureEntryPoint(record); + if (reasons.isEmpty()) { + sepSignatureValid = true; + } else { + sepReasons.addAll(reasons); + } + if (!verifiedSignatures.sepSignaturePresent) { + LOGGER.finer("SEP key is not self-signed."); + } + iterator.remove(); + } + + if (verifiedSignatures.sepSignaturePresent && !sepSignatureValid) { + result.addAll(sepReasons); + } + if (verifiedSignatures.sepSignatureRequired && !verifiedSignatures.sepSignaturePresent) { + result.add(new NoSecureEntryPointReason(q.name)); + } + if (!toBeVerified.isEmpty()) { + if (toBeVerified.size() != answers.size()) { + throw new DnssecValidationFailedException(q, "Only some records are signed!"); + } else { + result.add(new NoSignaturesReason(q)); + } + } + return result; + } + + private Set verifyNsec(DnsMessage dnsMessage) throws IOException { + Set result = new HashSet<>(); + Question q = dnsMessage.questions.get(0); + boolean validNsec = false; + boolean nsecPresent = false; + + // Get the SOA RR that has to be in the authority section. Note that we will verify its signature later, after + // we have verified the NSEC3 RR. And although the data form the SOA RR is only required for NSEC3 we check for + // its existence here, since it would be invalid if there is none. + // TODO: Add a reference to the relevant RFC parts which specify that there has to be a SOA RR in X. + DnsName zone = null; + List> authoritySection = dnsMessage.authoritySection; + for (Record authorityRecord : authoritySection) { + if (authorityRecord.type == TYPE.SOA) { + zone = authorityRecord.name; + break; + } + } + if (zone == null) + throw new AuthorityDoesNotContainSoa(dnsMessage); + + // TODO Examine if it is better to verify the RRs in the authority section *before* we verify NSEC(3). We + // currently do it the other way around. + + // TODO: This whole logic needs to be changed. It currently checks one NSEC(3) record after another, when it + // should first determine if we are dealing with NSEC or NSEC3 and the verify the whole response. + for (Record record : authoritySection) { + DnssecUnverifiedReason reason; + + switch (record.type) { + case NSEC: + nsecPresent = true; + Record nsecRecord = record.as(NSEC.class); + reason = Verifier.verifyNsec(nsecRecord, q); + break; + case NSEC3: + nsecPresent = true; + Record nsec3Record = record.as(NSEC3.class); + reason = Verifier.verifyNsec3(zone, nsec3Record, q); + break; + default: + continue; + } + + if (reason != null) { + result.add(reason); + } else { + validNsec = true; + } + } + + // TODO: Shouldn't we also throw if !nsecPresent? + if (nsecPresent && !validNsec) { + throw new DnssecValidationFailedException(q, "Invalid NSEC!"); + } + + List> toBeVerified = dnsMessage.copyAuthority(); + VerifySignaturesResult verifiedSignatures = verifySignatures(q, authoritySection, toBeVerified); + if (validNsec && verifiedSignatures.reasons.isEmpty()) { + result.clear(); + } else { + result.addAll(verifiedSignatures.reasons); + } + + if (!toBeVerified.isEmpty() && toBeVerified.size() != authoritySection.size()) { + // TODO Refine this exception and include the missing toBeVerified RRs and the whole DnsMessage into it. + throw new DnssecValidationFailedException(q, "Only some resource records from the authority section are signed!"); + } + + return result; + } + + private static class VerifySignaturesResult { + boolean sepSignatureRequired = false; + boolean sepSignaturePresent = false; + Set reasons = new HashSet<>(); + } + + private VerifySignaturesResult verifySignatures(Question q, Collection> reference, List> toBeVerified) throws IOException { + final Date now = new Date(); + final List outdatedRrSigs = new LinkedList<>(); + VerifySignaturesResult result = new VerifySignaturesResult(); + final List> rrsigs = new ArrayList<>(toBeVerified.size()); + + for (Record recordToBeVerified : toBeVerified) { + Record record = recordToBeVerified.ifPossibleAs(RRSIG.class); + if (record == null) continue; + + RRSIG rrsig = record.payloadData; + if (rrsig.signatureExpiration.compareTo(now) < 0 || rrsig.signatureInception.compareTo(now) > 0) { + // This RRSIG is out of date, but there might be one that is not. + outdatedRrSigs.add(rrsig); + continue; + } + rrsigs.add(record); + } + + if (rrsigs.isEmpty()) { + if (!outdatedRrSigs.isEmpty()) { + result.reasons.add(new NoActiveSignaturesReason(q, outdatedRrSigs)); + } else { + // TODO: Check if QNAME results should have signatures and add a different reason if there are RRSIGs + // expected compared to when not. + result.reasons.add(new NoSignaturesReason(q)); + } + return result; + } + + for (Record sigRecord : rrsigs) { + RRSIG rrsig = sigRecord.payloadData; + + List> records = new ArrayList<>(reference.size()); + for (Record record : reference) { + if (record.type == rrsig.typeCovered && record.name.equals(sigRecord.name)) { + records.add(record); + } + } + + Set reasons = verifySignedRecords(q, rrsig, records); + result.reasons.addAll(reasons); + + if (q.name.equals(rrsig.signerName) && rrsig.typeCovered == TYPE.DNSKEY) { + for (Iterator> iterator = records.iterator(); iterator.hasNext(); ) { + Record dnsKeyRecord = iterator.next().ifPossibleAs(DNSKEY.class); + // dnsKeyRecord should never be null here. + DNSKEY dnskey = dnsKeyRecord.payloadData; + // DNSKEYs are verified separately, so don't mark them verified now. + iterator.remove(); + if (dnskey.getKeyTag() == rrsig.keyTag) { + result.sepSignaturePresent = true; + } + } + // DNSKEY's should be signed by a SEP + result.sepSignatureRequired = true; + } + + if (!isParentOrSelf(sigRecord.name.ace, rrsig.signerName.ace)) { + LOGGER.finer("Records at " + sigRecord.name + " are cross-signed with a key from " + rrsig.signerName); + } else { + toBeVerified.removeAll(records); + } + toBeVerified.remove(sigRecord); + } + return result; + } + + private static boolean isParentOrSelf(String child, String parent) { + if (child.equals(parent)) return true; + if (parent.isEmpty()) return true; + String[] childSplit = child.split("\\."); + String[] parentSplit = parent.split("\\."); + if (parentSplit.length > childSplit.length) return false; + for (int i = 1; i <= parentSplit.length; i++) { + if (!parentSplit[parentSplit.length - i].equals(childSplit[childSplit.length - i])) { + return false; + } + } + return true; + } + + private Set verifySignedRecords(Question q, RRSIG rrsig, List> records) throws IOException { + Set result = new HashSet<>(); + DNSKEY dnskey = null; + + if (rrsig.typeCovered == TYPE.DNSKEY) { + // Key must be present + List> dnskeyRrs = Record.filter(DNSKEY.class, records); + for (Record dnsKeyRecord : dnskeyRrs) { + if (dnsKeyRecord.payloadData.getKeyTag() == rrsig.keyTag) { + dnskey = dnsKeyRecord.payloadData; + break; + } + } + } else if (q.type == TYPE.DS && rrsig.signerName.equals(q.name)) { + // We should not probe for the self signed DS negative response, as it will be an endless loop. + result.add(new NoTrustAnchorReason(q.name)); + return result; + } else { + DnssecQueryResult dnskeyRes = queryDnssec(rrsig.signerName, TYPE.DNSKEY); + result.addAll(dnskeyRes.getUnverifiedReasons()); + List> dnskeyRrs = dnskeyRes.dnsQueryResult.response.filterAnswerSectionBy(DNSKEY.class); + for (Record dnsKeyRecord : dnskeyRrs) { + if (dnsKeyRecord.payloadData.getKeyTag() == rrsig.keyTag) { + dnskey = dnsKeyRecord.payloadData; + break; + } + } + } + + if (dnskey == null) { + throw new DnssecValidationFailedException(q, records.size() + " " + rrsig.typeCovered + " record(s) are signed using an unknown key."); + } + + DnssecUnverifiedReason unverifiedReason = Verifier.verify(records, rrsig, dnskey); + if (unverifiedReason != null) { + result.add(unverifiedReason); + } + + return result; + } + + private Set verifySecureEntryPoint(final Record sepRecord) throws IOException { + final DNSKEY dnskey = sepRecord.payloadData; + + Set unverifiedReasons = new HashSet<>(); + Set activeReasons = new HashSet<>(); + if (knownSeps.containsKey(sepRecord.name)) { + if (dnskey.keyEquals(knownSeps.get(sepRecord.name))) { + return unverifiedReasons; + } else { + unverifiedReasons.add(new DnssecUnverifiedReason.ConflictsWithSep(sepRecord)); + return unverifiedReasons; + } + } + + // If we are looking for the SEP of the root zone at this point, then the client was not + // configured with one and we can abort stating the reason. + if (sepRecord.name.isRootLabel()) { + unverifiedReasons.add(new DnssecUnverifiedReason.NoRootSecureEntryPointReason()); + return unverifiedReasons; + } + + DelegatingDnssecRR delegation = null; + DnssecQueryResult dsResp = queryDnssec(sepRecord.name, TYPE.DS); + unverifiedReasons.addAll(dsResp.getUnverifiedReasons()); + + List> dsRrs = dsResp.dnsQueryResult.response.filterAnswerSectionBy(DS.class); + for (Record dsRecord : dsRrs) { + DS ds = dsRecord.payloadData; + if (dnskey.getKeyTag() == ds.keyTag) { + delegation = ds; + activeReasons = dsResp.getUnverifiedReasons(); + break; + } + } + + if (delegation == null) { + LOGGER.fine("There is no DS record for " + sepRecord.name + ", server gives empty result"); + } + + if (delegation == null && dlv != null && !dlv.isChildOf(sepRecord.name)) { + DnssecQueryResult dlvResp = queryDnssec(DnsName.from(sepRecord.name, dlv), TYPE.DLV); + unverifiedReasons.addAll(dlvResp.getUnverifiedReasons()); + + List> dlvRrs = dlvResp.dnsQueryResult.response.filterAnswerSectionBy(DLV.class); + for (Record dlvRecord : dlvRrs) { + if (sepRecord.payloadData.getKeyTag() == dlvRecord.payloadData.keyTag) { + LOGGER.fine("Found DLV for " + sepRecord.name + ", awesome."); + delegation = dlvRecord.payloadData; + activeReasons = dlvResp.getUnverifiedReasons(); + break; + } + } + } + + if (delegation != null) { + DnssecUnverifiedReason unverifiedReason = Verifier.verify(sepRecord, delegation); + if (unverifiedReason != null) { + unverifiedReasons.add(unverifiedReason); + } else { + unverifiedReasons = activeReasons; + } + } else if (unverifiedReasons.isEmpty()) { + unverifiedReasons.add(new NoTrustAnchorReason(sepRecord.name)); + } + return unverifiedReasons; + } + + @Override + protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) { + message.getEdnsBuilder().setUdpPayloadSize(dataSource.getUdpPayloadSize()).setDnssecOk(); + message.setCheckingDisabled(true); + return super.newQuestion(message); + } + + @Override + protected String isResponseAcceptable(DnsMessage response) { + boolean dnssecOk = response.isDnssecOk(); + if (!dnssecOk) { + // This is a deliberate violation of RFC 6840 § 5.6. I doubt that + // "resolvers MUST ignore the DO bit in responses" does any good. Also we basically ignore the DO bit after + // the fall back to iterative mode. + return "DNSSEC OK (DO) flag not set in response"; + } + boolean checkingDisabled = response.checkingDisabled; + if (!checkingDisabled) { + return "CHECKING DISABLED (CD) flag not set in response"; + } + return super.isResponseAcceptable(response); + } + + /** + * Add a new secure entry point to the list of known secure entry points. + * + * A secure entry point acts as a trust anchor. By default, the only secure entry point is the key signing key + * provided by the root zone. + * + * @param name The domain name originating the key. Once the secure entry point for this domain is requested, + * the resolver will use this key without further verification instead of using the DNS system to + * verify the key. + * @param key The secure entry point corresponding to the domain name. This key can be retrieved by requesting + * the DNSKEY record for the domain and using the key with first flags bit set + * (also called key signing key) + */ + public void addSecureEntryPoint(DnsName name, byte[] key) { + knownSeps.put(name, key); + } + + /** + * Remove the secure entry point stored for a domain name. + * + * @param name The domain name of which the corresponding secure entry point shall be removed. For the root zone, + * use the empty string here. + */ + public void removeSecureEntryPoint(DnsName name) { + knownSeps.remove(name); + } + + /** + * Clears the list of known secure entry points. + * + * This will also remove the secure entry point of the root zone and + * thus render this instance useless until a new secure entry point is added. + */ + public void clearSecureEntryPoints() { + knownSeps.clear(); + } + + /** + * Whether signature records (RRSIG) are stripped from the resulting {@link DnsMessage}. + * + * Default is {@code true}. + * + * @return Whether signature records are stripped. + */ + public boolean isStripSignatureRecords() { + return stripSignatureRecords; + } + + /** + * Enable or disable stripping of signature records (RRSIG) from the result {@link DnsMessage}. + * @param stripSignatureRecords Whether signature records shall be stripped. + */ + public void setStripSignatureRecords(boolean stripSignatureRecords) { + this.stripSignatureRecords = stripSignatureRecords; + } + + /** + * Enables DNSSEC Lookaside Validation (DLV) using the default DLV service at dlv.isc.org. + */ + public void enableLookasideValidation() { + configureLookasideValidation(DEFAULT_DLV); + } + + /** + * Disables DNSSEC Lookaside Validation (DLV). + * DLV is disabled by default, this is only required if {@link #enableLookasideValidation()} was used before. + */ + public void disableLookasideValidation() { + configureLookasideValidation(null); + } + + /** + * Enables DNSSEC Lookaside Validation (DLV) using the given DLV service. + * + * @param dlv The domain name of the DLV service to be used or {@code null} to disable DLV. + */ + public void configureLookasideValidation(DnsName dlv) { + this.dlv = dlv; + } +} diff --git a/app/src/main/java/org/minidns/dnssec/DnssecQueryResult.java b/app/src/main/java/org/minidns/dnssec/DnssecQueryResult.java new file mode 100644 index 0000000000..41a9993ab8 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/DnssecQueryResult.java @@ -0,0 +1,53 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import java.util.Collections; +import java.util.Set; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsqueryresult.DnsQueryResult; +import org.minidns.record.RRSIG; +import org.minidns.record.Record; + +public class DnssecQueryResult { + + public final DnsMessage synthesizedResponse; + public final DnsQueryResult dnsQueryResult; + + private final Set> signatures; + private final Set dnssecUnverifiedReasons; + + DnssecQueryResult(DnsMessage synthesizedResponse, DnsQueryResult dnsQueryResult, Set> signatures, + Set dnssecUnverifiedReasons) { + this.synthesizedResponse = synthesizedResponse; + this.dnsQueryResult = dnsQueryResult; + this.signatures = Collections.unmodifiableSet(signatures); + if (dnssecUnverifiedReasons == null) { + this.dnssecUnverifiedReasons = Collections.emptySet(); + } else { + this.dnssecUnverifiedReasons = Collections.unmodifiableSet(dnssecUnverifiedReasons); + } + } + + public boolean isAuthenticData() { + return dnssecUnverifiedReasons.isEmpty(); + } + + public Set> getSignatures() { + return signatures; + } + + public Set getUnverifiedReasons() { + return dnssecUnverifiedReasons; + } + +} diff --git a/app/src/main/java/org/minidns/dnssec/DnssecResultNotAuthenticException.java b/app/src/main/java/org/minidns/dnssec/DnssecResultNotAuthenticException.java new file mode 100644 index 0000000000..b3c41b53e4 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/DnssecResultNotAuthenticException.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import java.util.Collections; +import java.util.Set; + +import org.minidns.MiniDnsException; + +public final class DnssecResultNotAuthenticException extends MiniDnsException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final Set unverifiedReasons; + + private DnssecResultNotAuthenticException(String message, Set unverifiedReasons) { + super(message); + if (unverifiedReasons.isEmpty()) { + throw new IllegalArgumentException(); + } + this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons); + } + + public static DnssecResultNotAuthenticException from(Set unverifiedReasons) { + StringBuilder sb = new StringBuilder(); + sb.append("DNSSEC result not authentic. Reasons: "); + for (DnssecUnverifiedReason reason : unverifiedReasons) { + sb.append(reason).append('.'); + } + + return new DnssecResultNotAuthenticException(sb.toString(), unverifiedReasons); + } + + public Set getUnverifiedReasons() { + return unverifiedReasons; + } +} diff --git a/app/src/main/java/org/minidns/dnssec/DnssecUnverifiedReason.java b/app/src/main/java/org/minidns/dnssec/DnssecUnverifiedReason.java new file mode 100644 index 0000000000..9f3df6221a --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/DnssecUnverifiedReason.java @@ -0,0 +1,175 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import java.util.Collections; +import java.util.List; + +import org.minidns.constants.DnssecConstants.DigestAlgorithm; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.record.DNSKEY; +import org.minidns.record.Data; +import org.minidns.record.RRSIG; +import org.minidns.record.Record; +import org.minidns.record.Record.TYPE; + +public abstract class DnssecUnverifiedReason { + public abstract String getReasonString(); + + @Override + public String toString() { + return getReasonString(); + } + + @Override + public int hashCode() { + return getReasonString().hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof DnssecUnverifiedReason && ((DnssecUnverifiedReason) obj).getReasonString().equals(getReasonString()); + } + + public static class AlgorithmNotSupportedReason extends DnssecUnverifiedReason { + private final String algorithm; + private final TYPE type; + private final Record record; + + public AlgorithmNotSupportedReason(byte algorithm, TYPE type, Record record) { + this.algorithm = Integer.toString(algorithm & 0xff); + this.type = type; + this.record = record; + } + + @Override + public String getReasonString() { + return type.name() + " algorithm " + algorithm + " required to verify " + record.name + " is unknown or not supported by platform"; + } + } + + public static class AlgorithmExceptionThrownReason extends DnssecUnverifiedReason { + private final int algorithmNumber; + private final String kind; + private final Exception reason; + private final Record record; + + public AlgorithmExceptionThrownReason(DigestAlgorithm algorithm, String kind, Record record, Exception reason) { + this.algorithmNumber = algorithm.value; + this.kind = kind; + this.record = record; + this.reason = reason; + } + + @Override + public String getReasonString() { + return kind + " algorithm " + algorithmNumber + " threw exception while verifying " + record.name + ": " + reason; + } + } + + public static class ConflictsWithSep extends DnssecUnverifiedReason { + private final Record record; + + public ConflictsWithSep(Record record) { + this.record = record; + } + + @Override + public String getReasonString() { + return "Zone " + record.name.ace + " is in list of known SEPs, but DNSKEY from response mismatches!"; + } + } + + public static class NoTrustAnchorReason extends DnssecUnverifiedReason { + private final DnsName zone; + + public NoTrustAnchorReason(DnsName zone) { + this.zone = zone; + } + + @Override + public String getReasonString() { + return "No trust anchor was found for zone " + zone + ". Try enabling DLV"; + } + } + + public static class NoSecureEntryPointReason extends DnssecUnverifiedReason { + private final DnsName zone; + + public NoSecureEntryPointReason(DnsName zone) { + this.zone = zone; + } + + @Override + public String getReasonString() { + return "No secure entry point was found for zone " + zone; + } + } + + public static class NoRootSecureEntryPointReason extends DnssecUnverifiedReason { + public NoRootSecureEntryPointReason() { + } + + @Override + public String getReasonString() { + return "No secure entry point was found for the root zone (\"Did you forget to configure a root SEP?\")"; + } + } + + public static class NoSignaturesReason extends DnssecUnverifiedReason { + private final Question question; + + public NoSignaturesReason(Question question) { + this.question = question; + } + + @Override + public String getReasonString() { + return "No signatures were attached to answer on question for " + question.type + " at " + question.name; + } + } + + public static class NoActiveSignaturesReason extends DnssecUnverifiedReason { + private final Question question; + private final List outdatedRrSigs; + + public NoActiveSignaturesReason(Question question, List outdatedRrSigs) { + this.question = question; + assert !outdatedRrSigs.isEmpty(); + this.outdatedRrSigs = Collections.unmodifiableList(outdatedRrSigs); + } + + @Override + public String getReasonString() { + return "No currently active signatures were attached to answer on question for " + question.type + " at " + question.name; + } + + public List getOutdatedRrSigs() { + return outdatedRrSigs; + } + } + + public static class NSECDoesNotMatchReason extends DnssecUnverifiedReason { + private final Question question; + private final Record record; + + public NSECDoesNotMatchReason(Question question, Record record) { + this.question = question; + this.record = record; + } + + @Override + public String getReasonString() { + return "NSEC " + record.name + " does nat match question for " + question.type + " at " + question.name; + } + } +} diff --git a/app/src/main/java/org/minidns/dnssec/DnssecValidationFailedException.java b/app/src/main/java/org/minidns/dnssec/DnssecValidationFailedException.java new file mode 100644 index 0000000000..1d5982f3ad --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/DnssecValidationFailedException.java @@ -0,0 +1,104 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import org.minidns.dnsmessage.DnsMessage; +import org.minidns.dnsmessage.Question; +import org.minidns.record.Data; +import org.minidns.record.Record; + +import java.io.IOException; +import java.security.spec.InvalidKeySpecException; +import java.util.List; + +public class DnssecValidationFailedException extends IOException { + private static final long serialVersionUID = 5413184667629832742L; + + public DnssecValidationFailedException(Question question, String reason) { + super("Validation of request to " + question + " failed: " + reason); + } + + public DnssecValidationFailedException(String message) { + super(message); + } + + public DnssecValidationFailedException(String message, Throwable cause) { + super(message, cause); + } + + public DnssecValidationFailedException(Record record, String reason) { + super("Validation of record " + record + " failed: " + reason); + } + + public DnssecValidationFailedException(List> records, String reason) { + super("Validation of " + records.size() + " " + records.get(0).type + " record" + (records.size() > 1 ? "s" : "") + " failed: " + reason); + } + + public static class DataMalformedException extends DnssecValidationFailedException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final byte[] data; + + public DataMalformedException(IOException exception, byte[] data) { + super("Malformed data", exception); + this.data = data; + } + + public DataMalformedException(String message, IOException exception, byte[] data) { + super(message, exception); + this.data = data; + } + + public byte[] getData() { + return data; + } + } + + public static class DnssecInvalidKeySpecException extends DnssecValidationFailedException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + public DnssecInvalidKeySpecException(InvalidKeySpecException exception) { + super("Invalid key spec", exception); + } + + public DnssecInvalidKeySpecException(String message, InvalidKeySpecException exception, byte[] data) { + super(message, exception); + } + + } + + public static class AuthorityDoesNotContainSoa extends DnssecValidationFailedException { + + /** + * + */ + private static final long serialVersionUID = 1L; + + private final DnsMessage response; + + public AuthorityDoesNotContainSoa(DnsMessage response) { + super("Autority does not contain SOA"); + this.response = response; + } + + public DnsMessage getResponse() { + return response; + } + } +} diff --git a/app/src/main/java/org/minidns/dnssec/DnssecValidatorInitializationException.java b/app/src/main/java/org/minidns/dnssec/DnssecValidatorInitializationException.java new file mode 100644 index 0000000000..0340fb4698 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/DnssecValidatorInitializationException.java @@ -0,0 +1,19 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +public class DnssecValidatorInitializationException extends RuntimeException { + private static final long serialVersionUID = -1464257268053507791L; + + public DnssecValidatorInitializationException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/app/src/main/java/org/minidns/dnssec/SignatureVerifier.java b/app/src/main/java/org/minidns/dnssec/SignatureVerifier.java new file mode 100644 index 0000000000..d665fc9bf9 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/SignatureVerifier.java @@ -0,0 +1,18 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; + +public interface SignatureVerifier { + boolean verify(byte[] content, RRSIG rrsig, DNSKEY key) throws DnssecValidationFailedException; +} diff --git a/app/src/main/java/org/minidns/dnssec/Verifier.java b/app/src/main/java/org/minidns/dnssec/Verifier.java new file mode 100644 index 0000000000..ee33a95698 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/Verifier.java @@ -0,0 +1,219 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec; + +import org.minidns.dnslabel.DnsLabel; +import org.minidns.dnsmessage.Question; +import org.minidns.dnsname.DnsName; +import org.minidns.dnssec.DnssecUnverifiedReason.AlgorithmExceptionThrownReason; +import org.minidns.dnssec.DnssecUnverifiedReason.AlgorithmNotSupportedReason; +import org.minidns.dnssec.DnssecUnverifiedReason.NSECDoesNotMatchReason; +import org.minidns.dnssec.algorithms.AlgorithmMap; +import org.minidns.record.DNSKEY; +import org.minidns.record.Data; +import org.minidns.record.DelegatingDnssecRR; +import org.minidns.record.NSEC; +import org.minidns.record.NSEC3; +import org.minidns.record.RRSIG; +import org.minidns.record.Record; +import org.minidns.util.Base32; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +class Verifier { + private static final AlgorithmMap algorithmMap = AlgorithmMap.INSTANCE; + + public static DnssecUnverifiedReason verify(Record dnskeyRecord, DelegatingDnssecRR ds) throws DnssecValidationFailedException { + DNSKEY dnskey = dnskeyRecord.payloadData; + DigestCalculator digestCalculator = algorithmMap.getDsDigestCalculator(ds.digestType); + if (digestCalculator == null) { + return new AlgorithmNotSupportedReason(ds.digestTypeByte, ds.getType(), dnskeyRecord); + } + + byte[] dnskeyData = dnskey.toByteArray(); + byte[] dnskeyOwner = dnskeyRecord.name.getBytes(); + byte[] combined = new byte[dnskeyOwner.length + dnskeyData.length]; + System.arraycopy(dnskeyOwner, 0, combined, 0, dnskeyOwner.length); + System.arraycopy(dnskeyData, 0, combined, dnskeyOwner.length, dnskeyData.length); + byte[] digest; + try { + digest = digestCalculator.digest(combined); + } catch (Exception e) { + return new AlgorithmExceptionThrownReason(ds.digestType, "DS", dnskeyRecord, e); + } + + if (!ds.digestEquals(digest)) { + // TODO: Add 'ds' and 'digest' to this exception, and rename the exception to "DigestComparisionFailedException". + throw new DnssecValidationFailedException(dnskeyRecord, "SEP is not properly signed by parent DS!"); + } + return null; + } + + public static DnssecUnverifiedReason verify(List> records, RRSIG rrsig, DNSKEY key) throws IOException { + SignatureVerifier signatureVerifier = algorithmMap.getSignatureVerifier(rrsig.algorithm); + if (signatureVerifier == null) { + return new AlgorithmNotSupportedReason(rrsig.algorithmByte, rrsig.getType(), records.get(0)); + } + + byte[] combine = combine(rrsig, records); + if (signatureVerifier.verify(combine, rrsig, key)) { + return null; + } else { + throw new DnssecValidationFailedException(records, "Signature is invalid."); + } + } + + public static DnssecUnverifiedReason verifyNsec(Record nsecRecord, Question q) { + NSEC nsec = nsecRecord.payloadData; + if (nsecRecord.name.equals(q.name) && !nsec.types.contains(q.type)) { + // records with same name but different types exist + return null; + } else if (nsecMatches(q.name, nsecRecord.name, nsec.next)) { + return null; + } + return new NSECDoesNotMatchReason(q, nsecRecord); + } + + public static DnssecUnverifiedReason verifyNsec3(DnsName zone, Record nsec3record, Question q) { + NSEC3 nsec3 = nsec3record.payloadData; + DigestCalculator digestCalculator = algorithmMap.getNsecDigestCalculator(nsec3.hashAlgorithm); + if (digestCalculator == null) { + return new AlgorithmNotSupportedReason(nsec3.hashAlgorithmByte, nsec3.getType(), nsec3record); + } + + byte[] bytes = nsec3hash(digestCalculator, nsec3, q.name, nsec3.iterations); + String s = Base32.encodeToString(bytes); + DnsName computedNsec3Record = DnsName.from(s + "." + zone); + if (nsec3record.name.equals(computedNsec3Record)) { + if (nsec3.types.contains(q.type)) { + // TODO: Refine exception thrown in this case. + return new NSECDoesNotMatchReason(q, nsec3record); + } + return null; + } + if (nsecMatches(s, nsec3record.name.getHostpart(), Base32.encodeToString(nsec3.getNextHashed()))) { + return null; + } + return new NSECDoesNotMatchReason(q, nsec3record); + } + + static byte[] combine(RRSIG rrsig, List> records) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + // Write RRSIG without signature + try { + rrsig.writePartialSignature(dos); + + DnsName sigName = records.get(0).name; + if (!sigName.isRootLabel()) { + if (sigName.getLabelCount() < rrsig.labels) { + // TODO: This is currently not covered by the unit tests. + throw new DnssecValidationFailedException("Invalid RRsig record"); + } + + if (sigName.getLabelCount() > rrsig.labels) { + // TODO: This is currently not covered by the unit tests. + // Expand wildcards + sigName = DnsName.from(DnsLabel.WILDCARD_LABEL, sigName.stripToLabels(rrsig.labels)); + } + } + + List recordBytes = new ArrayList<>(records.size()); + for (Record record : records) { + Record ref = new Record(sigName, record.type, record.clazzValue, rrsig.originalTtl, record.payloadData); + recordBytes.add(ref.toByteArray()); + } + + // Sort correctly (cause they might be ordered randomly) as per RFC 4034 § 6.3. + final int offset = sigName.size() + 10; // Where the RDATA begins + Collections.sort(recordBytes, new Comparator() { + @Override + public int compare(byte[] b1, byte[] b2) { + for (int i = offset; i < b1.length && i < b2.length; i++) { + if (b1[i] != b2[i]) { + return (b1[i] & 0xFF) - (b2[i] & 0xFF); + } + } + return b1.length - b2.length; + } + }); + + for (byte[] recordByte : recordBytes) { + dos.write(recordByte); + } + dos.flush(); + } catch (IOException e) { + // Never happens + throw new RuntimeException(e); + } + return bos.toByteArray(); + } + + static boolean nsecMatches(String test, String lowerBound, String upperBound) { + return nsecMatches(DnsName.from(test), DnsName.from(lowerBound), DnsName.from(upperBound)); + } + + /** + * Tests if a nsec domain name is part of an NSEC record. + * + * @param test test domain name + * @param lowerBound inclusive lower bound + * @param upperBound exclusive upper bound + * @return test domain name is covered by NSEC record + */ + static boolean nsecMatches(DnsName test, DnsName lowerBound, DnsName upperBound) { + int lowerParts = lowerBound.getLabelCount(); + int upperParts = upperBound.getLabelCount(); + int testParts = test.getLabelCount(); + + if (testParts > lowerParts && !test.isChildOf(lowerBound) && test.stripToLabels(lowerParts).compareTo(lowerBound) < 0) + return false; + if (testParts <= lowerParts && test.compareTo(lowerBound.stripToLabels(testParts)) < 0) + return false; + + if (testParts > upperParts && !test.isChildOf(upperBound) && test.stripToLabels(upperParts).compareTo(upperBound) > 0) + return false; + if (testParts <= upperParts && test.compareTo(upperBound.stripToLabels(testParts)) >= 0) + return false; + + return true; + } + + static byte[] nsec3hash(DigestCalculator digestCalculator, NSEC3 nsec3, DnsName ownerName, int iterations) { + return nsec3hash(digestCalculator, nsec3.getSalt(), ownerName.getBytes(), iterations); + } + + /** + * Derived from RFC 5155 Section 5. + * + * @param digestCalculator the digest calculator. + * @param salt the salt. + * @param data the data. + * @param iterations the number of iterations. + * @return the NSEC3 hash. + */ + static byte[] nsec3hash(DigestCalculator digestCalculator, byte[] salt, byte[] data, int iterations) { + while (iterations-- >= 0) { + byte[] combined = new byte[data.length + salt.length]; + System.arraycopy(data, 0, combined, 0, data.length); + System.arraycopy(salt, 0, combined, data.length, salt.length); + data = digestCalculator.digest(combined); + } + return data; + } +} diff --git a/app/src/main/java/org/minidns/dnssec/algorithms/AlgorithmMap.java b/app/src/main/java/org/minidns/dnssec/algorithms/AlgorithmMap.java new file mode 100644 index 0000000000..0a7af75315 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/algorithms/AlgorithmMap.java @@ -0,0 +1,121 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.constants.DnssecConstants.DigestAlgorithm; +import org.minidns.constants.DnssecConstants.SignatureAlgorithm; +import org.minidns.dnssec.DnssecValidatorInitializationException; +import org.minidns.dnssec.DigestCalculator; +import org.minidns.dnssec.SignatureVerifier; +import org.minidns.record.NSEC3.HashAlgorithm; + +import java.security.NoSuchAlgorithmException; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +public final class AlgorithmMap { + private Logger LOGGER = Logger.getLogger(AlgorithmMap.class.getName()); + + public static final AlgorithmMap INSTANCE = new AlgorithmMap(); + + private final Map dsDigestMap = new HashMap<>(); + private final Map signatureMap = new HashMap<>(); + private final Map nsecDigestMap = new HashMap<>(); + + @SuppressWarnings("deprecation") + private AlgorithmMap() { + try { + dsDigestMap.put(DigestAlgorithm.SHA1, new JavaSecDigestCalculator("SHA-1")); + nsecDigestMap.put(HashAlgorithm.SHA1, new JavaSecDigestCalculator("SHA-1")); + } catch (NoSuchAlgorithmException e) { + // SHA-1 is MANDATORY + throw new DnssecValidatorInitializationException("SHA-1 is mandatory", e); + } + try { + dsDigestMap.put(DigestAlgorithm.SHA256, new JavaSecDigestCalculator("SHA-256")); + } catch (NoSuchAlgorithmException e) { + // SHA-256 is MANDATORY + throw new DnssecValidatorInitializationException("SHA-256 is mandatory", e); + } + try { + dsDigestMap.put(DigestAlgorithm.SHA384, new JavaSecDigestCalculator("SHA-384")); + } catch (NoSuchAlgorithmException e) { + // SHA-384 is OPTIONAL + LOGGER.log(Level.FINE, "Platform does not support SHA-384", e); + } + + try { + signatureMap.put(SignatureAlgorithm.RSAMD5, new RsaSignatureVerifier("MD5withRSA")); + } catch (NoSuchAlgorithmException e) { + // RSA/MD5 is DEPRECATED + LOGGER.log(Level.FINER, "Platform does not support RSA/MD5", e); + } + try { + DsaSignatureVerifier sha1withDSA = new DsaSignatureVerifier("SHA1withDSA"); + signatureMap.put(SignatureAlgorithm.DSA, sha1withDSA); + signatureMap.put(SignatureAlgorithm.DSA_NSEC3_SHA1, sha1withDSA); + } catch (NoSuchAlgorithmException e) { + // DSA/SHA-1 is OPTIONAL + LOGGER.log(Level.FINE, "Platform does not support DSA/SHA-1", e); + } + try { + RsaSignatureVerifier sha1withRSA = new RsaSignatureVerifier("SHA1withRSA"); + signatureMap.put(SignatureAlgorithm.RSASHA1, sha1withRSA); + signatureMap.put(SignatureAlgorithm.RSASHA1_NSEC3_SHA1, sha1withRSA); + } catch (NoSuchAlgorithmException e) { + throw new DnssecValidatorInitializationException("Platform does not support RSA/SHA-1", e); + } + try { + signatureMap.put(SignatureAlgorithm.RSASHA256, new RsaSignatureVerifier("SHA256withRSA")); + } catch (NoSuchAlgorithmException e) { + // RSA/SHA-256 is RECOMMENDED + LOGGER.log(Level.INFO, "Platform does not support RSA/SHA-256", e); + } + try { + signatureMap.put(SignatureAlgorithm.RSASHA512, new RsaSignatureVerifier("SHA512withRSA")); + } catch (NoSuchAlgorithmException e) { + // RSA/SHA-512 is RECOMMENDED + LOGGER.log(Level.INFO, "Platform does not support RSA/SHA-512", e); + } + try { + signatureMap.put(SignatureAlgorithm.ECC_GOST, new EcgostSignatureVerifier()); + } catch (NoSuchAlgorithmException e) { + // GOST R 34.10-2001 is OPTIONAL + LOGGER.log(Level.FINE, "Platform does not support GOST R 34.10-2001", e); + } + try { + signatureMap.put(SignatureAlgorithm.ECDSAP256SHA256, new EcdsaSignatureVerifier.P256SHA256()); + } catch (NoSuchAlgorithmException e) { + // ECDSA/SHA-256 is RECOMMENDED + LOGGER.log(Level.INFO, "Platform does not support ECDSA/SHA-256", e); + } + try { + signatureMap.put(SignatureAlgorithm.ECDSAP384SHA384, new EcdsaSignatureVerifier.P384SHA284()); + } catch (NoSuchAlgorithmException e) { + // ECDSA/SHA-384 is RECOMMENDED + LOGGER.log(Level.INFO, "Platform does not support ECDSA/SHA-384", e); + } + } + + public DigestCalculator getDsDigestCalculator(DigestAlgorithm algorithm) { + return dsDigestMap.get(algorithm); + } + + public SignatureVerifier getSignatureVerifier(SignatureAlgorithm algorithm) { + return signatureMap.get(algorithm); + } + + public DigestCalculator getNsecDigestCalculator(HashAlgorithm algorithm) { + return nsecDigestMap.get(algorithm); + } +} diff --git a/app/src/main/java/org/minidns/dnssec/algorithms/DsaSignatureVerifier.java b/app/src/main/java/org/minidns/dnssec/algorithms/DsaSignatureVerifier.java new file mode 100644 index 0000000000..ee64d5f1f4 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/algorithms/DsaSignatureVerifier.java @@ -0,0 +1,132 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException; +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; +import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; + +import java.io.ByteArrayOutputStream; +import java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; + +class DsaSignatureVerifier extends JavaSecSignatureVerifier { + private static final int LENGTH = 20; + + DsaSignatureVerifier(String algorithm) throws NoSuchAlgorithmException { + super("DSA", algorithm); + } + + @Override + protected byte[] getSignature(RRSIG rrsig) throws DataMalformedException { + DataInput dis = rrsig.getSignatureAsDataInputStream(); + + ByteArrayOutputStream bos; + try { + // Convert RFC 2536 to ASN.1 + @SuppressWarnings("unused") + byte t = dis.readByte(); + + byte[] r = new byte[LENGTH]; + dis.readFully(r); + int roff = 0; + final int rlen; + if (r[0] == 0) { + while (roff < LENGTH && r[roff] == 0) { + roff++; + } + rlen = r.length - roff; + } else if (r[0] < 0) { + rlen = r.length + 1; + } else { + rlen = r.length; + } + + byte[] s = new byte[LENGTH]; + dis.readFully(s); + int soff = 0; + final int slen; + if (s[0] == 0) { + while (soff < LENGTH && s[soff] == 0) { + soff++; + } + slen = s.length - soff; + } else if (s[0] < 0) { + slen = s.length + 1; + } else { + slen = s.length; + } + + bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + dos.writeByte(0x30); + dos.writeByte(rlen + slen + 4); + + dos.writeByte(0x2); + dos.writeByte(rlen); + if (rlen > LENGTH) + dos.writeByte(0); + dos.write(r, roff, LENGTH - roff); + + dos.writeByte(0x2); + dos.writeByte(slen); + if (slen > LENGTH) + dos.writeByte(0); + dos.write(s, soff, LENGTH - soff); + } catch (IOException e) { + throw new DataMalformedException(e, rrsig.getSignature()); + } + + return bos.toByteArray(); + } + + @Override + protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException { + DataInput dis = key.getKeyAsDataInputStream(); + BigInteger subPrime, prime, base, pubKey; + + try { + int t = dis.readUnsignedByte(); + + byte[] subPrimeBytes = new byte[LENGTH]; + dis.readFully(subPrimeBytes); + subPrime = new BigInteger(1, subPrimeBytes); + + byte[] primeBytes = new byte[64 + t * 8]; + dis.readFully(primeBytes); + prime = new BigInteger(1, primeBytes); + + byte[] baseBytes = new byte[64 + t * 8]; + dis.readFully(baseBytes); + base = new BigInteger(1, baseBytes); + + byte[] pubKeyBytes = new byte[64 + t * 8]; + dis.readFully(pubKeyBytes); + pubKey = new BigInteger(1, pubKeyBytes); + } catch (IOException e) { + throw new DataMalformedException(e, key.getKey()); + } + + try { + return getKeyFactory().generatePublic(new DSAPublicKeySpec(pubKey, prime, subPrime, base)); + } catch (InvalidKeySpecException e) { + throw new DnssecInvalidKeySpecException(e); + } + } +} diff --git a/app/src/main/java/org/minidns/dnssec/algorithms/EcdsaSignatureVerifier.java b/app/src/main/java/org/minidns/dnssec/algorithms/EcdsaSignatureVerifier.java new file mode 100644 index 0000000000..e7af9498ab --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/algorithms/EcdsaSignatureVerifier.java @@ -0,0 +1,133 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; +import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException; +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; + +import java.io.ByteArrayOutputStream; +import java.io.DataInput; +import java.io.DataOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.InvalidKeySpecException; + +abstract class EcdsaSignatureVerifier extends JavaSecSignatureVerifier { + private final ECParameterSpec spec; + private final int length; + + EcdsaSignatureVerifier(BigInteger[] spec, int length, String algorithm) throws NoSuchAlgorithmException { + this(new ECParameterSpec(new EllipticCurve(new ECFieldFp(spec[0]), spec[1], spec[2]), new ECPoint(spec[3], spec[4]), spec[5], 1), length, algorithm); + } + + EcdsaSignatureVerifier(ECParameterSpec spec, int length, String algorithm) throws NoSuchAlgorithmException { + super("EC", algorithm); + this.length = length; + this.spec = spec; + } + + @Override + protected byte[] getSignature(RRSIG rrsig) throws DataMalformedException { + DataInput dis = rrsig.getSignatureAsDataInputStream(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + + try { + byte[] r = new byte[length]; + dis.readFully(r); + int rlen = (r[0] < 0) ? length + 1 : length; + + byte[] s = new byte[length]; + dis.readFully(s); + int slen = (s[0] < 0) ? length + 1 : length; + + dos.writeByte(0x30); + dos.writeByte(rlen + slen + 4); + + dos.writeByte(0x2); + dos.writeByte(rlen); + if (rlen > length) dos.writeByte(0); + dos.write(r); + + dos.writeByte(0x2); + dos.writeByte(slen); + if (slen > length) dos.writeByte(0); + dos.write(s); + } catch (IOException e) { + throw new DataMalformedException(e, rrsig.getSignature()); + } + + return bos.toByteArray(); + } + + @Override + protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException { + DataInput dis = key.getKeyAsDataInputStream(); + BigInteger x, y; + + try { + byte[] xBytes = new byte[length]; + dis.readFully(xBytes); + x = new BigInteger(1, xBytes); + + byte[] yBytes = new byte[length]; + dis.readFully(yBytes); + y = new BigInteger(1, yBytes); + } catch (IOException e) { + throw new DataMalformedException(e, key.getKey()); + } + + try { + return getKeyFactory().generatePublic(new ECPublicKeySpec(new ECPoint(x, y), spec)); + } catch (InvalidKeySpecException e) { + throw new DnssecInvalidKeySpecException(e); + } + } + + public static class P256SHA256 extends EcdsaSignatureVerifier { + private static BigInteger[] SPEC = { + new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16), + new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16), + new BigInteger("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", 16), + new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16), + new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16), + new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16) + }; + + P256SHA256() throws NoSuchAlgorithmException { + super(SPEC, 32, "SHA256withECDSA"); + } + } + + public static class P384SHA284 extends EcdsaSignatureVerifier { + private static BigInteger[] SPEC = { + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", 16), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", 16), + new BigInteger("B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", 16), + new BigInteger("AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7", 16), + new BigInteger("3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F", 16), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973", 16) + }; + + P384SHA284() throws NoSuchAlgorithmException { + super(SPEC, 48, "SHA384withECDSA"); + } + } +} diff --git a/app/src/main/java/org/minidns/dnssec/algorithms/EcgostSignatureVerifier.java b/app/src/main/java/org/minidns/dnssec/algorithms/EcgostSignatureVerifier.java new file mode 100644 index 0000000000..2a70a41ae3 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/algorithms/EcgostSignatureVerifier.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException; +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; +import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; + +import java.io.DataInput; +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; +import java.security.spec.InvalidKeySpecException; + +class EcgostSignatureVerifier extends JavaSecSignatureVerifier { + private static final int LENGTH = 32; + private static final ECParameterSpec SPEC = new ECParameterSpec( + new EllipticCurve( + new ECFieldFp(new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD97", 16)), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD94", 16), + new BigInteger("A6", 16) + ), + new ECPoint(BigInteger.ONE, new BigInteger("8D91E471E0989CDA27DF505A453F2B7635294F2DDF23E3B122ACC99C9E9F1E14", 16)), + new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C611070995AD10045841B09B761B893", 16), + 1 + ); + + EcgostSignatureVerifier() throws NoSuchAlgorithmException { + super("ECGOST3410", "GOST3411withECGOST3410"); + } + + @Override + protected byte[] getSignature(RRSIG rrsig) { + return rrsig.getSignature(); + } + + @Override + protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException { + DataInput dis = key.getKeyAsDataInputStream(); + BigInteger x, y; + + try { + byte[] xBytes = new byte[LENGTH]; + dis.readFully(xBytes); + reverse(xBytes); + x = new BigInteger(1, xBytes); + + byte[] yBytes = new byte[LENGTH]; + dis.readFully(yBytes); + reverse(yBytes); + y = new BigInteger(1, yBytes); + } catch (IOException e) { + throw new DataMalformedException(e, key.getKey()); + } + + try { + return getKeyFactory().generatePublic(new ECPublicKeySpec(new ECPoint(x, y), SPEC)); + } catch (InvalidKeySpecException e) { + throw new DnssecInvalidKeySpecException(e); + } + } + + private static void reverse(byte[] array) { + for (int i = 0; i < array.length / 2; i++) { + int j = array.length - i - 1; + byte tmp = array[i]; + array[i] = array[j]; + array[j] = tmp; + } + } +} diff --git a/app/src/main/java/org/minidns/dnssec/algorithms/JavaSecDigestCalculator.java b/app/src/main/java/org/minidns/dnssec/algorithms/JavaSecDigestCalculator.java new file mode 100644 index 0000000000..3be9f616cd --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/algorithms/JavaSecDigestCalculator.java @@ -0,0 +1,29 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DigestCalculator; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class JavaSecDigestCalculator implements DigestCalculator { + private MessageDigest md; + + public JavaSecDigestCalculator(String algorithm) throws NoSuchAlgorithmException { + md = MessageDigest.getInstance(algorithm); + } + + @Override + public byte[] digest(byte[] bytes) { + return md.digest(bytes); + } +} diff --git a/app/src/main/java/org/minidns/dnssec/algorithms/JavaSecSignatureVerifier.java b/app/src/main/java/org/minidns/dnssec/algorithms/JavaSecSignatureVerifier.java new file mode 100644 index 0000000000..8069c17a44 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/algorithms/JavaSecSignatureVerifier.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DnssecValidationFailedException; +import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; +import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException; +import org.minidns.dnssec.SignatureVerifier; +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; + +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; + +public abstract class JavaSecSignatureVerifier implements SignatureVerifier { + private final KeyFactory keyFactory; + private final String signatureAlgorithm; + + public JavaSecSignatureVerifier(String keyAlgorithm, String signatureAlgorithm) throws NoSuchAlgorithmException { + keyFactory = KeyFactory.getInstance(keyAlgorithm); + this.signatureAlgorithm = signatureAlgorithm; + + // Verify signature algorithm to be valid + Signature.getInstance(signatureAlgorithm); + } + + public KeyFactory getKeyFactory() { + return keyFactory; + } + + @Override + public boolean verify(byte[] content, RRSIG rrsig, DNSKEY key) throws DnssecValidationFailedException { + try { + PublicKey publicKey = getPublicKey(key); + Signature signature = Signature.getInstance(signatureAlgorithm); + signature.initVerify(publicKey); + signature.update(content); + return signature.verify(getSignature(rrsig)); + } catch (NoSuchAlgorithmException e) { + // We checked against this before, it should never happen! + throw new AssertionError(e); + } catch (InvalidKeyException | SignatureException | ArithmeticException e) { + throw new DnssecValidationFailedException("Validating signature failed", e); + } + } + + protected abstract byte[] getSignature(RRSIG rrsig) throws DataMalformedException; + + protected abstract PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException; +} diff --git a/app/src/main/java/org/minidns/dnssec/algorithms/RsaSignatureVerifier.java b/app/src/main/java/org/minidns/dnssec/algorithms/RsaSignatureVerifier.java new file mode 100644 index 0000000000..9fb7712c87 --- /dev/null +++ b/app/src/main/java/org/minidns/dnssec/algorithms/RsaSignatureVerifier.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015-2020 the original author or authors + * + * This software is licensed under the Apache License, Version 2.0, + * the GNU Lesser General Public License version 2 or later ("LGPL") + * and the WTFPL. + * You may choose either license to govern your use of this software only + * upon the condition that you accept all of the terms of either + * the Apache License 2.0, the LGPL 2.1+ or the WTFPL. + */ +package org.minidns.dnssec.algorithms; + +import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException; +import org.minidns.record.DNSKEY; +import org.minidns.record.RRSIG; +import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException; + +import java.io.DataInput; +import java.io.IOException; +import java.math.BigInteger; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; + +class RsaSignatureVerifier extends JavaSecSignatureVerifier { + RsaSignatureVerifier(String algorithm) throws NoSuchAlgorithmException { + super("RSA", algorithm); + } + + @Override + protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException { + DataInput dis = key.getKeyAsDataInputStream(); + BigInteger exponent, modulus; + + try { + int exponentLength = dis.readUnsignedByte(); + int bytesRead = 1; + if (exponentLength == 0) { + bytesRead += 2; + exponentLength = dis.readUnsignedShort(); + } + + byte[] exponentBytes = new byte[exponentLength]; + dis.readFully(exponentBytes); + bytesRead += exponentLength; + exponent = new BigInteger(1, exponentBytes); + + byte[] modulusBytes = new byte[key.getKeyLength() - bytesRead]; + dis.readFully(modulusBytes); + modulus = new BigInteger(1, modulusBytes); + } catch (IOException e) { + throw new DataMalformedException(e, key.getKey()); + } + + try { + return getKeyFactory().generatePublic(new RSAPublicKeySpec(modulus, exponent)); + } catch (InvalidKeySpecException e) { + throw new DnssecInvalidKeySpecException(e); + } + } + + @Override + protected byte[] getSignature(RRSIG rrsig) { + return rrsig.getSignature(); + } +}