package com.verisign.iot.discovery.utils; import com.verisign.iot.discovery.commons.StatusCode; import com.verisign.iot.discovery.domain.Fqdn; import com.verisign.iot.discovery.domain.TextRecord; import com.verisign.iot.discovery.exceptions.ConfigurationException; import com.verisign.iot.discovery.exceptions.LookupException; import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.LinkedHashMap; import java.util.Map; import org.jitsi.dnssec.validator.ValidatingResolver; import org.xbill.DNS.Cache; import org.xbill.DNS.DClass; import org.xbill.DNS.Flags; import org.xbill.DNS.Lookup; import org.xbill.DNS.Message; import org.xbill.DNS.Name; import org.xbill.DNS.RRset; import org.xbill.DNS.Rcode; import org.xbill.DNS.Record; import org.xbill.DNS.Resolver; import org.xbill.DNS.ResolverConfig; import org.xbill.DNS.Section; import org.xbill.DNS.SimpleResolver; import org.xbill.DNS.TXTRecord; import org.xbill.DNS.TextParseException; import org.xbill.DNS.Type; /** * A collection of static utility methods to deal with DNS. * * @author pmaresca <pmaresca@verisign.com> * @version 1.0 * @since 2015/05/02 */ public final class DnsUtil { private static final String INSECURE = "insecure"; private static final String CHAIN_OF_TRUST = "chain of trust"; private static final String NO_DATA = "nodata"; private static final String NO_SIGNATURE = "missing signature"; /** * Instantiate a DNS <code>Resolver</code> by the provided Server. In case of DNSSEC validation * is needed, a <code>ValidatingResolver</code> is instantiated. * * @param dnsSec <code>true</code> iff DNSSEC is enabled * @param trustAnchor Public cryptographic to validate against * @param server Server to use as DNS resolver * @return An instance of <code>Resolver</code> * @throws ConfigurationException Exceptional circumstances in which <code>Resolver</code> * cannot be created. */ public static Resolver getResolver(boolean dnsSec, String trustAnchor, String server) throws ConfigurationException { Resolver resolver = instantiateResolver(dnsSec, trustAnchor, server); if (resolver == null) { throw new ConfigurationException(String.format("Unable to retrieve a Resolver from [%s]", server)); } return resolver; } /** * Instantiate a set of default DNS <code>Resolver</code> by the provided Server. In case of * DNSSEC validation is needed, <code>ValidatingResolver</code> will be instantiated. * * @param dnsSec <code>true</code> iff DNSSEC is enabled * @param trustAnchor Public cryptographic to validate against * @return A list of default <code>Resolver</code> * @throws ConfigurationException Exceptional circumstances in which no default * <code>Resolver</code> can be created. */ public static Map<String, Resolver> getResolvers(boolean dnsSec, String trustAnchor) throws ConfigurationException { String[] servers = ResolverConfig.getCurrentConfig().servers(); Map<String, Resolver> resolvers = new LinkedHashMap<>(servers.length); for (String server : servers) { Resolver resolver = instantiateResolver(dnsSec, trustAnchor, server); if (resolver != null) { resolvers.put(server, resolver); } } if (resolvers.isEmpty()) { throw new ConfigurationException("Unable to retrieve Default Resolvers"); } return resolvers; } /** * Validate the DNSSEC trust chain against the provided domain name (i.e. <code>Fqdn</code>). * * @param name A <code>Fqdn</code> representing the validating domain * @param resolver A DNS <code>Resovler</code> to be used in this validation * @return <code>true</code> iff the DNSSEC is valid * @throws LookupException Containing the specific <code>StatusCode</code> defining the error * that has been raised. */ public static boolean checkDnsSec(Fqdn name, Resolver resolver) throws LookupException { try { ValidatingResolver validating = (ValidatingResolver) resolver; Record toValidate = Record.newRecord(Name.fromConstantString(name.fqdn()), Type.A, DClass.IN); Message dnsResponse = validating.send(Message.newQuery(toValidate)); RRset[] rrSets = dnsResponse.getSectionRRsets(Section.ADDITIONAL); StringBuilder reason = new StringBuilder(""); for (RRset rrset : rrSets) { if (rrset.getName().equals(Name.root) && rrset.getType() == Type.TXT && rrset.getDClass() == ValidatingResolver.VALIDATION_REASON_QCLASS) { reason.append(TextRecord.build((TXTRecord) rrset.first()).getRData()); } } StatusCode outcome = StatusCode.SUCCESSFUL_OPERATION; if(dnsResponse.getRcode() == Rcode.SERVFAIL) { if(reason.toString().toLowerCase().contains(INSECURE) || reason.toString().toLowerCase().contains(CHAIN_OF_TRUST)) outcome = StatusCode.RESOURCE_INSECURE_ERROR; else if(reason.toString().toLowerCase().contains(NO_SIGNATURE)) outcome = StatusCode.RESOLUTION_NAME_ERROR; else if(reason.toString().toLowerCase().contains(NO_DATA)) outcome = StatusCode.NETWORK_ERROR; }else if(dnsResponse.getRcode() == Rcode.NXDOMAIN) { outcome = StatusCode.RESOLUTION_NAME_ERROR; }else if(dnsResponse.getRcode() == Rcode.NOERROR && !dnsResponse.getHeader().getFlag(Flags.AD)) { outcome = StatusCode.RESOURCE_INSECURE_ERROR; } if(outcome != StatusCode.SUCCESSFUL_OPERATION) throw ExceptionsUtil.build(outcome, "DNSSEC Validation Failed", new LinkedHashMap<String, StatusCode>()); } catch (IOException e) { // it might be a transient error network: retry with next Resolver return false; } return true; } /** * Validate the DNS <code>Lookup</code>, catching any transient or blocking issue. * * @param lookup A <code>Lookup</code> used to pull Resource Records * @return A <code>StatusCode</code> with the check outcome * @throws LookupException Containing the specific <code>StatusCode</code> defining the error * that has been raised. */ public static StatusCode checkLookupStatus(Lookup lookup) throws LookupException { StatusCode outcome = null; if (lookup.getResult() == Lookup.TRY_AGAIN) { outcome = StatusCode.NETWORK_ERROR; } else if (lookup.getResult() == Lookup.UNRECOVERABLE) { outcome = StatusCode.SERVER_ERROR; } else if (lookup.getResult() == Lookup.HOST_NOT_FOUND) { // Domain Name not found outcome = StatusCode.RESOLUTION_NAME_ERROR; } else if (lookup.getResult() == Lookup.TYPE_NOT_FOUND) { // RR set not found outcome = StatusCode.RESOLUTION_RR_TYPE_ERROR; } else { outcome = StatusCode.SUCCESSFUL_OPERATION; } return outcome; } /** * Instantiate a DNS <code>Lookup</code> object. * * @param domainName A domain name to lookup * @param resolver A <code>Resolver</code> to be used for lookup * @param rrType The Resource Record <code>Type</code> * @param cache The Resource Record <code>Cache</code> * @return An instance of <code>Lookup</code> * @throws LookupException Containing the specific <code>StatusCode</code> defining the error * that has been raised. */ public static Lookup instantiateLookup(String domainName, Resolver resolver, int rrType, Cache cache) throws LookupException { Lookup lookup = null; try { lookup = new Lookup(domainName, rrType); lookup.setResolver(resolver); lookup.setCache(cache); } catch (TextParseException ex) { throw new LookupException(StatusCode.RESOURCE_LOOKUP_ERROR, String.format("Unable to crea a Lookup for [%s]", domainName)); } return lookup; } /** * Private helper to instantiate a DNS <code>Resolver</code> by the provided Server. * * @param dnsSec <code>true</code> iff DNSSEC is enabled * @param trustAnchor Public cryptographic to validate against * @param server Server to use as DNS resolver * @return <code>null</code> in case the <code>Resolver</code> cannot be instantiated */ private static Resolver instantiateResolver(boolean dnsSec, String trustAnchor, String server) { try { Resolver resolver = new SimpleResolver(server); if (!dnsSec) { return resolver; } ValidatingResolver validating = new ValidatingResolver(resolver); validating.loadTrustAnchors(new ByteArrayInputStream(trustAnchor.getBytes(StandardCharsets.UTF_8))); return validating; } catch (IOException e) { return null; } } private DnsUtil() { throw new AssertionError(String.format("No instances of %s for you!", this.getClass().getName())); } }