Build MiniDNS/DNSSEC inline

master
M66B 2 weeks ago
parent 9503fea5bb
commit 957ffdc2a2

@ -516,6 +516,9 @@ configurations.configureEach {
// Markwon
exclude group: "com.atlassian.commonmark", module: "commonmark"
// https://github.com/MiniDNS/minidns/issues/139
exclude group: "org.minidns", module: "minidns-dnssec"
}
configurations.configureEach {
@ -756,6 +759,10 @@ dependencies {
// https://github.com/MiniDNS/minidns
// https://mvnrepository.com/artifact/org.minidns/minidns-hla
implementation "org.minidns:minidns-hla:$minidns_version"
// Include
implementation "org.minidns:minidns-client:$minidns_version"
implementation "org.minidns:minidns-core:$minidns_version"
implementation "org.minidns:minidns-iterative-resolver:$minidns_version"
// https://github.com/open-keychain/openpgp-api
// https://mvnrepository.com/artifact/org.sufficientlysecure/openpgp-api

@ -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…
Cancel
Save