Skip to content

Commit

Permalink
Add support for DNSSEC/DANE
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Flowdalic committed Oct 31, 2016
1 parent 042fe3c commit a1630d0
Show file tree
Hide file tree
Showing 18 changed files with 698 additions and 134 deletions.
75 changes: 75 additions & 0 deletions 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).
1 change: 1 addition & 0 deletions documentation/index.md
Expand Up @@ -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)
Expand Up @@ -586,11 +586,10 @@ protected List<HostAddress> populateHostAddresses() {
// 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
Expand Down
Expand Up @@ -39,6 +39,7 @@
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;

/**
Expand Down Expand Up @@ -97,6 +98,10 @@ public abstract class ConnectionConfiguration {
private final boolean legacySessionDisabled;
private final SecurityMode securityMode;

private final DnssecMode dnssecMode;

private final X509TrustManager customX509TrustManager;

/**
*
*/
Expand Down Expand Up @@ -135,6 +140,10 @@ protected ConnectionConfiguration(Builder<?,?> builder) {
proxy = builder.proxy;
socketFactory = builder.socketFactory;

dnssecMode = builder.dnssecMode;

customX509TrustManager = builder.customX509TrustManager;

securityMode = builder.securityMode;
keystoreType = builder.keystoreType;
keystorePath = builder.keystorePath;
Expand All @@ -151,6 +160,11 @@ protected ConnectionConfiguration(Builder<?,?> builder) {

// 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");
}

}

/**
Expand Down Expand Up @@ -183,6 +197,14 @@ public SecurityMode getSecurityMode() {
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,
Expand Down Expand Up @@ -342,6 +364,37 @@ public static enum SecurityMode {
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.
*
Expand Down Expand Up @@ -437,6 +490,7 @@ public Set<String> getEnabledSaslMechanisms() {
*/
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";
Expand All @@ -460,6 +514,7 @@ public static abstract class Builder<B extends Builder<B, C>, C extends Connecti
private boolean allowEmptyOrNullUsername = false;
private boolean saslMechanismsSealed;
private Set<String> enabledSaslMechanisms;
private X509TrustManager customX509TrustManager;

protected Builder() {
}
Expand Down Expand Up @@ -569,6 +624,16 @@ public B setCallbackHandler(CallbackHandler callbackHandler) {
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}.
Expand Down
75 changes: 43 additions & 32 deletions 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.
Expand All @@ -25,19 +25,23 @@
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;

/**
* 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.
Expand All @@ -62,7 +66,7 @@ public String transform(String string) {
* @param resolver
*/
public static void setDNSResolver(DNSResolver resolver) {
dnsResolver = resolver;
dnsResolver = Objects.requireNonNull(resolver);
}

/**
Expand All @@ -74,6 +78,23 @@ public static DNSResolver getDNSResolver() {
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.
Expand Down Expand Up @@ -109,15 +130,10 @@ private static enum DomainType {
* @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);
}

/**
Expand All @@ -134,25 +150,25 @@ public static List<HostAddress> resolveXMPPServiceDomain(String domain, List<Hos
* @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
Expand All @@ -167,8 +183,9 @@ private static List<HostAddress> resolveDomain(String domain, DomainType domainT
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)
Expand All @@ -178,18 +195,12 @@ private static List<HostAddress> resolveDomain(String domain, DomainType domainT
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;
}
Expand Down

0 comments on commit a1630d0

Please sign in to comment.