/***************************** -*- Java -*- ********************************\ * * * Copyright (c) 2009 VeriSign, Inc. All rights reserved. * * * * This software is provided solely in connection with the terms of the * * license agreement. Any other use without the prior express written * * permission of VeriSign is completely prohibited. The software and * * documentation are "Commercial Items", as that term is defined in 48 * * C.F.R. section 2.101, consisting of "Commercial Computer Software" and * * "Commercial Computer Software Documentation" as such terms are defined * * in 48 C.F.R. section 252.227-7014(a)(5) and 48 C.F.R. section * * 252.227-7014(a)(1), and used in 48 C.F.R. section 12.212 and 48 C.F.R. * * section 227.7202, as applicable. Pursuant to the above and other * * relevant sections of the Code of Federal Regulations, as applicable, * * VeriSign's publications, commercial computer software, and commercial * * computer software documentation are distributed and licensed to United * * States Government end users with only those rights as granted to all * * other end users, according to the terms and conditions contained in the * * license agreement(s) that accompany the products and software * * documentation. * * * \***************************************************************************/ package com.verisign.tat.dnssec; import org.apache.log4j.Logger; import org.xbill.DNS.*; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Iterator; /** * This is a collection of routines encompassing the logic of validating * different message types. */ public class ValUtils { private static Logger st_log = Logger.getLogger(ValUtils.class); private Logger log = Logger.getLogger(this.getClass()); /** A local copy of the verifier object. */ private DnsSecVerifier mVerifier; public ValUtils(DnsSecVerifier verifier) { mVerifier = verifier; } /** * Given a response, classify ANSWER responses into a subtype. * * @param m * The response to classify. * * @return A subtype ranging from UNKNOWN to NAMEERROR. */ public static ResponseType classifyResponse(SMessage m, Name zone) { SRRset[] rrsets; // Normal Name Error's are easy to detect -- but don't mistake a CNAME // chain ending in NXDOMAIN. if ((m.getRcode() == Rcode.NXDOMAIN) && (m.getCount(Section.ANSWER) == 0)) { return ResponseType.NAMEERROR; } // If rcode isn't NXDOMAIN or NOERROR, it is a throwaway response. if (m.getRcode() != Rcode.NOERROR) { return ResponseType.THROWAWAY; } // Next is REFERRAL. These are distinguished by having: // 1) nothing in the ANSWER section // 2) an NS RRset in the AUTHORITY section that is a strict subdomain of // 'zone' (the presumed queried zone). if ((zone != null) && (m.getCount(Section.ANSWER) == 0) && (m.getCount(Section.AUTHORITY) > 0)) { rrsets = m.getSectionRRsets(Section.AUTHORITY); for (int i = 0; i < rrsets.length; ++i) { if ((rrsets[i].getType() == Type.NS) && strictSubdomain(rrsets[i].getName(), zone)) { return ResponseType.REFERRAL; } } } // Next is NODATA if (m.getCount(Section.ANSWER) == 0) { return ResponseType.NODATA; } // We distinguish between CNAME response and other positive/negative // responses because CNAME answers require extra processing. int qtype = m.getQuestion().getType(); // We distinguish between ANY and CNAME or POSITIVE because ANY // responses are validated differently. if (qtype == Type.ANY) { return ResponseType.ANY; } rrsets = m.getSectionRRsets(Section.ANSWER); // Note that DNAMEs will be ignored here, unless qtype=DNAME. Unless // qtype=CNAME, this will yield a CNAME response. for (int i = 0; i < rrsets.length; i++) { if (rrsets[i].getType() == qtype) { return ResponseType.POSITIVE; } if (rrsets[i].getType() == Type.CNAME) { return ResponseType.CNAME; } } st_log.warn("Failed to classify response message:\n" + m); return ResponseType.UNKNOWN; } /** * Given a response, determine the name of the "signer". This is primarily * to determine if the response is, in fact, signed at all, and, if so, what * is the name of the most pertinent keyset. * * @param m * The response to analyze. * @return a signer name, if the response is signed (even partially), or * null if the response isn't signed. */ public Name findSigner(SMessage m) { // FIXME: this used to classify the message, then look in the pertinent // section. Now we just find the first RRSIG in the ANSWER and AUTHORIY // sections. for (int section = Section.ANSWER; section < Section.ADDITIONAL; ++section) { SRRset[] rrsets = m.getSectionRRsets(section); for (int i = 0; i < rrsets.length; ++i) { Name signerName = rrsets[i].getSignerName(); if (signerName != null) { return signerName; } } } return null; } /** * Given a DNSKEY record, generate the DS record from it. * * @param keyrec * the DNSKEY record in question. * @param ds_alg * The DS digest algorithm in use. * @return the corresponding {@link org.xbill.DNS.DSRecord} */ public static byte[] calculateDSHash(DNSKEYRecord keyrec, int ds_alg) { DNSOutput os = new DNSOutput(); os.writeByteArray(keyrec.getName().toWireCanonical()); os.writeByteArray(keyrec.rdataToWireCanonical()); try { MessageDigest md = null; switch (ds_alg) { case DSRecord.SHA1_DIGEST_ID: md = MessageDigest.getInstance("SHA"); return md.digest(os.toByteArray()); case DSRecord.SHA256_DIGEST_ID: md = MessageDigest.getInstance("SHA256"); return md.digest(os.toByteArray()); default: st_log.warn("Unknown DS algorithm: " + ds_alg); return null; } } catch (NoSuchAlgorithmException e) { st_log.error("Error using DS algorithm: " + ds_alg, e); return null; } } public static boolean supportsDigestID(int digest_id) { if (digest_id == DSRecord.SHA1_DIGEST_ID) { return true; } if (digest_id == DSRecord.SHA256_DIGEST_ID) { return true; } return false; } /** * Check to see if a type is a special DNSSEC type. * * @param type * The type. * * @return true if the type is one of the special DNSSEC types. */ public static boolean isDNSSECType(int type) { switch (type) { case Type.DNSKEY: case Type.NSEC: case Type.DS: case Type.RRSIG: case Type.NSEC3: return true; default: return false; } } /** * Set the security status of a particular RRset. This will only upgrade the * security status. * * @param rrset * The SRRset to update. * @param security * The security status. */ public static void setRRsetSecurity(SRRset rrset, byte security) { if (rrset == null) { return; } int cur_sec = rrset.getSecurityStatus(); if ((cur_sec == SecurityStatus.UNCHECKED) || (security > cur_sec)) { rrset.setSecurityStatus(security); } } /** * Set the security status of a message and all of its RRsets. This will * only upgrade the status of the message (i.e., set to more secure, not * less) and all of the RRsets. * * @param m * @param security * KeyEntry ke; * * SMessage m = response.getSMessage(); SRRset ans_rrset = * m.findAnswerRRset(qname, qtype, qclass); * * ke = verifySRRset(ans_rrset, key_rrset); if * (ans_rrset.getSecurityStatus() != SecurityStatus.SECURE) { * return; } key_rrset = ke.getRRset(); */ public static void setMessageSecurity(SMessage m, byte security) { if (m == null) { return; } int cur_sec = m.getStatus(); if ((cur_sec == SecurityStatus.UNCHECKED) || (security > cur_sec)) { m.setStatus(security); } for (int section = Section.ANSWER; section <= Section.ADDITIONAL; section++) { SRRset[] rrsets = m.getSectionRRsets(section); for (int i = 0; i < rrsets.length; i++) { setRRsetSecurity(rrsets[i], security); } } } /** * Given an SRRset that is signed by a DNSKEY found in the key_rrset, verify * it. This will return the status (either BOGUS or SECURE) and set that * status in rrset. * * @param rrset * The SRRset to verify. * @param key_rrset * The set of keys to verify against. * @return The status (BOGUS or SECURE). */ public byte verifySRRset(SRRset rrset, SRRset key_rrset) { String rrset_name = rrset.getName() + "/" + Type.string(rrset.getType()) + "/" + DClass.string(rrset.getDClass()); if (rrset.getSecurityStatus() == SecurityStatus.SECURE) { log.trace("verifySRRset: rrset <" + rrset_name + "> previously found to be SECURE"); return SecurityStatus.SECURE; } byte status = mVerifier.verify(rrset, key_rrset); if (status != SecurityStatus.SECURE) { log.debug("verifySRRset: rrset <" + rrset_name + "> found to be BAD"); status = SecurityStatus.BOGUS; } else { log.trace("verifySRRset: rrset <" + rrset_name + "> found to be SECURE"); } rrset.setSecurityStatus(status); return status; } /** * Determine if a given type map has a given type. * * @param types * The type map from the NSEC record. * @param type * The type to look for. * @return true if the type is present in the type map, false otherwise. */ public static boolean typeMapHasType(int[] types, int type) { for (int i = 0; i < types.length; i++) { if (types[i] == type) { return true; } } return false; } @SuppressWarnings("unchecked") public static RRSIGRecord rrsetFirstSig(RRset rrset) { for (Iterator i = rrset.sigs(); i.hasNext();) { return (RRSIGRecord) i.next(); } return null; } /** * Finds the longest common name between two domain names. * * @param domain1 * @param domain2 * @return */ public static Name longestCommonName(Name domain1, Name domain2) { if ((domain1 == null) || (domain2 == null)) { return null; } // for now, do this in a a fairly brute force way // FIXME: convert this to direct operations on the byte[] int d1_labels = domain1.labels(); int d2_labels = domain2.labels(); int l = (d1_labels < d2_labels) ? d1_labels : d2_labels; for (int i = l; i > 0; i--) { Name n1 = new Name(domain1, d1_labels - i); Name n2 = new Name(domain2, d2_labels - i); if (n1.equals(n2)) { return n1; } } return Name.root; } public static boolean strictSubdomain(Name child, Name parent) { int clabels = child.labels(); int plabels = parent.labels(); if (plabels >= clabels) { return false; } Name n = new Name(child, clabels - plabels); return parent.equals(n); } /** * Determine by looking at a signed RRset whether or not the rrset name was * the result of a wildcard expansion. * * @param rrset * The rrset to examine. * @return true if the rrset is a wildcard expansion. This will return false * for all unsigned rrsets. */ public static boolean rrsetIsWildcardExpansion(RRset rrset) { if (rrset == null) { return false; } RRSIGRecord rrsig = rrsetFirstSig(rrset); if ((rrset.getName().labels() - 1) > rrsig.getLabels()) { return true; } return false; } /** * Determine by looking at a signed RRset whether or not the RRset name was * the result of a wildcard expansion. If so, return the name of the * generating wildcard. * * @param rrset * The rrset to check. * @return the wildcard name, if the rrset was synthesized from a wildcard. * null if not. */ public static Name rrsetWildcard(RRset rrset) { if (rrset == null) { return null; } RRSIGRecord rrsig = rrsetFirstSig(rrset); // if the RRSIG label count is shorter than the number of actual labels, // then this rrset was synthesized from a wildcard. // Note that the RRSIG label count doesn't count the root label. int label_diff = (rrset.getName().labels() - 1) - rrsig.getLabels(); if (label_diff > 0) { return rrset.getName().wild(label_diff); } return null; } public static Name closestEncloser(Name domain, NSECRecord nsec) { Name n1 = longestCommonName(domain, nsec.getName()); Name n2 = longestCommonName(domain, nsec.getNext()); return (n1.labels() > n2.labels()) ? n1 : n2; } public static Name nsecWildcard(Name domain, NSECRecord nsec) { try { return new Name("*", closestEncloser(domain, nsec)); } catch (TextParseException e) { // this should never happen. return null; } } /** * Determine if the given NSEC proves a NameError (NXDOMAIN) for a given * qname. * * @param nsec * The NSEC to check. * @param qname * The qname to check against. * @param signerName * The signer name of the NSEC record, which is used as the zone * name, for a more precise (but perhaps more brittle) check for * the last NSEC in a zone. * @return true if the NSEC proves the condition. */ public static boolean nsecProvesNameError(NSECRecord nsec, Name qname, Name signerName) { Name owner = nsec.getName(); Name next = nsec.getNext(); // If NSEC owner == qname, then this NSEC proves that qname exists. if (qname.equals(owner)) { return false; } // If NSEC is a parent of qname, we need to check the type map // If the parent name has a DNAME or is a delegation point, then this // NSEC is being misused. boolean hasBadType = typeMapHasType(nsec.getTypes(), Type.DNAME) || (typeMapHasType(nsec.getTypes(), Type.NS) && !typeMapHasType( nsec.getTypes(), Type.SOA)); if (qname.subdomain(owner) && hasBadType) { return false; } if (((qname.compareTo(owner) > 0) && (qname.compareTo(next) < 0)) || signerName.equals(next)) { return true; } return false; } /** * Determine if a NSEC record proves the non-existence of a wildcard that * could have produced qname. * * @param nsec * The nsec to check. * @param qname * The qname to check against. * @param signerName * The signer name for the NSEC rrset, used as the zone name. * @return true if the NSEC proves the condition. */ public static boolean nsecProvesNoWC(NSECRecord nsec, Name qname, Name signerName) { Name owner = nsec.getName(); Name next = nsec.getNext(); int qname_labels = qname.labels(); int signer_labels = signerName.labels(); for (int i = qname_labels - signer_labels; i > 0; i--) { Name wc_name = qname.wild(i); if ((wc_name.compareTo(owner) > 0) && ((wc_name.compareTo(next) < 0) || signerName .equals(next))) { return true; } } return false; } /** * Determine if a NSEC proves the NOERROR/NODATA conditions. This will also * handle the empty non-terminal (ENT) case and partially handle the * wildcard case. If the ownername of 'nsec' is a wildcard, the validator * must still be provided proof that qname did not directly exist and that * the wildcard is, in fact, *.closest_encloser. * * @param nsec * The NSEC to check * @param qname * The query name to check against. * @param qtype * The query type to check against. * @return true if the NSEC proves the condition. */ public static boolean nsecProvesNodata(NSECRecord nsec, Name qname, int qtype) { if (!nsec.getName().equals(qname)) { // wildcard checking. // If this is a wildcard NSEC, make sure that a) it was possible to // have // generated qname from the wildcard and b) the type map does not // contain qtype. Note that this does NOT prove that this wildcard // was // the applicable wildcard. if (nsec.getName().isWild()) { // the is the purported closest encloser. Name ce = new Name(nsec.getName(), 1); // The qname must be a strict subdomain of the closest encloser, // and // the qtype must be absent from the type map. if (!strictSubdomain(qname, ce) || typeMapHasType(nsec.getTypes(), qtype)) { return false; } return true; } // empty-non-terminal checking. // If the nsec is proving that qname is an ENT, the nsec owner will // be // less than qname, and the next name will be a child domain of the // qname. if (strictSubdomain(nsec.getNext(), qname) && (qname.compareTo(nsec.getName()) > 0)) { return true; } // Otherwise, this NSEC does not prove ENT, so it does not prove // NODATA. return false; } // If the qtype exists, then we should have gotten it. if (typeMapHasType(nsec.getTypes(), qtype)) { return false; } // if the name is a CNAME node, then we should have gotten the CNAME if (typeMapHasType(nsec.getTypes(), Type.CNAME)) { return false; } // If an NS set exists at this name, and NOT a SOA (so this is a zone // cut, // not a zone apex), then we should have gotten a referral (or we just // got // the wrong NSEC). if (typeMapHasType(nsec.getTypes(), Type.NS) && !typeMapHasType(nsec.getTypes(), Type.SOA)) { return false; } return true; } public static byte nsecProvesNoDS(NSECRecord nsec, Name qname) { // Could check to make sure the qname is a subdomain of nsec int[] types = nsec.getTypes(); if (typeMapHasType(types, Type.SOA) || typeMapHasType(types, Type.DS)) { // SOA present means that this is the NSEC from the child, not the // parent (so it is the wrong one) // DS present means that there should have been a positive response // to // the DS query, so there is something wrong. return SecurityStatus.BOGUS; } if (!typeMapHasType(types, Type.NS)) { // If there is no NS at this point at all, then this doesn't prove // anything one way or the other. return SecurityStatus.INSECURE; } // Otherwise, this proves no DS. return SecurityStatus.SECURE; } // These are response subtypes. They are necessary for determining the // validation strategy. They have no bearing on the iterative resolution // algorithm, so they are confined here. public enum ResponseType { UNTYPED, UNKNOWN, POSITIVE, CNAME, NODATA, NAMEERROR, ANY, REFERRAL, // a referral response THROWAWAY; // a throwaway response (i.e., an error) } }