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

75
documentation/dnssec.md Normal file
View File

@ -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).

View File

@ -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)

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);

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,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 <a href="http://www.dnsjava.org/">dnsjava</a> 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<SRVRecord> lookupSRVRecords(String name) throws TextParseException {
protected List<SRVRecord> lookupSRVRecords0(String name, List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
List<SRVRecord> res = new ArrayList<SRVRecord>();
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<InetAddress> hostAddresses = lookupHostAddress0(host, failedAddresses, dnssecMode);
if (hostAddresses == null) {
continue;
}
SRVRecord r = new SRVRecord(host, port, priority, weight, hostAddresses);
res.add(r);
}
}

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,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<SRVRecord> lookupSRVRecords(String name) throws NamingException {
protected List<SRVRecord> lookupSRVRecords0(String name, List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
List<SRVRecord> res = new ArrayList<SRVRecord>();
Attributes dnsLookup = dirContext.getAttributes(name, new String[] { "SRV" });
Attribute srvAttribute = dnsLookup.get("SRV");
if (srvAttribute == null)
return res;
@SuppressWarnings("unchecked")
NamingEnumeration<String> srvRecords = (NamingEnumeration<String>) 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<String> srvRecords = (NamingEnumeration<String>) 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<InetAddress> 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;
}

View File

@ -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"
}

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.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);
}
}

View File

@ -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();
}
}
}

View File

@ -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 <a href="https://github.com/rtreffer/minidns/">minidns</a> implementation for
* This implementation uses the <a href="https://github.com/rtreffer/minidns/">MiniDNS</a> 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<Question, DNSMessage> cache = new ExpirationCache<Question, DNSMessage>(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<SRVRecord> lookupSRVRecords(String name) {
List<SRVRecord> res = new LinkedList<SRVRecord>();
DNSMessage message = client.query(name, TYPE.SRV, CLASS.IN);
if (message == null) {
return res;
protected List<SRVRecord> lookupSRVRecords0(final String name, List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
final ResolverApi resolver = getResolver(dnssecMode);
ResolverResult<SRV> 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<SRVRecord> res = new LinkedList<SRVRecord>();
for (SRV srv : result.getAnswers()) {
String hostname = srv.name.ace;
List<InetAddress> 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<InetAddress> lookupHostAddress0(final String name, List<HostAddress> failedAddresses, DnssecMode dnssecMode) {
final ResolverApi resolver = getResolver(dnssecMode);
final ResolverResult<A> aResult;
final ResolverResult<AAAA> 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<InetAddress> 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<Exception> 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<HostAddress> 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;
}
}

View File

@ -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<InetAddress> 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.");