mirror of https://github.com/M66B/FairEmail.git
parent
9503fea5bb
commit
957ffdc2a2
@ -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<CertificateMismatch> certificateMismatchExceptions;
|
||||
|
||||
public MultipleCertificateMismatchExceptions(List<CertificateMismatch> 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,273 @@
|
||||
/*
|
||||
* 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.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<DaneCertificateException.CertificateMismatch> certificateMismatchExceptions = new LinkedList<>();
|
||||
boolean verified = false;
|
||||
for (Record<? extends Data> record : res.answerSection) {
|
||||
if (record.type == Record.TYPE.TLSA && record.name.equals(req)) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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<X509Certificate> certs = new ArrayList<>();
|
||||
for (Certificate certificate : certificates) {
|
||||
if (certificate instanceof X509Certificate) {
|
||||
certs.add((X509Certificate) certificate);
|
||||
}
|
||||
}
|
||||
return certs.toArray(new X509Certificate[certs.size()]);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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<DnsName, byte[]> knownSeps = new ConcurrentHashMap<>();
|
||||
|
||||
private boolean stripSignatureRecords = true;
|
||||
|
||||
/**
|
||||
* The active DNSSEC Look-aside Validation Registry. May be <code>null</code>.
|
||||
*/
|
||||
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<DnssecUnverifiedReason> unverifiedReasons = verify(dnsMessage);
|
||||
|
||||
messageBuilder.setAuthenticData(unverifiedReasons.isEmpty());
|
||||
|
||||
List<Record<? extends Data>> answers = dnsMessage.answerSection;
|
||||
List<Record<? extends Data>> nameserverRecords = dnsMessage.authoritySection;
|
||||
List<Record<? extends Data>> additionalResourceRecords = dnsMessage.additionalSection;
|
||||
Set<Record<RRSIG>> 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<Record<? extends Data>> stripSignatureRecords(List<Record<? extends Data>> records) {
|
||||
if (records.isEmpty()) return records;
|
||||
List<Record<? extends Data>> recordList = new ArrayList<>(records.size());
|
||||
for (Record<? extends Data> record : records) {
|
||||
if (record.type != TYPE.RRSIG) {
|
||||
recordList.add(record);
|
||||
}
|
||||
}
|
||||
return recordList;
|
||||
}
|
||||
|
||||
private Set<DnssecUnverifiedReason> verify(DnsMessage dnsMessage) throws IOException {
|
||||
if (!dnsMessage.answerSection.isEmpty()) {
|
||||
return verifyAnswer(dnsMessage);
|
||||
} else {
|
||||
return verifyNsec(dnsMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private Set<DnssecUnverifiedReason> verifyAnswer(DnsMessage dnsMessage) throws IOException {
|
||||
Question q = dnsMessage.questions.get(0);
|
||||
List<Record<? extends Data>> answers = dnsMessage.answerSection;
|
||||
List<Record<? extends Data>> toBeVerified = dnsMessage.copyAnswers();
|
||||
VerifySignaturesResult verifiedSignatures = verifySignatures(q, answers, toBeVerified);
|
||||
Set<DnssecUnverifiedReason> result = verifiedSignatures.reasons;
|
||||
if (!result.isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Keep SEPs separated, we only need one valid SEP.
|
||||
boolean sepSignatureValid = false;
|
||||
Set<DnssecUnverifiedReason> sepReasons = new HashSet<>();
|
||||
for (Iterator<Record<? extends Data>> iterator = toBeVerified.iterator(); iterator.hasNext(); ) {
|
||||
Record<DNSKEY> 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<DnssecUnverifiedReason> 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<DnssecUnverifiedReason> verifyNsec(DnsMessage dnsMessage) throws IOException {
|
||||
Set<DnssecUnverifiedReason> 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<Record<? extends Data>> authoritySection = dnsMessage.authoritySection;
|
||||
for (Record<? extends Data> 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<? extends Data> record : authoritySection) {
|
||||
DnssecUnverifiedReason reason;
|
||||
|
||||
switch (record.type) {
|
||||
case NSEC:
|
||||
nsecPresent = true;
|
||||
Record<NSEC> nsecRecord = record.as(NSEC.class);
|
||||
reason = Verifier.verifyNsec(nsecRecord, q);
|
||||
break;
|
||||
case NSEC3:
|
||||
nsecPresent = true;
|
||||
Record<NSEC3> 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<Record<? extends Data>> 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<DnssecUnverifiedReason> reasons = new HashSet<>();
|
||||
}
|
||||
|
||||
private VerifySignaturesResult verifySignatures(Question q, Collection<Record<? extends Data>> reference, List<Record<? extends Data>> toBeVerified) throws IOException {
|
||||
final Date now = new Date();
|
||||
final List<RRSIG> outdatedRrSigs = new LinkedList<>();
|
||||
VerifySignaturesResult result = new VerifySignaturesResult();
|
||||
final List<Record<RRSIG>> rrsigs = new ArrayList<>(toBeVerified.size());
|
||||
|
||||
for (Record<? extends Data> recordToBeVerified : toBeVerified) {
|
||||
Record<RRSIG> 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<RRSIG> sigRecord : rrsigs) {
|
||||
RRSIG rrsig = sigRecord.payloadData;
|
||||
|
||||
List<Record<? extends Data>> records = new ArrayList<>(reference.size());
|
||||
for (Record<? extends Data> record : reference) {
|
||||
if (record.type == rrsig.typeCovered && record.name.equals(sigRecord.name)) {
|
||||
records.add(record);
|
||||
}
|
||||
}
|
||||
|
||||
Set<DnssecUnverifiedReason> reasons = verifySignedRecords(q, rrsig, records);
|
||||
result.reasons.addAll(reasons);
|
||||
|
||||
if (q.name.equals(rrsig.signerName) && rrsig.typeCovered == TYPE.DNSKEY) {
|
||||
for (Iterator<Record<? extends Data>> iterator = records.iterator(); iterator.hasNext(); ) {
|
||||
Record<DNSKEY> 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<DnssecUnverifiedReason> verifySignedRecords(Question q, RRSIG rrsig, List<Record<? extends Data>> records) throws IOException {
|
||||
Set<DnssecUnverifiedReason> result = new HashSet<>();
|
||||
DNSKEY dnskey = null;
|
||||
|
||||
if (rrsig.typeCovered == TYPE.DNSKEY) {
|
||||
// Key must be present
|
||||
List<Record<DNSKEY>> dnskeyRrs = Record.filter(DNSKEY.class, records);
|
||||
for (Record<DNSKEY> 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<Record<DNSKEY>> dnskeyRrs = dnskeyRes.dnsQueryResult.response.filterAnswerSectionBy(DNSKEY.class);
|
||||
for (Record<DNSKEY> 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<DnssecUnverifiedReason> verifySecureEntryPoint(final Record<DNSKEY> sepRecord) throws IOException {
|
||||
final DNSKEY dnskey = sepRecord.payloadData;
|
||||
|
||||
Set<DnssecUnverifiedReason> unverifiedReasons = new HashSet<>();
|
||||
Set<DnssecUnverifiedReason> 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<Record<DS>> dsRrs = dsResp.dnsQueryResult.response.filterAnswerSectionBy(DS.class);
|
||||
for (Record<DS> 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<Record<DLV>> dlvRrs = dlvResp.dnsQueryResult.response.filterAnswerSectionBy(DLV.class);
|
||||
for (Record<DLV> 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;
|
||||
}
|
||||
}
|
@ -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<Record<RRSIG>> signatures;
|
||||
private final Set<DnssecUnverifiedReason> dnssecUnverifiedReasons;
|
||||
|
||||
DnssecQueryResult(DnsMessage synthesizedResponse, DnsQueryResult dnsQueryResult, Set<Record<RRSIG>> signatures,
|
||||
Set<DnssecUnverifiedReason> 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<Record<RRSIG>> getSignatures() {
|
||||
return signatures;
|
||||
}
|
||||
|
||||
public Set<DnssecUnverifiedReason> getUnverifiedReasons() {
|
||||
return dnssecUnverifiedReasons;
|
||||
}
|
||||
|
||||
}
|
@ -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<DnssecUnverifiedReason> unverifiedReasons;
|
||||
|
||||
private DnssecResultNotAuthenticException(String message, Set<DnssecUnverifiedReason> unverifiedReasons) {
|
||||
super(message);
|
||||
if (unverifiedReasons.isEmpty()) {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons);
|
||||
}
|
||||
|
||||
public static DnssecResultNotAuthenticException from(Set<DnssecUnverifiedReason> 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<DnssecUnverifiedReason> getUnverifiedReasons() {
|
||||
return unverifiedReasons;
|
||||
}
|
||||
}
|
@ -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<? extends Data> record;
|
||||
|
||||
public AlgorithmNotSupportedReason(byte algorithm, TYPE type, Record<? extends Data> 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<? extends Data> record;
|
||||
|
||||
public AlgorithmExceptionThrownReason(DigestAlgorithm algorithm, String kind, Record<? extends Data> 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<DNSKEY> record;
|
||||
|
||||
public ConflictsWithSep(Record<DNSKEY> 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<RRSIG> outdatedRrSigs;
|
||||
|
||||
public NoActiveSignaturesReason(Question question, List<RRSIG> 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<RRSIG> getOutdatedRrSigs() {
|
||||
return outdatedRrSigs;
|
||||
}
|
||||
}
|
||||
|
||||
public static class NSECDoesNotMatchReason extends DnssecUnverifiedReason {
|
||||
private final Question question;
|
||||
private final Record<? extends Data> record;
|
||||
|
||||
public NSECDoesNotMatchReason(Question question, Record<? extends Data> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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<? extends Data> record, String reason) {
|
||||
super("Validation of record " + record + " failed: " + reason);
|
||||
}
|
||||
|
||||
public DnssecValidationFailedException(List<Record<? extends Data>> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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<DNSKEY> 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<Record<? extends Data>> 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<NSEC> 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<NSEC3> 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<Record<? extends Data>> 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<byte[]> recordBytes = new ArrayList<>(records.size());
|
||||
for (Record<? extends Data> record : records) {
|
||||
Record<Data> ref = new Record<Data>(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<byte[]>() {
|
||||
@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;
|
||||
}
|
||||
}
|
@ -0,0 +1,115 @@
|
||||
/*
|
||||
* 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<DigestAlgorithm, DigestCalculator> dsDigestMap = new HashMap<>();
|
||||
private final Map<SignatureAlgorithm, SignatureVerifier> signatureMap = new HashMap<>();
|
||||
private final Map<HashAlgorithm, DigestCalculator> 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 {
|
||||
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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in new issue