1
0
Fork 0
mirror of https://github.com/vanitasvitae/Smack.git synced 2025-09-09 09:09:38 +02:00

Add support for DNSSEC/DANE

This closes the cycle which started with a GSOC 2015 project under the
umbrella of the XSF adding DNSSEC/DANE support to MiniDNS.

Fixes SMACK-366.
This commit is contained in:
Florian Schmaus 2016-10-31 10:45:38 +01:00
parent 042fe3c72c
commit a1630d033e
18 changed files with 698 additions and 134 deletions

View file

@ -586,11 +586,10 @@ public abstract class AbstractXMPPConnection implements XMPPConnection {
// N.B.: Important to use config.serviceName and not AbstractXMPPConnection.serviceName
if (config.host != null) {
hostAddresses = new ArrayList<HostAddress>(1);
HostAddress hostAddress;
hostAddress = new HostAddress(config.host, config.port);
HostAddress hostAddress = DNSUtil.getDNSResolver().lookupHostAddress(config.host, failedAddresses, config.getDnssecMode());
hostAddresses.add(hostAddress);
} else {
hostAddresses = DNSUtil.resolveXMPPServiceDomain(config.getXMPPServiceDomain().toString(), failedAddresses);
hostAddresses = DNSUtil.resolveXMPPServiceDomain(config.getXMPPServiceDomain().toString(), failedAddresses, config.getDnssecMode());
}
// If we reach this, then hostAddresses *must not* be empty, i.e. there is at least one host added, either the
// config.host one or the host representing the service name by DNSUtil

View file

@ -39,6 +39,7 @@ import org.jxmpp.stringprep.XmppStringprepException;
import javax.net.SocketFactory;
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import javax.security.auth.callback.CallbackHandler;
/**
@ -97,6 +98,10 @@ public abstract class ConnectionConfiguration {
private final boolean legacySessionDisabled;
private final SecurityMode securityMode;
private final DnssecMode dnssecMode;
private final X509TrustManager customX509TrustManager;
/**
*
*/
@ -135,6 +140,10 @@ public abstract class ConnectionConfiguration {
proxy = builder.proxy;
socketFactory = builder.socketFactory;
dnssecMode = builder.dnssecMode;
customX509TrustManager = builder.customX509TrustManager;
securityMode = builder.securityMode;
keystoreType = builder.keystoreType;
keystorePath = builder.keystorePath;
@ -151,6 +160,11 @@ public abstract class ConnectionConfiguration {
// If the enabledSaslmechanisms are set, then they must not be empty
assert(enabledSaslMechanisms != null ? !enabledSaslMechanisms.isEmpty() : true);
if (dnssecMode != DnssecMode.disabled && customSSLContext != null) {
throw new IllegalStateException("You can not use a custom SSL context with DNSSEC enabled");
}
}
/**
@ -183,6 +197,14 @@ public abstract class ConnectionConfiguration {
return securityMode;
}
public DnssecMode getDnssecMode() {
return dnssecMode;
}
public X509TrustManager getCustomX509TrustManager() {
return customX509TrustManager;
}
/**
* Retuns the path to the keystore file. The key store file contains the
* certificates that may be used to authenticate the client to the server,
@ -342,6 +364,37 @@ public abstract class ConnectionConfiguration {
disabled
}
/**
* Determines the requested DNSSEC security mode.
* <b>Note that Smack's support for DNSSEC/DANE is experimental!</b>
* <p>
* The default '{@link #disabled}' means that neither DNSSEC nor DANE verification will be performed. When
* '{@link #needsDnssec}' is used, then the connection will not be established if the resource records used to connect
* to the XMPP service are not authenticated by DNSSEC. Additionally, if '{@link #needsDnssecAndDane}' is used, then
* the XMPP service's TLS certificate is verified using DANE.
*
*/
public enum DnssecMode {
/**
* Do not perform any DNSSEC authentication or DANE verification.
*/
disabled,
/**
* <b>Experimental!</b>
* Require all DNS information to be authenticated by DNSSEC.
*/
needsDnssec,
/**
* <b>Experimental!</b>
* Require all DNS information to be authenticated by DNSSEC and require the XMPP service's TLS certificate to be verified using DANE.
*/
needsDnssecAndDane,
}
/**
* Returns the username to use when trying to reconnect to the server.
*
@ -437,6 +490,7 @@ public abstract class ConnectionConfiguration {
*/
public static abstract class Builder<B extends Builder<B, C>, C extends ConnectionConfiguration> {
private SecurityMode securityMode = SecurityMode.ifpossible;
private DnssecMode dnssecMode = DnssecMode.disabled;
private String keystorePath = System.getProperty("javax.net.ssl.keyStore");
private String keystoreType = "jks";
private String pkcs11Library = "pkcs11.config";
@ -460,6 +514,7 @@ public abstract class ConnectionConfiguration {
private boolean allowEmptyOrNullUsername = false;
private boolean saslMechanismsSealed;
private Set<String> enabledSaslMechanisms;
private X509TrustManager customX509TrustManager;
protected Builder() {
}
@ -569,6 +624,16 @@ public abstract class ConnectionConfiguration {
return getThis();
}
public B setDnssecMode(DnssecMode dnssecMode) {
this.dnssecMode = Objects.requireNonNull(dnssecMode, "DNSSEC mode must not be null");
return getThis();
}
public B setCustomX509TrustManager(X509TrustManager x509TrustManager) {
this.customX509TrustManager = x509TrustManager;
return getThis();
}
/**
* Sets the TLS security mode used when making the connection. By default,
* the mode is {@link SecurityMode#ifpossible}.

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2003-2005 Jive Software.
* Copyright 2003-2005 Jive Software, 2016 Florian Schmaus.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -25,7 +25,9 @@ import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode;
import org.jivesoftware.smack.util.dns.DNSResolver;
import org.jivesoftware.smack.util.dns.SmackDaneProvider;
import org.jivesoftware.smack.util.dns.HostAddress;
import org.jivesoftware.smack.util.dns.SRVRecord;
@ -33,11 +35,13 @@ import org.jivesoftware.smack.util.dns.SRVRecord;
* Utility class to perform DNS lookups for XMPP services.
*
* @author Matt Tucker
* @author Florian Schmaus
*/
public class DNSUtil {
private static final Logger LOGGER = Logger.getLogger(DNSUtil.class.getName());
private static DNSResolver dnsResolver = null;
private static SmackDaneProvider daneProvider;
/**
* International Domain Name transformer.
@ -62,7 +66,7 @@ public class DNSUtil {
* @param resolver
*/
public static void setDNSResolver(DNSResolver resolver) {
dnsResolver = resolver;
dnsResolver = Objects.requireNonNull(resolver);
}
/**
@ -74,6 +78,23 @@ public class DNSUtil {
return dnsResolver;
}
/**
* Set the DANE provider that should be used when DANE is enabled.
*
* @param daneProvider
*/
public static void setDaneProvider(SmackDaneProvider daneProvider) {
daneProvider = Objects.requireNonNull(daneProvider);
}
/**
* Returns the currently active DANE provider used when DANE is enabled.
*
* @return the active DANE provider
*/
public static SmackDaneProvider getDaneProvider() {
return daneProvider;
}
/**
* Set the IDNA (Internationalizing Domain Names in Applications, RFC 3490) transformer.
@ -109,15 +130,10 @@ public class DNSUtil {
* @return List of HostAddress, which encompasses the hostname and port that the
* XMPP server can be reached at for the specified domain.
*/
public static List<HostAddress> resolveXMPPServiceDomain(String domain, List<HostAddress> failedAddresses) {
public static List<HostAddress> resolveXMPPServiceDomain(String domain, List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
domain = idnaTransformer.transform(domain);
if (dnsResolver == null) {
LOGGER.warning("No DNS Resolver active in Smack, will be unable to perform DNS SRV lookups");
List<HostAddress> addresses = new ArrayList<HostAddress>(1);
addresses.add(new HostAddress(domain, 5222));
return addresses;
}
return resolveDomain(domain, DomainType.Client, failedAddresses);
return resolveDomain(domain, DomainType.Client, failedAddresses, dnssecMode);
}
/**
@ -134,25 +150,25 @@ public class DNSUtil {
* @return List of HostAddress, which encompasses the hostname and port that the
* XMPP server can be reached at for the specified domain.
*/
public static List<HostAddress> resolveXMPPServerDomain(String domain, List<HostAddress> failedAddresses) {
public static List<HostAddress> resolveXMPPServerDomain(String domain, List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
domain = idnaTransformer.transform(domain);
if (dnsResolver == null) {
LOGGER.warning("No DNS Resolver active in Smack, will be unable to perform DNS SRV lookups");
List<HostAddress> addresses = new ArrayList<HostAddress>(1);
addresses.add(new HostAddress(domain, 5269));
return addresses;
}
return resolveDomain(domain, DomainType.Server, failedAddresses);
return resolveDomain(domain, DomainType.Server, failedAddresses, dnssecMode);
}
/**
*
* @param domain the domain.
* @param domainType the XMPP domain type, server or client.
* @param failedAddresses on optional list that will be populated with host addresses that failed to resolve.
* @param failedAddresses a list that will be populated with host addresses that failed to resolve.
* @return a list of resolver host addresses for this domain.
*/
private static List<HostAddress> resolveDomain(String domain, DomainType domainType, List<HostAddress> failedAddresses) {
private static List<HostAddress> resolveDomain(String domain, DomainType domainType,
List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
if (dnsResolver == null) {
throw new IllegalStateException("No DNS Resolver active in Smack");
}
List<HostAddress> addresses = new ArrayList<HostAddress>();
// Step one: Do SRV lookups
@ -167,8 +183,9 @@ public class DNSUtil {
default:
throw new AssertionError();
}
try {
List<SRVRecord> srvRecords = dnsResolver.lookupSRVRecords(srvDomain);
List<SRVRecord> srvRecords = dnsResolver.lookupSRVRecords(srvDomain, failedAddresses, dnssecMode);
if (srvRecords != null) {
if (LOGGER.isLoggable(Level.FINE)) {
String logMessage = "Resolved SRV RR for " + srvDomain + ":";
for (SRVRecord r : srvRecords)
@ -178,18 +195,12 @@ public class DNSUtil {
List<HostAddress> sortedRecords = sortSRVRecords(srvRecords);
addresses.addAll(sortedRecords);
}
catch (Exception e) {
LOGGER.log(Level.WARNING, "Exception while resovling SRV records for " + domain
+ ". Consider adding '_xmpp-(server|client)._tcp' DNS SRV Records", e);
if (failedAddresses != null) {
HostAddress failedHostAddress = new HostAddress(srvDomain);
failedHostAddress.setException(e);
failedAddresses.add(failedHostAddress);
}
}
// Step two: Add the hostname to the end of the list
addresses.add(new HostAddress(domain));
HostAddress hostAddress = dnsResolver.lookupHostAddress(domain, failedAddresses, dnssecMode);
if (hostAddress != null) {
addresses.add(hostAddress);
}
return addresses;
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2013 Florian Schmaus
* Copyright 2013-2016 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,19 +16,67 @@
*/
package org.jivesoftware.smack.util.dns;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode;
/**
* Implementations of this interface define a class that is capable of resolving DNS addresses.
*
*/
public interface DNSResolver {
public abstract class DNSResolver {
private final boolean supportsDnssec;
protected DNSResolver(boolean supportsDnssec) {
this.supportsDnssec = supportsDnssec;
}
/**
* Gets a list of service records for the specified service.
* @param name The symbolic name of the service.
* @return The list of SRV records mapped to the service name.
*/
List<SRVRecord> lookupSRVRecords(String name) throws Exception;
public final List<SRVRecord> lookupSRVRecords(String name, List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
checkIfDnssecRequestedAndSupported(dnssecMode);
return lookupSRVRecords0(name, failedAddresses, dnssecMode);
}
protected abstract List<SRVRecord> lookupSRVRecords0(String name, List<HostAddress> failedAddresses, DnssecMode dnssecMode);
public final HostAddress lookupHostAddress(String name, List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
checkIfDnssecRequestedAndSupported(dnssecMode);
List<InetAddress> inetAddresses = lookupHostAddress0(name, failedAddresses, dnssecMode);
if (inetAddresses == null) {
return null;
}
return new HostAddress(name, inetAddresses);
}
protected List<InetAddress> lookupHostAddress0(String name, List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
// Default implementation of a DNS name lookup for A/AAAA records. It is assumed that this method does never
// support DNSSEC. Subclasses are free to override this method.
if (dnssecMode != DnssecMode.disabled) {
throw new UnsupportedOperationException("This resolver does not support DNSSEC");
}
InetAddress[] inetAddressArray;
try {
inetAddressArray = InetAddress.getAllByName(name);
} catch (UnknownHostException e) {
failedAddresses.add(new HostAddress(name, e));
return null;
}
return Arrays.asList(inetAddressArray);
}
private final void checkIfDnssecRequestedAndSupported(DnssecMode dnssecMode) {
if (dnssecMode != DnssecMode.disabled && !supportsDnssec) {
throw new UnsupportedOperationException("This resolver does not support DNSSEC");
}
}
}

View file

@ -1,6 +1,6 @@
/**
*
* Copyright © 2013-2014 Florian Schmaus
* Copyright © 2013-2016 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,6 +20,7 @@ import java.net.InetAddress;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
@ -30,6 +31,7 @@ public class HostAddress {
private final String fqdn;
private final int port;
private final Map<InetAddress, Exception> exceptions = new LinkedHashMap<>();
private final List<InetAddress> inetAddresses;
/**
* Creates a new HostAddress with the given FQDN. The port will be set to the default XMPP client port: 5222
@ -37,9 +39,9 @@ public class HostAddress {
* @param fqdn Fully qualified domain name.
* @throws IllegalArgumentException If the fqdn is null.
*/
public HostAddress(String fqdn) {
public HostAddress(String fqdn, List<InetAddress> inetAddresses) {
// Set port to the default port for XMPP client communication
this(fqdn, 5222);
this(fqdn, 5222, inetAddresses);
}
/**
@ -49,7 +51,7 @@ public class HostAddress {
* @param port The port to connect on.
* @throws IllegalArgumentException If the fqdn is null or port is out of valid range (0 - 65535).
*/
public HostAddress(String fqdn, int port) {
public HostAddress(String fqdn, int port, List<InetAddress> inetAddresses) {
Objects.requireNonNull(fqdn, "FQDN is null");
if (port < 0 || port > 65535)
throw new IllegalArgumentException(
@ -61,6 +63,24 @@ public class HostAddress {
this.fqdn = fqdn;
}
this.port = port;
if (inetAddresses.isEmpty()) {
throw new IllegalArgumentException("Must provide at least one InetAddress");
}
this.inetAddresses = inetAddresses;
}
/**
* Constructs a new failed HostAddress. This constructor is usually used when the DNS resolution of the domain name
* failed for some reason.
*
* @param fqdn the domain name of the host.
* @param e the exception causing the failure.
*/
public HostAddress(String fqdn, Exception e) {
this.fqdn = fqdn;
this.port = 5222;
inetAddresses = Collections.emptyList();
setException(e);
}
public String getFQDN() {
@ -91,6 +111,10 @@ public class HostAddress {
return Collections.unmodifiableMap(exceptions);
}
public List<InetAddress> getInetAddresses() {
return Collections.unmodifiableList(inetAddresses);
}
@Override
public String toString() {
return fqdn + ":" + port;

View file

@ -1,6 +1,6 @@
/**
*
* Copyright 2013-2014 Florian Schmaus
* Copyright 2013-2016 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -16,6 +16,9 @@
*/
package org.jivesoftware.smack.util.dns;
import java.net.InetAddress;
import java.util.List;
/**
* A DNS SRV RR.
*
@ -38,8 +41,8 @@ public class SRVRecord extends HostAddress implements Comparable<SRVRecord> {
* @param weight Relative weight for records with same priority
* @throws IllegalArgumentException fqdn is null or any other field is not in valid range (0-65535).
*/
public SRVRecord(String fqdn, int port, int priority, int weight) {
super(fqdn, port);
public SRVRecord(String fqdn, int port, int priority, int weight, List<InetAddress> inetAddresses) {
super(fqdn, port, inetAddresses);
if (weight < 0 || weight > 65535)
throw new IllegalArgumentException(
"DNS SRV records weight must be a 16-bit unsiged integer (i.e. between 0-65535. Weight was: "

View file

@ -0,0 +1,24 @@
/**
*
* Copyright 2015-2016 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.util.dns;
/**
* Implementations of this interface define a class that is capable of enabling DANE on a connection.
*/
public interface SmackDaneProvider {
SmackDaneVerifier newInstance();
}

View file

@ -0,0 +1,34 @@
/**
*
* Copyright 2015-2016 Florian Schmaus
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.smack.util.dns;
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.X509TrustManager;
import java.security.KeyManagementException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
/**
* Implementations of this interface define a class that is capable of enabling DANE on a connection.
*/
public interface SmackDaneVerifier {
void init(SSLContext context, KeyManager[] km, X509TrustManager tm, SecureRandom random) throws KeyManagementException;
void finish(SSLSocket socket) throws CertificateException;
}

View file

@ -18,6 +18,9 @@ package org.jivesoftware.smack;
import static org.junit.Assert.assertEquals;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
@ -28,14 +31,20 @@ import org.junit.Test;
public class SmackExceptionTest {
@Test
public void testConnectionException() {
public void testConnectionException() throws UnknownHostException {
List<HostAddress> failedAddresses = new LinkedList<HostAddress>();
HostAddress hostAddress = new HostAddress("foo.bar.example", 1234);
String host = "foo.bar.example";
InetAddress inetAddress = InetAddress.getByAddress(host, new byte[] { 0, 0, 0, 0 });
List<InetAddress> inetAddresses = Collections.singletonList(inetAddress);
HostAddress hostAddress = new HostAddress(host, 1234, inetAddresses);
hostAddress.setException(new Exception("Failed for some reason"));
failedAddresses.add(hostAddress);
hostAddress = new HostAddress("barz.example", 5678);
host = "barz.example";
inetAddress = InetAddress.getByAddress(host, new byte[] { 0, 0, 0, 0 });
inetAddresses = Collections.singletonList(inetAddress);
hostAddress = new HostAddress(host, 5678, inetAddresses);
hostAddress.setException(new Exception("Failed for some other reason"));
failedAddresses.add(hostAddress);