// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/base/dnssec_chain_verifier.h" #include "base/logging.h" #include "base/memory/scoped_ptr.h" #include "base/sha1.h" #include "base/string_util.h" #include "crypto/sha2.h" #include "net/base/dns_util.h" #include "net/base/dnssec_keyset.h" // We don't have a location for the spec yet, so we'll include it here until it // finds a better home. /* When connecting to a host www.example.com, www.example.com may present a certificate which includes a DNSSEC chain embedded in it. The aim of the embedded chain is to prove that the fingerprint of the public key is valid DNSSEC data. This is achieved by proving a CERT record for the target domain. Initially, the target domain is constructed by prepending _ssl. For example, the initial target domain for www.example.com is _ssl.www.example.com. A DNSSEC chain verifier can be in one of two states: entering a zone, or within a zone. Initially, the verifier is entering the root zone. When entering a zone, the verifier reads the following structure: uint8 entryKey uint16 signature length: // See RRSIG RDATA in RFC 4043 for details uint8 algorithm uint8 labels uint32 ttl uint32 expires uint32 begins uint16 keyid []byte signature uint8 numKeys // for each key: uint16 key length: []byte DNSKEY RDATA |entryKey| indexes the array of DNSKEYs and MUST be less than |numKeys|. The indexed DNSKEY MUST be a key that the verifier trusts, either because it's the long-term root key, or because of a previously presented DS signature. If only a trusted key is needed within this zone, then the signature length MAY be zero. In which case, |entryKey| MUST be 0 and |numKeys| MUST be 1. After processing this data, the verifier trusts one or more keys for this zone. When within a zone, the verifier reads the following structure: dnsName name uint16 RRtype |name| is in DNS format (a series of 8-bit, length prefixed strings). No DNS name compression is permitted. |name| must be closer to the current target domain than the current zone. Here, 'closer' is defined as a greater number of matching labels when comparing right to left. |RRtype| may be either DS, CERT or CNAME: DS: this indicates a zone transition to a new zone named |name|. The verifier reads the following structure: uint16 signature length: ... (see above for the signature structure) uint8 num_ds // for each DS: uint8 digest_type uint16 length []byte DS DATA The verifier is now entering the named zone. It reads ahead and extracts the entry key from the zone entry data and synthisises a DS record for the given digest type and verifies the signature. It then enters the next zone. CERT: |name| MUST match the target domain. The verifier reads the following structure: uint16 signature length: ... (see above for the signature structure) []byte CERT RDATA (The format of the CERT RDATA isn't specified here, but the verifier must be able to extract a public key fingerprint in order to validate the original certificate.) This terminates the verification. There MUST NOT be any more data in the chain. CNAME: |name| MUST match the target domain. The verifier reads the following structure: uint16 signature length: ... (see above for the signature structure) []byte CNAME RDATA This replaces the target domain with a new domain. The new domain is the target of the CNAME with _ssl prepended. The verifier is now in the zone that is the greatest common ancestor of the old and new target domains. (For example, when switching from _ssl.www.example.com to _ssl.www.example2.com, the verifier is now in com.) Example for www.google.com: The target domain is www.google.com. The verifier enters ., it already trusts the long-term root key and both root keys are presented in order to extend the trust to the smaller root key. A DS signature is presented for .com. The verifier is now entering .com. All four .com keys are presented. The verifier is now in .com. A DS signature is presented for google.com. The verifier is now entering google.com As google.com contains only a single DNSKEY, it is included without a signature. The verifier is now in google.com. A CNAME is presented for www.google.com pointing to www.l.google.com. The target domain is now www.l.google.com. The verifier is now in google.com. A DS signature is presented for l.google.com. The verifier is now entering l.google.com. As l.google.com contains only a single DNSKEY, it is included without a signature. The verifier is now in l.google.com. A CERT record is presented for www.l.google.com. The verification is complete. */ namespace { // This is the 2048-bit DNS root key: http://www.iana.org/dnssec // 19036 8 2 49AAC11D7B6F6446702E54A1607371607A1A41855200FD2CE1CDDE32F24E8FB5 const unsigned char kRootKey[] = { 0x01, 0x01, 0x03, 0x08, 0x03, 0x01, 0x00, 0x01, 0xa8, 0x00, 0x20, 0xa9, 0x55, 0x66, 0xba, 0x42, 0xe8, 0x86, 0xbb, 0x80, 0x4c, 0xda, 0x84, 0xe4, 0x7e, 0xf5, 0x6d, 0xbd, 0x7a, 0xec, 0x61, 0x26, 0x15, 0x55, 0x2c, 0xec, 0x90, 0x6d, 0x21, 0x16, 0xd0, 0xef, 0x20, 0x70, 0x28, 0xc5, 0x15, 0x54, 0x14, 0x4d, 0xfe, 0xaf, 0xe7, 0xc7, 0xcb, 0x8f, 0x00, 0x5d, 0xd1, 0x82, 0x34, 0x13, 0x3a, 0xc0, 0x71, 0x0a, 0x81, 0x18, 0x2c, 0xe1, 0xfd, 0x14, 0xad, 0x22, 0x83, 0xbc, 0x83, 0x43, 0x5f, 0x9d, 0xf2, 0xf6, 0x31, 0x32, 0x51, 0x93, 0x1a, 0x17, 0x6d, 0xf0, 0xda, 0x51, 0xe5, 0x4f, 0x42, 0xe6, 0x04, 0x86, 0x0d, 0xfb, 0x35, 0x95, 0x80, 0x25, 0x0f, 0x55, 0x9c, 0xc5, 0x43, 0xc4, 0xff, 0xd5, 0x1c, 0xbe, 0x3d, 0xe8, 0xcf, 0xd0, 0x67, 0x19, 0x23, 0x7f, 0x9f, 0xc4, 0x7e, 0xe7, 0x29, 0xda, 0x06, 0x83, 0x5f, 0xa4, 0x52, 0xe8, 0x25, 0xe9, 0xa1, 0x8e, 0xbc, 0x2e, 0xcb, 0xcf, 0x56, 0x34, 0x74, 0x65, 0x2c, 0x33, 0xcf, 0x56, 0xa9, 0x03, 0x3b, 0xcd, 0xf5, 0xd9, 0x73, 0x12, 0x17, 0x97, 0xec, 0x80, 0x89, 0x04, 0x1b, 0x6e, 0x03, 0xa1, 0xb7, 0x2d, 0x0a, 0x73, 0x5b, 0x98, 0x4e, 0x03, 0x68, 0x73, 0x09, 0x33, 0x23, 0x24, 0xf2, 0x7c, 0x2d, 0xba, 0x85, 0xe9, 0xdb, 0x15, 0xe8, 0x3a, 0x01, 0x43, 0x38, 0x2e, 0x97, 0x4b, 0x06, 0x21, 0xc1, 0x8e, 0x62, 0x5e, 0xce, 0xc9, 0x07, 0x57, 0x7d, 0x9e, 0x7b, 0xad, 0xe9, 0x52, 0x41, 0xa8, 0x1e, 0xbb, 0xe8, 0xa9, 0x01, 0xd4, 0xd3, 0x27, 0x6e, 0x40, 0xb1, 0x14, 0xc0, 0xa2, 0xe6, 0xfc, 0x38, 0xd1, 0x9c, 0x2e, 0x6a, 0xab, 0x02, 0x64, 0x4b, 0x28, 0x13, 0xf5, 0x75, 0xfc, 0x21, 0x60, 0x1e, 0x0d, 0xee, 0x49, 0xcd, 0x9e, 0xe9, 0x6a, 0x43, 0x10, 0x3e, 0x52, 0x4d, 0x62, 0x87, 0x3d, }; // kRootKeyID is the key id for kRootKey const uint16 kRootKeyID = 19036; // CountLabels returns the number of DNS labels in |a|, which must be in DNS, // length-prefixed form. unsigned CountLabels(base::StringPiece a) { for (unsigned c = 0;; c++) { if (!a.size()) return c; uint8 label_len = a.data()[0]; a.remove_prefix(1); DCHECK_GE(a.size(), label_len); a.remove_prefix(label_len); } } // RemoveLeadingLabel removes the first label from |a|, which must be in DNS, // length-prefixed form. void RemoveLeadingLabel(base::StringPiece* a) { if (!a->size()) return; uint8 label_len = a->data()[0]; a->remove_prefix(1); a->remove_prefix(label_len); } } // namespace namespace net { struct DNSSECChainVerifier::Zone { base::StringPiece name; // The number of consecutive labels which |name| shares with |target_|, // counting right-to-left from the root. unsigned matching_labels; DNSSECKeySet trusted_keys; Zone* prev; }; DNSSECChainVerifier::DNSSECChainVerifier(const std::string& target, const base::StringPiece& chain) : current_zone_(NULL), target_(target), chain_(chain), ignore_timestamps_(false), valid_(false), already_entered_zone_(false), rrtype_(0) { } DNSSECChainVerifier::~DNSSECChainVerifier() { for (std::vector<void*>::iterator i = scratch_pool_.begin(); i != scratch_pool_.end(); i++) { free(*i); } Zone* next; for (Zone* cur = current_zone_; cur; cur = next) { next = cur->prev; delete cur; } } void DNSSECChainVerifier::IgnoreTimestamps() { ignore_timestamps_ = true; } DNSSECChainVerifier::Error DNSSECChainVerifier::Verify() { Error err; err = EnterRoot(); if (err != OK) return err; for (;;) { base::StringPiece next_name; err = LeaveZone(&next_name); if (err != OK) return err; if (valid_) { if (!chain_.empty()) return BAD_DATA; // no trailing data allowed. break; } if (already_entered_zone_) { already_entered_zone_ = false; } else { err = EnterZone(next_name); if (err != OK) return err; } } return OK; } uint16 DNSSECChainVerifier::rrtype() const { DCHECK(valid_); return rrtype_; } const std::vector<base::StringPiece>& DNSSECChainVerifier::rrdatas() const { DCHECK(valid_); return rrdatas_; } // static std::map<std::string, std::string> DNSSECChainVerifier::ParseTLSTXTRecord(base::StringPiece rrdata) { std::map<std::string, std::string> ret; if (rrdata.empty()) return ret; std::string txt; txt.reserve(rrdata.size()); // TXT records are a series of 8-bit length prefixed substrings that we // concatenate into |txt| while (!rrdata.empty()) { unsigned len = rrdata[0]; if (len == 0 || len + 1 > rrdata.size()) return ret; txt.append(rrdata.data() + 1, len); rrdata.remove_prefix(len + 1); } // We append a space to |txt| to make the parsing code, below, cleaner. txt.append(" "); // RECORD = KV (' '+ KV)* // KV = KEY '=' VALUE // KEY = [a-zA-Z0-9]+ // VALUE = [^ \0]* enum State { STATE_KEY, STATE_VALUE, STATE_SPACE, }; State state = STATE_KEY; std::map<std::string, std::string> m; unsigned start = 0; std::string key; for (unsigned i = 0; i < txt.size(); i++) { char c = txt[i]; if (c == 0) return ret; // NUL values are never allowed. switch (state) { case STATE_KEY: if (c == '=') { if (i == start) return ret; // zero length keys are not allowed. key = txt.substr(start, i - start); start = i + 1; state = STATE_VALUE; continue; } if (!IsAsciiAlpha(c) && !IsAsciiDigit(c)) return ret; // invalid key value break; case STATE_VALUE: if (c == ' ') { if (m.find(key) == m.end()) m.insert(make_pair(key, txt.substr(start, i - start))); state = STATE_SPACE; continue; } break; case STATE_SPACE: if (c != ' ') { start = i; i--; state = STATE_KEY; continue; } break; default: NOTREACHED(); return ret; } } if (state != STATE_SPACE) return ret; ret.swap(m); return ret; } // MatchingLabels returns the number of labels which |a| and |b| share, // counting right-to-left from the root. |a| and |b| must be DNS, // length-prefixed names. All names match at the root label, so this always // returns a value >= 1. // static unsigned DNSSECChainVerifier::MatchingLabels(base::StringPiece a, base::StringPiece b) { unsigned c = 0; unsigned a_labels = CountLabels(a); unsigned b_labels = CountLabels(b); while (a_labels > b_labels) { RemoveLeadingLabel(&a); a_labels--; } while (b_labels > a_labels) { RemoveLeadingLabel(&b); b_labels--; } for (;;) { if (!a.size()) { if (!b.size()) return c; return 0; } if (!b.size()) return 0; uint8 a_length = a.data()[0]; a.remove_prefix(1); uint8 b_length = b.data()[0]; b.remove_prefix(1); DCHECK_GE(a.size(), a_length); DCHECK_GE(b.size(), b_length); if (a_length == b_length && memcmp(a.data(), b.data(), a_length) == 0) { c++; } else { c = 0; } a.remove_prefix(a_length); b.remove_prefix(b_length); } } // U8 reads, and removes, a single byte from |chain_| bool DNSSECChainVerifier::U8(uint8* v) { if (chain_.size() < 1) return false; *v = chain_[0]; chain_.remove_prefix(1); return true; } // U16 reads, and removes, a big-endian uint16 from |chain_| bool DNSSECChainVerifier::U16(uint16* v) { if (chain_.size() < 2) return false; const uint8* data = reinterpret_cast<const uint8*>(chain_.data()); *v = static_cast<uint16>(data[0]) << 8 | static_cast<uint16>(data[1]); chain_.remove_prefix(2); return true; } // VariableLength16 reads, and removes, a big-endian, uint16, length-prefixed // chunk from |chain_| bool DNSSECChainVerifier::VariableLength16(base::StringPiece* v) { uint16 length; if (!U16(&length)) return false; if (chain_.size() < length) return false; *v = chain_.substr(0, length); chain_.remove_prefix(length); return true; } // ReadName reads, and removes, an 8-bit length prefixed DNS name from |chain_| bool DNSSECChainVerifier::ReadName(base::StringPiece* v) { base::StringPiece saved = chain_; unsigned length = 0; static const uint8 kMaxDNSLabelLen = 63; for (;;) { if (chain_.size() < 1) return false; uint8 label_len = chain_.data()[0]; chain_.remove_prefix(1); if (label_len > kMaxDNSLabelLen) return false; length += 1 + label_len; if (label_len == 0) break; if (chain_.size() < label_len) return false; chain_.remove_prefix(label_len); } *v = base::StringPiece(saved.data(), length); return true; } // ReadAheadEntryKey returns the entry key when |chain_| is positioned at the // start of a zone. bool DNSSECChainVerifier::ReadAheadEntryKey(base::StringPiece* v) { base::StringPiece saved = chain_; uint8 entry_key; base::StringPiece sig; if (!U8(&entry_key) || !VariableLength16(&sig)) { return false; } if (!ReadAheadKey(v, entry_key)) return false; chain_ = saved; return true; } // ReadAheadKey returns the entry key when |chain_| is positioned at the start // of a list of keys. bool DNSSECChainVerifier::ReadAheadKey(base::StringPiece* v, uint8 entry_key) { base::StringPiece saved = chain_; uint8 num_keys; if (!U8(&num_keys)) return false; for (unsigned i = 0; i < num_keys; i++) { if (!VariableLength16(v)) return false; if (i == entry_key) { chain_ = saved; return true; } } return false; } bool DNSSECChainVerifier::ReadDNSKEYs(std::vector<base::StringPiece>* out, bool is_root) { uint8 num_keys; if (!U8(&num_keys)) return false; for (unsigned i = 0; i < num_keys; i++) { base::StringPiece key; if (!VariableLength16(&key)) return false; if (key.empty()) { if (!is_root) return false; key = base::StringPiece(reinterpret_cast<const char*>(kRootKey), sizeof(kRootKey)); } out->push_back(key); } return true; } // DigestKey calculates a DS digest as specified in // http://tools.ietf.org/html/rfc4034#section-5.1.4 // name: the DNS form name of the key // dnskey: the DNSKEY's RRDATA // digest_type: see http://tools.ietf.org/html/rfc4034#appendix-A.2 // keyid: the key's id // algorithm: see http://tools.ietf.org/html/rfc4034#appendix-A.1 bool DNSSECChainVerifier::DigestKey(base::StringPiece* out, const base::StringPiece& name, const base::StringPiece& dnskey, uint8 digest_type, uint16 keyid, uint8 algorithm) { std::string temp; uint8 temp2[crypto::SHA256_LENGTH]; const uint8* digest; unsigned digest_len; std::string input = name.as_string() + dnskey.as_string(); if (digest_type == kDNSSEC_SHA1) { temp = base::SHA1HashString(input); digest = reinterpret_cast<const uint8*>(temp.data()); digest_len = base::SHA1_LENGTH; } else if (digest_type == kDNSSEC_SHA256) { crypto::SHA256HashString(input, temp2, sizeof(temp2)); digest = temp2; digest_len = sizeof(temp2); } else { return false; } uint8* output = static_cast<uint8*>(malloc(4 + digest_len)); scratch_pool_.push_back(output); output[0] = static_cast<uint8>(keyid >> 8); output[1] = static_cast<uint8>(keyid); output[2] = algorithm; output[3] = digest_type; memcpy(output + 4, digest, digest_len); *out = base::StringPiece(reinterpret_cast<char*>(output), 4 + digest_len); return true; } // EnterRoot enters the root zone at the beginning of the chain. This is // special because no DS record lead us here: we have to validate that the // entry key is the DNS root key that we already know and trust. Additionally, // for the root zone only, the keyid of the entry key is prepended to the data. DNSSECChainVerifier::Error DNSSECChainVerifier::EnterRoot() { uint16 root_keyid; if (!U16(&root_keyid)) return BAD_DATA; if (root_keyid != kRootKeyID) return UNKNOWN_ROOT_KEY; base::StringPiece root_key; if (!ReadAheadEntryKey(&root_key)) return BAD_DATA; // If the root key is given then it must match the expected root key exactly. if (root_key.size()) { if (root_key.size() != sizeof(kRootKey) || memcmp(root_key.data(), kRootKey, sizeof(kRootKey))) { return UNKNOWN_ROOT_KEY; } } base::StringPiece root("", 1); return EnterZone(root); } // EnterZone enters a new DNS zone. On entry it's assumed that the entry key // has been validated. DNSSECChainVerifier::Error DNSSECChainVerifier::EnterZone( const base::StringPiece& zone) { Zone* prev = current_zone_; current_zone_ = new Zone; current_zone_->prev = prev; current_zone_->name = zone; current_zone_->matching_labels = MatchingLabels(target_, zone); if (ignore_timestamps_) current_zone_->trusted_keys.IgnoreTimestamps(); uint8 entry_key; base::StringPiece sig; if (!U8(&entry_key) || !VariableLength16(&sig)) { return BAD_DATA; } base::StringPiece key; if (!ReadAheadKey(&key, entry_key)) return BAD_DATA; if (zone.size() == 1 && key.empty()) { // If a key is omitted in the root zone then it's the root key. key = base::StringPiece(reinterpret_cast<const char*>(kRootKey), sizeof(kRootKey)); } if (!current_zone_->trusted_keys.AddKey(key)) return BAD_DATA; std::vector<base::StringPiece> dnskeys; if (!ReadDNSKEYs(&dnskeys, zone.size() == 1)) return BAD_DATA; if (sig.empty()) { // An omitted signature on the keys means that only the entry key is used. if (dnskeys.size() > 1 || entry_key != 0) return BAD_DATA; return OK; } if (!current_zone_->trusted_keys.CheckSignature( zone, zone, sig, kDNS_DNSKEY, dnskeys)) { return BAD_SIGNATURE; } // Add all the keys as trusted. for (unsigned i = 0; i < dnskeys.size(); i++) { if (i == entry_key) continue; current_zone_->trusted_keys.AddKey(dnskeys[i]); } return OK; } // LeaveZone transitions out of the current zone, either by following DS // records to validate the entry key of the next zone, or because the final // resource records are given. DNSSECChainVerifier::Error DNSSECChainVerifier::LeaveZone( base::StringPiece* next_name) { base::StringPiece sig; uint16 rrtype; Error err; if (!ReadName(next_name) || !U16(&rrtype) || !VariableLength16(&sig)) { return BAD_DATA; } std::vector<base::StringPiece> rrdatas; if (rrtype == kDNS_DS) { err = ReadDSSet(&rrdatas, *next_name); } else if (rrtype == kDNS_CERT || rrtype == kDNS_TXT) { err = ReadGenericRRs(&rrdatas); } else if (rrtype == kDNS_CNAME) { err = ReadCNAME(&rrdatas); } else { return UNKNOWN_TERMINAL_RRTYPE; } if (err != OK) return err; if (!current_zone_->trusted_keys.CheckSignature( *next_name, current_zone_->name, sig, rrtype, rrdatas)) { return BAD_SIGNATURE; } if (rrtype == kDNS_DS) { // If we are transitioning to another zone then the next zone must be // 'closer' to the target than the current zone. if (MatchingLabels(target_, *next_name) <= current_zone_->matching_labels) return OFF_COURSE; } else if (rrtype == kDNS_CERT || rrtype == kDNS_TXT) { // If this is the final entry in the chain then the name must match target_ if (next_name->size() != target_.size() || memcmp(next_name->data(), target_.data(), target_.size())) { return BAD_TARGET; } rrdatas_ = rrdatas; valid_ = true; rrtype_ = rrtype; } else if (rrtype == kDNS_CNAME) { // A CNAME must match the current target. Then we update the current target // and unwind the chain to the closest common ancestor. if (next_name->size() != target_.size() || memcmp(next_name->data(), target_.data(), target_.size())) { return BAD_TARGET; } DCHECK_EQ(1u, rrdatas.size()); target_ = rrdatas[0].as_string(); // We unwind the zones until the current zone is a (non-strict) subset of // the new target. while (MatchingLabels(target_, current_zone_->name) < CountLabels(current_zone_->name)) { Zone* prev = current_zone_->prev; delete current_zone_; current_zone_ = prev; if (!current_zone_) { NOTREACHED(); return BAD_DATA; } } already_entered_zone_ = true; } else { NOTREACHED(); return UNKNOWN_TERMINAL_RRTYPE; } return OK; } // ReadDSSet reads a set of DS records from the chain. DS records which are // omitted are calculated from the entry key of the next zone. DNSSECChainVerifier::Error DNSSECChainVerifier::ReadDSSet( std::vector<base::StringPiece>* rrdatas, const base::StringPiece& next_name) { uint8 num_ds; if (!U8(&num_ds)) return BAD_DATA; scoped_array<uint8> digest_types(new uint8[num_ds]); // lookahead[i] is true iff the i'th DS record was empty and needs to be // computed by hashing the next entry key. scoped_array<bool> lookahead(new bool[num_ds]); rrdatas->resize(num_ds); for (unsigned i = 0; i < num_ds; i++) { uint8 digest_type; base::StringPiece digest; if (!U8(&digest_type) || !VariableLength16(&digest)) { return BAD_DATA; } digest_types[i] = digest_type; lookahead[i] = digest.empty(); if (!digest.empty()) (*rrdatas)[i] = digest; } base::StringPiece next_entry_key; if (!ReadAheadEntryKey(&next_entry_key)) return BAD_DATA; if (next_entry_key.size() < 4) return BAD_DATA; uint16 keyid = DNSSECKeySet::DNSKEYToKeyID(next_entry_key); uint8 algorithm = next_entry_key[3]; bool good = false; for (unsigned i = 0; i < num_ds; i++) { base::StringPiece digest; bool have_digest = false; if (DigestKey(&digest, next_name, next_entry_key, digest_types[i], keyid, algorithm)) { have_digest = true; } if (lookahead[i]) { // If we needed to fill in one of the DS entries, but we can't calculate // that type of digest, then we can't continue. if (!have_digest) return UNKNOWN_DIGEST; (*rrdatas)[i] = digest; good = true; } else { const base::StringPiece& given_digest = (*rrdatas)[i]; if (have_digest && given_digest.size() == digest.size() && memcmp(given_digest.data(), digest.data(), digest.size()) == 0) { good = true; } } } if (!good) { // We didn't calculate or match any of the digests. return NO_DS_LINK; } return OK; } DNSSECChainVerifier::Error DNSSECChainVerifier::ReadGenericRRs( std::vector<base::StringPiece>* rrdatas) { uint8 num_rrs; if (!U8(&num_rrs)) return BAD_DATA; rrdatas->resize(num_rrs); for (unsigned i = 0; i < num_rrs; i++) { base::StringPiece rrdata; if (!VariableLength16(&rrdata)) return BAD_DATA; (*rrdatas)[i] = rrdata; } return OK; } DNSSECChainVerifier::Error DNSSECChainVerifier::ReadCNAME( std::vector<base::StringPiece>* rrdatas) { base::StringPiece name; if (!ReadName(&name)) return BAD_DATA; rrdatas->resize(1); (*rrdatas)[0] = name; return OK; } } // namespace net