From a1630d033e97cc24242046c7cccf9ebe65410ae0 Mon Sep 17 00:00:00 2001 From: Florian Schmaus Date: Mon, 31 Oct 2016 10:45:38 +0100 Subject: [PATCH] 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. --- documentation/dnssec.md | 75 +++++++ documentation/index.md | 1 + .../smack/AbstractXMPPConnection.java | 5 +- .../smack/ConnectionConfiguration.java | 65 ++++++ .../org/jivesoftware/smack/util/DNSUtil.java | 75 ++++--- .../smack/util/dns/DNSResolver.java | 54 ++++- .../smack/util/dns/HostAddress.java | 32 ++- .../smack/util/dns/SRVRecord.java | 9 +- .../smack/util/dns/SmackDaneProvider.java | 24 +++ .../smack/util/dns/SmackDaneVerifier.java | 34 ++++ .../smack/SmackExceptionTest.java | 15 +- .../util/dns/dnsjava/DNSJavaResolver.java | 29 ++- .../smack/util/dns/javax/JavaxResolver.java | 54 +++-- smack-resolver-minidns/build.gradle | 2 +- .../smack/util/dns/minidns/MiniDnsDane.java | 34 ++++ .../util/dns/minidns/MiniDnsDaneVerifier.java | 76 +++++++ .../util/dns/minidns/MiniDnsResolver.java | 190 ++++++++++++++---- .../smack/tcp/XMPPTCPConnection.java | 58 ++++-- 18 files changed, 698 insertions(+), 134 deletions(-) create mode 100644 documentation/dnssec.md create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneProvider.java create mode 100644 smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneVerifier.java create mode 100644 smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDane.java create mode 100644 smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDaneVerifier.java diff --git a/documentation/dnssec.md b/documentation/dnssec.md new file mode 100644 index 000000000..f16c4b382 --- /dev/null +++ b/documentation/dnssec.md @@ -0,0 +1,75 @@ +DNSSEC and DANE +=============== + +[Back](index.md) + +**DNSSEC and DANE support in Smack and MiniDNS is still in its +infancy. It should be considered experimental and not ready for +production use at this time.** We would like to see more thorough +testing and review by the security community. If you can help, then +please do not hesitate to contact us. + +About +----- + +DNSSEC ([RFC 4033](https://tools.ietf.org/html/rfc4033) and others) +authenticates DNS answers, positive and negative ones. This means that +if a DNS response secured by DNSSEC turns out to be authentic, then +you can be sure that the domain either exists, and that the returned +resource records (RRs) are the ones the domain owner authorized, or +that the domain does not exists and that nobody tried to fake its non +existence. + +The tricky part is that an application using DNSSEC can not determine +whether a domain uses DNSSEC, does not use DNSSEC or if someone +downgraded your DNS query using DNSSEC to a response without DNSSEC. + +[DANE](https://tools.ietf.org/html/rfc6698) allows the verification of +a TLS certificate with information stored in the DNS system and +secured by DNSSEC. Thus DANE requires DNSSEC. + +Prerequisites +------------- + +From the three DNS resolver providers (MiniDNS, javax, dnsjava) +supported by Smack only [MiniDNS](https://github.com/rtreffer/minidns) +currently supports DNSSEC. MiniDNS is the default resolver when +smack-android is used. For other configurations, make sure to add +smack-resolver-minidns to your dependencies and call +`MiniDnsResolver.setup()` prior using Smack (e.g. in a `static {}` +code block). + +DNSSEC API +---------- + +Smack's DNSSEC API is very simple: Just use +`ConnectionConfiguration.Builder..setDnssecMode(DnssecMode)` to enable +DNSSEC. `DnssecMode` can be one of + +- `disabled` +- `needsDnssec` +- `needsDnssecAndDane` + +The default is `disabled`. + +If `needsDnssec` is used, then Smack will only connect if the DNS +results required to determine a host for the XMPP domain could be +verified using DNSSEC. + +If `needsDnssecAndDane` then DANE will be used to verify the XMPP +service's TLS certificate if STARTTLS is used. Note that you may want +to configure +`ConnectionConfiguration.Builder.setSecurityMode(SecurityMode.required)` +if you use this DNSSEC mode setting. + +Best practices +-------------- + +We recommend that applications using Smack's DNSSEC API do not ask the +user if DNSSEC is avaialble. Instead they should check for DNSSEC +suport on every connection attempt. Once DNSSEC support has been +discovered, the application should use the `needsDnssec` mode for all +future connection attempts. The same scheme can be applied when using +DANE. This approach is similar to the scheme established by +to +["HTTP Strict Transport Security" (HSTS, RFC 6797)](https://tools.ietf.org/html/rfc6797). diff --git a/documentation/index.md b/documentation/index.md index 00e98e92e..034f18070 100644 --- a/documentation/index.md +++ b/documentation/index.md @@ -9,6 +9,7 @@ * [Roster and Presence](roster.md) * [Processing Incoming Stanzas](processing.md) * [Provider Architecture](providers.md) + * [DNSSEC and DANE](dnssec.md) * [Debugging with Smack](debugging.md) * [Smack Extensions Manual](extensions/index.md) diff --git a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java index 2f535f563..e76901474 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/AbstractXMPPConnection.java @@ -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(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 diff --git a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java index cb61ddf60..ff6207dc9 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/ConnectionConfiguration.java @@ -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. + * Note that Smack's support for DNSSEC/DANE is experimental! + *

+ * 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, + + /** + * Experimental! + * Require all DNS information to be authenticated by DNSSEC. + */ + needsDnssec, + + /** + * Experimental! + * 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, 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 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}. diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java b/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java index 54520b4c9..391e03034 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/DNSUtil.java @@ -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 resolveXMPPServiceDomain(String domain, List failedAddresses) { + public static List resolveXMPPServiceDomain(String domain, List 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 addresses = new ArrayList(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 resolveXMPPServerDomain(String domain, List failedAddresses) { + public static List resolveXMPPServerDomain(String domain, List 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 addresses = new ArrayList(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 resolveDomain(String domain, DomainType domainType, List failedAddresses) { + private static List resolveDomain(String domain, DomainType domainType, + List failedAddresses, DnssecMode dnssecMode) { + if (dnsResolver == null) { + throw new IllegalStateException("No DNS Resolver active in Smack"); + } + List addresses = new ArrayList(); // Step one: Do SRV lookups @@ -167,8 +183,9 @@ public class DNSUtil { default: throw new AssertionError(); } - try { - List srvRecords = dnsResolver.lookupSRVRecords(srvDomain); + + List 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 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; } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java index 89f1b404a..052795bda 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/DNSResolver.java @@ -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 lookupSRVRecords(String name) throws Exception; + public final List lookupSRVRecords(String name, List failedAddresses, DnssecMode dnssecMode) { + checkIfDnssecRequestedAndSupported(dnssecMode); + return lookupSRVRecords0(name, failedAddresses, dnssecMode); + } + protected abstract List lookupSRVRecords0(String name, List failedAddresses, DnssecMode dnssecMode); + + public final HostAddress lookupHostAddress(String name, List failedAddresses, DnssecMode dnssecMode) { + checkIfDnssecRequestedAndSupported(dnssecMode); + List inetAddresses = lookupHostAddress0(name, failedAddresses, dnssecMode); + if (inetAddresses == null) { + return null; + } + return new HostAddress(name, inetAddresses); + } + + protected List lookupHostAddress0(String name, List 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"); + } + } } diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java index 3e93872f8..e9ae53717 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/HostAddress.java @@ -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 exceptions = new LinkedHashMap<>(); + private final List 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 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 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 getInetAddresses() { + return Collections.unmodifiableList(inetAddresses); + } + @Override public String toString() { return fqdn + ":" + port; diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java index 7da214c55..e84d846de 100644 --- a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SRVRecord.java @@ -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 { * @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 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: " diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneProvider.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneProvider.java new file mode 100644 index 000000000..abb4f5da5 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneProvider.java @@ -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(); +} diff --git a/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneVerifier.java b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneVerifier.java new file mode 100644 index 000000000..f50b38756 --- /dev/null +++ b/smack-core/src/main/java/org/jivesoftware/smack/util/dns/SmackDaneVerifier.java @@ -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; +} diff --git a/smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java b/smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java index 8f231049e..c14f13740 100644 --- a/smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java +++ b/smack-core/src/test/java/org/jivesoftware/smack/SmackExceptionTest.java @@ -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 failedAddresses = new LinkedList(); - 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 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); diff --git a/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java b/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java index 3af2f12b5..21cc028cf 100644 --- a/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java +++ b/smack-resolver-dnsjava/src/main/java/org/jivesoftware/smack/util/dns/dnsjava/DNSJavaResolver.java @@ -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,12 +16,15 @@ */ package org.jivesoftware.smack.util.dns.dnsjava; +import java.net.InetAddress; import java.util.ArrayList; import java.util.List; +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.initializer.SmackInitializer; import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.HostAddress; import org.jivesoftware.smack.util.dns.SRVRecord; import org.xbill.DNS.Lookup; import org.xbill.DNS.Record; @@ -32,7 +35,7 @@ import org.xbill.DNS.Type; * This implementation uses the dnsjava implementation for resolving DNS addresses. * */ -public class DNSJavaResolver implements SmackInitializer, DNSResolver { +public class DNSJavaResolver extends DNSResolver implements SmackInitializer { private static DNSJavaResolver instance = new DNSJavaResolver(); @@ -40,11 +43,22 @@ public class DNSJavaResolver implements SmackInitializer, DNSResolver { return instance; } + public DNSJavaResolver() { + super(false); + } + @Override - public List lookupSRVRecords(String name) throws TextParseException { + protected List lookupSRVRecords0(String name, List failedAddresses, DnssecMode dnssecMode) { List res = new ArrayList(); - Lookup lookup = new Lookup(name, Type.SRV); + Lookup lookup; + try { + lookup = new Lookup(name, Type.SRV); + } + catch (TextParseException e) { + throw new IllegalStateException(e); + } + Record[] recs = lookup.run(); if (recs == null) return res; @@ -57,7 +71,12 @@ public class DNSJavaResolver implements SmackInitializer, DNSResolver { int priority = srvRecord.getPriority(); int weight = srvRecord.getWeight(); - SRVRecord r = new SRVRecord(host, port, priority, weight); + List hostAddresses = lookupHostAddress0(host, failedAddresses, dnssecMode); + if (hostAddresses == null) { + continue; + } + + SRVRecord r = new SRVRecord(host, port, priority, weight, hostAddresses); res.add(r); } } diff --git a/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java b/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java index e7c831e4e..7de7b911e 100644 --- a/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java +++ b/smack-resolver-javax/src/main/java/org/jivesoftware/smack/util/dns/javax/JavaxResolver.java @@ -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,7 @@ */ package org.jivesoftware.smack.util.dns.javax; +import java.net.InetAddress; import java.util.ArrayList; import java.util.Hashtable; import java.util.List; @@ -27,9 +28,11 @@ import javax.naming.directory.Attributes; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.initializer.SmackInitializer; import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.HostAddress; import org.jivesoftware.smack.util.dns.SRVRecord; /** @@ -38,7 +41,7 @@ import org.jivesoftware.smack.util.dns.SRVRecord; * @author Florian Schmaus * */ -public class JavaxResolver implements SmackInitializer, DNSResolver { +public class JavaxResolver extends DNSResolver implements SmackInitializer { private static JavaxResolver instance; private static DirContext dirContext; @@ -71,27 +74,42 @@ public class JavaxResolver implements SmackInitializer, DNSResolver { DNSUtil.setDNSResolver(getInstance()); } + public JavaxResolver() { + super(false); + } + @Override - public List lookupSRVRecords(String name) throws NamingException { + protected List lookupSRVRecords0(String name, List failedAddresses, DnssecMode dnssecMode) { List res = new ArrayList(); - Attributes dnsLookup = dirContext.getAttributes(name, new String[] { "SRV" }); - Attribute srvAttribute = dnsLookup.get("SRV"); - if (srvAttribute == null) - return res; - @SuppressWarnings("unchecked") - NamingEnumeration srvRecords = (NamingEnumeration) srvAttribute.getAll(); - while (srvRecords.hasMore()) { - String srvRecordString = srvRecords.next(); - String[] srvRecordEntries = srvRecordString.split(" "); - int priority = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 4]); - int port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 2]); - int weight = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 3]); - String host = srvRecordEntries[srvRecordEntries.length - 1]; + try { + Attributes dnsLookup = dirContext.getAttributes(name, new String[] { "SRV" }); + Attribute srvAttribute = dnsLookup.get("SRV"); + if (srvAttribute == null) + return res; + @SuppressWarnings("unchecked") + NamingEnumeration srvRecords = (NamingEnumeration) srvAttribute.getAll(); + while (srvRecords.hasMore()) { + String srvRecordString = srvRecords.next(); + String[] srvRecordEntries = srvRecordString.split(" "); + int priority = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 4]); + int port = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 2]); + int weight = Integer.parseInt(srvRecordEntries[srvRecordEntries.length - 3]); + String host = srvRecordEntries[srvRecordEntries.length - 1]; - SRVRecord srvRecord = new SRVRecord(host, port, priority, weight); - res.add(srvRecord); + List hostAddresses = lookupHostAddress0(host, failedAddresses, dnssecMode); + if (hostAddresses == null) { + continue; + } + + SRVRecord srvRecord = new SRVRecord(host, port, priority, weight, hostAddresses); + res.add(srvRecord); + } } + catch (NamingException e) { + throw new IllegalStateException(e); + } + return res; } diff --git a/smack-resolver-minidns/build.gradle b/smack-resolver-minidns/build.gradle index 812fe8c4b..8e5c95b9e 100644 --- a/smack-resolver-minidns/build.gradle +++ b/smack-resolver-minidns/build.gradle @@ -5,6 +5,6 @@ javax.naming API (e.g. Android).""" dependencies { compile project(path: ':smack-core') - compile 'de.measite.minidns:minidns:[0.1,0.2)' + compile 'de.measite.minidns:minidns-hla:0.2.0-beta1' compile "org.jxmpp:jxmpp-util-cache:$jxmppVersion" } diff --git a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDane.java b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDane.java new file mode 100644 index 000000000..cb66bb02d --- /dev/null +++ b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDane.java @@ -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.minidns; + +import org.jivesoftware.smack.util.DNSUtil; +import org.jivesoftware.smack.util.dns.SmackDaneProvider; +import org.jivesoftware.smack.util.dns.SmackDaneVerifier; + +public class MiniDnsDane implements SmackDaneProvider { + public static final MiniDnsDane INSTANCE = new MiniDnsDane(); + + @Override + public SmackDaneVerifier newInstance() { + return new MiniDnsDaneVerifier(); + } + + public static void setup() { + DNSUtil.setDaneProvider(INSTANCE); + } +} diff --git a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDaneVerifier.java b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDaneVerifier.java new file mode 100644 index 000000000..ad63c74a1 --- /dev/null +++ b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsDaneVerifier.java @@ -0,0 +1,76 @@ +/** + * + * 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.minidns; + +import java.io.IOException; +import java.security.KeyManagementException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; + +import org.jivesoftware.smack.util.dns.SmackDaneVerifier; + +import de.measite.minidns.dane.DaneVerifier; +import de.measite.minidns.dane.ExpectingTrustManager; + +public class MiniDnsDaneVerifier implements SmackDaneVerifier { + private static final Logger LOGGER = Logger.getLogger(MiniDnsDaneVerifier.class.getName()); + + private static final DaneVerifier VERIFIER = new DaneVerifier(); + + private ExpectingTrustManager expectingTrustManager; + + // Package protected constructor. Use MiniDnsDane.newInstance() to create the verifier. + MiniDnsDaneVerifier() { + } + + @Override + public void init(SSLContext context, KeyManager[] km, X509TrustManager tm, SecureRandom random) throws KeyManagementException { + if (expectingTrustManager != null) { + throw new IllegalStateException("DaneProvider was initialized before. Use newInstance() instead."); + } + expectingTrustManager = new ExpectingTrustManager(tm); + context.init(km, new TrustManager[]{expectingTrustManager}, random); + } + + @Override + public void finish(SSLSocket sslSocket) throws CertificateException { + if (VERIFIER.verify(sslSocket)) { + // DANE verification was the only requirement according to the TLSA RR. We can return here. + return; + } + + // DANE verification was successful, but according to the TLSA RR we also must perform PKIX validation. + if (expectingTrustManager.hasException()) { + // PKIX validation has failed. Throw an exception but close the socket first. + try { + sslSocket.close(); + } catch (IOException e) { + LOGGER.log(Level.FINER, "Closing TLS socket failed", e); + } + throw expectingTrustManager.getException(); + } + } + +} diff --git a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java index 6b2b78d36..eda7d7929 100644 --- a/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java +++ b/smack-resolver-minidns/src/main/java/org/jivesoftware/smack/util/dns/minidns/MiniDnsResolver.java @@ -16,82 +16,148 @@ */ package org.jivesoftware.smack.util.dns.minidns; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.initializer.SmackInitializer; import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.dns.DNSResolver; +import org.jivesoftware.smack.util.dns.HostAddress; import org.jivesoftware.smack.util.dns.SRVRecord; -import org.jxmpp.util.cache.ExpirationCache; -import de.measite.minidns.Client; import de.measite.minidns.DNSCache; -import de.measite.minidns.DNSMessage; +import de.measite.minidns.DNSMessage.RESPONSE_CODE; import de.measite.minidns.Question; -import de.measite.minidns.Record; -import de.measite.minidns.Record.CLASS; -import de.measite.minidns.Record.TYPE; -import de.measite.minidns.record.Data; +import de.measite.minidns.cache.LRUCache; +import de.measite.minidns.dnssec.DNSSECClient; +import de.measite.minidns.hla.ResolutionUnsuccessfulException; +import de.measite.minidns.hla.ResolverApi; +import de.measite.minidns.hla.ResolverResult; +import de.measite.minidns.record.A; +import de.measite.minidns.record.AAAA; import de.measite.minidns.record.SRV; +import de.measite.minidns.recursive.ReliableDNSClient; /** - * This implementation uses the minidns implementation for + * This implementation uses the MiniDNS implementation for * resolving DNS addresses. */ -public class MiniDnsResolver implements SmackInitializer, DNSResolver { +public class MiniDnsResolver extends DNSResolver implements SmackInitializer { - private static final long ONE_DAY = 24*60*60*1000; - private static final MiniDnsResolver instance = new MiniDnsResolver(); - private static final ExpirationCache cache = new ExpirationCache(10, ONE_DAY); - private final Client client; + private static final MiniDnsResolver INSTANCE = new MiniDnsResolver(); - public MiniDnsResolver() { - client = new Client(new DNSCache() { + private static final DNSCache CACHE = new LRUCache(128); - @Override - public DNSMessage get(Question question) { - return cache.get(question); - } + private static final ResolverApi DNSSEC_RESOLVER = new ResolverApi(new DNSSECClient(CACHE)); - @Override - public void put(Question question, DNSMessage message) { - long expirationTime = ONE_DAY; - for (Record record : message.getAnswers()) { - if (record.isAnswer(question)) { - expirationTime = record.getTtl(); - break; - } - } - cache.put(question, message, expirationTime); - } - - }); - } + private static final ResolverApi NON_DNSSEC_RESOLVER = new ResolverApi(new ReliableDNSClient(CACHE)); public static DNSResolver getInstance() { - return instance; + return INSTANCE; + } + + public MiniDnsResolver() { + super(true); } @Override - public List lookupSRVRecords(String name) { - List res = new LinkedList(); - DNSMessage message = client.query(name, TYPE.SRV, CLASS.IN); - if (message == null) { - return res; + protected List lookupSRVRecords0(final String name, List failedAddresses, DnssecMode dnssecMode) { + final ResolverApi resolver = getResolver(dnssecMode); + + ResolverResult result; + try { + result = resolver.resolve(name, SRV.class); + } catch (IOException e) { + failedAddresses.add(new HostAddress(name, e)); + return null; } - for (Record record : message.getAnswers()) { - Data data = record.getPayload(); - if (!(data instanceof SRV)) { + + // TODO: Use ResolverResult.getResolutionUnsuccessfulException() found in newer MiniDNS versions. + if (!result.wasSuccessful()) { + ResolutionUnsuccessfulException resolutionUnsuccessfulException = getExceptionFrom(result); + failedAddresses.add(new HostAddress(name, resolutionUnsuccessfulException)); + return null; + } + + if (shouldAbortIfNotAuthentic(name, dnssecMode, result, failedAddresses)) { + return null; + } + + List res = new LinkedList(); + for (SRV srv : result.getAnswers()) { + String hostname = srv.name.ace; + List hostAddresses = lookupHostAddress0(hostname, failedAddresses, dnssecMode); + if (hostAddresses == null) { continue; } - SRV srv = (SRV) data; - res.add(new SRVRecord(srv.getName(), srv.getPort(), srv.getPriority(), srv.getWeight())); + + SRVRecord srvRecord = new SRVRecord(hostname, srv.port, srv.priority, srv.weight, hostAddresses); + res.add(srvRecord); } + return res; } + @Override + protected List lookupHostAddress0(final String name, List failedAddresses, DnssecMode dnssecMode) { + final ResolverApi resolver = getResolver(dnssecMode); + + final ResolverResult aResult; + final ResolverResult aaaaResult; + + try { + aResult = resolver.resolve(name, A.class); + aaaaResult = resolver.resolve(name, AAAA.class); + } catch (IOException e) { + failedAddresses.add(new HostAddress(name, e)); + return null; + } + + if (!aResult.wasSuccessful() && !aaaaResult.wasSuccessful()) { + // Both results where not successful. + failedAddresses.add(new HostAddress(name, getExceptionFrom(aResult))); + failedAddresses.add(new HostAddress(name, getExceptionFrom(aaaaResult))); + return null; + } + + if (shouldAbortIfNotAuthentic(name, dnssecMode, aResult, failedAddresses) + || shouldAbortIfNotAuthentic(name, dnssecMode, aaaaResult, failedAddresses)) { + return null; + } + + List inetAddresses = new ArrayList<>(aResult.getAnswers().size() + + aaaaResult.getAnswers().size()); + + for (A a : aResult.getAnswers()) { + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByAddress(a.getIp()); + } + catch (UnknownHostException e) { + continue; + } + inetAddresses.add(inetAddress); + } + for (AAAA aaaa : aaaaResult.getAnswers()) { + InetAddress inetAddress; + try { + inetAddress = InetAddress.getByAddress(name, aaaa.getIp()); + } + catch (UnknownHostException e) { + continue; + } + inetAddresses.add(inetAddress); + } + + return inetAddresses; + } + public static void setup() { DNSUtil.setDNSResolver(getInstance()); } @@ -99,7 +165,43 @@ public class MiniDnsResolver implements SmackInitializer, DNSResolver { @Override public List initialize() { setup(); + MiniDnsDane.setup(); return null; } + private static ResolverApi getResolver(DnssecMode dnssecMode) { + if (dnssecMode == DnssecMode.disabled) { + return NON_DNSSEC_RESOLVER; + } else { + return DNSSEC_RESOLVER; + } + } + + private static boolean shouldAbortIfNotAuthentic(String name, DnssecMode dnssecMode, + ResolverResult result, List failedAddresses) { + switch (dnssecMode) { + case needsDnssec: + case needsDnssecAndDane: + // Check if the result is authentic data, i.e. there a no reasons the result is unverified. + // TODO: Use ResolverResult.getDnssecResultNotAuthenticException() of newer MiniDNS versions. + if (!result.isAuthenticData()) { + Exception exception = new Exception("DNSSEC verification failed: " + result.getUnverifiedReasons().iterator().next().getReasonString()); + failedAddresses.add(new HostAddress(name, exception)); + return true; + } + break; + case disabled: + break; + default: + throw new IllegalStateException("Unknown DnssecMode: " + dnssecMode); + } + return false; + } + + private static ResolutionUnsuccessfulException getExceptionFrom(ResolverResult result) { + Question question = result.getQuestion(); + RESPONSE_CODE responseCode = result.getResponseCode(); + ResolutionUnsuccessfulException resolutionUnsuccessfulException = new ResolutionUnsuccessfulException(question, responseCode); + return resolutionUnsuccessfulException; + } } diff --git a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java index 00f8882ef..8b336df7c 100644 --- a/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java +++ b/smack-tcp/src/main/java/org/jivesoftware/smack/tcp/XMPPTCPConnection.java @@ -19,6 +19,7 @@ package org.jivesoftware.smack.tcp; import org.jivesoftware.smack.AbstractConnectionListener; import org.jivesoftware.smack.AbstractXMPPConnection; import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.ConnectionConfiguration.DnssecMode; import org.jivesoftware.smack.ConnectionConfiguration.SecurityMode; import org.jivesoftware.smack.StanzaListener; import org.jivesoftware.smack.SmackConfiguration; @@ -72,11 +73,14 @@ import org.jivesoftware.smack.packet.XMPPError; import org.jivesoftware.smack.proxy.ProxyInfo; import org.jivesoftware.smack.util.ArrayBlockingQueueWithShutdown; import org.jivesoftware.smack.util.Async; +import org.jivesoftware.smack.util.DNSUtil; import org.jivesoftware.smack.util.PacketParserUtils; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smack.util.TLSUtils; import org.jivesoftware.smack.util.XmlStringBuilder; import org.jivesoftware.smack.util.dns.HostAddress; +import org.jivesoftware.smack.util.dns.SmackDaneProvider; +import org.jivesoftware.smack.util.dns.SmackDaneVerifier; import org.jxmpp.jid.impl.JidCreate; import org.jxmpp.jid.parts.Resourcepart; import org.jxmpp.stringprep.XmppStringprepException; @@ -90,6 +94,8 @@ import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; import javax.security.auth.callback.Callback; import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.PasswordCallback; @@ -107,20 +113,18 @@ import java.lang.reflect.Constructor; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; -import java.net.UnknownHostException; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.Provider; +import java.security.SecureRandom; import java.security.Security; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; -import java.util.Collections; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.LinkedList; @@ -559,20 +563,9 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { String host = hostAddress.getFQDN(); int port = hostAddress.getPort(); if (proxyInfo == null) { - try { - inetAddresses = Arrays.asList(InetAddress.getAllByName(host)).iterator(); - if (!inetAddresses.hasNext()) { - // This should not happen - LOGGER.warning("InetAddress.getAllByName() returned empty result array."); - throw new UnknownHostException(host); - } - } catch (UnknownHostException e) { - hostAddress.setException(e); - // TODO: Change to emptyIterator() once Smack's minimum Android SDK level is >= 19. - List emptyInetAddresses = Collections.emptyList(); - inetAddresses = emptyInetAddresses.iterator(); - continue; - } + inetAddresses = hostAddress.getInetAddresses().iterator(); + assert(inetAddresses.hasNext()); + innerloop: while (inetAddresses.hasNext()) { // Create a *new* Socket before every connection attempt, i.e. connect() call, since Sockets are not // re-usable after a failed connection attempt. See also SMACK-724. @@ -689,6 +682,18 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { KeyStore ks = null; KeyManager[] kms = null; PasswordCallback pcb = null; + SmackDaneVerifier daneVerifier = null; + + if (config.getDnssecMode() == DnssecMode.needsDnssecAndDane) { + SmackDaneProvider daneProvider = DNSUtil.getDaneProvider(); + if (daneProvider == null) { + throw new UnsupportedOperationException("DANE enabled but no SmackDaneProvider configured"); + } + daneVerifier = daneProvider.newInstance(); + if (daneVerifier == null) { + throw new IllegalStateException("DANE requested but DANE provider did not return a DANE verifier"); + } + } if (context == null) { final String keyStoreType = config.getKeystoreType(); @@ -753,7 +758,20 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // If the user didn't specify a SSLContext, use the default one context = SSLContext.getInstance("TLS"); - context.init(kms, null, new java.security.SecureRandom()); + + final SecureRandom secureRandom = new java.security.SecureRandom(); + X509TrustManager customTrustManager = config.getCustomX509TrustManager(); + + if (daneVerifier != null) { + // User requested DANE verification. + daneVerifier.init(context, kms, customTrustManager, secureRandom); + } else { + TrustManager[] customTrustManagers = null; + if (customTrustManager != null) { + customTrustManagers = new TrustManager[] { customTrustManager }; + } + context.init(kms, customTrustManagers, secureRandom); + } } Socket plain = socket; @@ -773,6 +791,10 @@ public class XMPPTCPConnection extends AbstractXMPPConnection { // Proceed to do the handshake sslSocket.startHandshake(); + if (daneVerifier != null) { + daneVerifier.finish(sslSocket); + } + final HostnameVerifier verifier = getConfiguration().getHostnameVerifier(); if (verifier == null) { throw new IllegalStateException("No HostnameVerifier set. Use connectionConfiguration.setHostnameVerifier() to configure.");