]> git.ipfire.org Git - thirdparty/pdns.git/blob - pdns/zonemd.cc
Merge pull request #14032 from rgacogne/ddist-192-changelog-secpoll
[thirdparty/pdns.git] / pdns / zonemd.cc
1 #include "zonemd.hh"
2
3 #include "dnsrecords.hh"
4 #include "dnssecinfra.hh"
5 #include "sha.hh"
6 #include "zoneparser-tng.hh"
7 #include "base32.hh"
8
9 void pdns::ZoneMD::readRecords(ZoneParserTNG& zpt)
10 {
11 DNSResourceRecord dnsResourceRecord;
12
13 while (zpt.get(dnsResourceRecord)) {
14 std::shared_ptr<DNSRecordContent> drc;
15 try {
16 drc = DNSRecordContent::make(dnsResourceRecord.qtype, QClass::IN, dnsResourceRecord.content);
17 }
18 catch (const PDNSException& pe) {
19 std::string err = "Bad record content in record for '" + dnsResourceRecord.qname.toStringNoDot() + "'|" + dnsResourceRecord.qtype.toString() + ": " + pe.reason;
20 throw PDNSException(err);
21 }
22 catch (const std::exception& e) {
23 std::string err = "Bad record content in record for '" + dnsResourceRecord.qname.toStringNoDot() + "|" + dnsResourceRecord.qtype.toString() + "': " + e.what();
24 throw PDNSException(err);
25 }
26 DNSRecord rec;
27 rec.d_name = dnsResourceRecord.qname;
28 rec.setContent(std::move(drc));
29 rec.d_type = dnsResourceRecord.qtype;
30 rec.d_class = dnsResourceRecord.qclass;
31 rec.d_ttl = dnsResourceRecord.ttl;
32 rec.d_clen = dnsResourceRecord.content.length(); // XXX is this correct?
33 readRecord(rec);
34 }
35 }
36
37 void pdns::ZoneMD::readRecords(const vector<DNSRecord>& records)
38 {
39 for (const auto& record : records) {
40 readRecord(record);
41 }
42 }
43
44 void pdns::ZoneMD::processRecord(const DNSRecord& record)
45 {
46 if (record.d_class == QClass::IN && record.d_name == d_zone) {
47 switch (record.d_type) {
48 case QType::SOA: {
49 d_soaRecordContent = getRR<SOARecordContent>(record);
50 if (d_soaRecordContent == nullptr) {
51 throw PDNSException("Invalid SOA record");
52 }
53 break;
54 }
55 case QType::DNSKEY: {
56 auto dnskey = getRR<DNSKEYRecordContent>(record);
57 if (dnskey == nullptr) {
58 throw PDNSException("Invalid DNSKEY record");
59 }
60 d_dnskeys.emplace(dnskey);
61 break;
62 }
63 case QType::ZONEMD: {
64 auto zonemd = getRR<ZONEMDRecordContent>(record);
65 if (zonemd == nullptr) {
66 throw PDNSException("Invalid ZONEMD record");
67 }
68 auto inserted = d_zonemdRecords.insert({pair(zonemd->d_scheme, zonemd->d_hashalgo), {zonemd, false}});
69 if (!inserted.second) {
70 // Mark as duplicate
71 inserted.first->second.duplicate = true;
72 }
73 break;
74 }
75 case QType::RRSIG: {
76 auto rrsig = getRR<RRSIGRecordContent>(record);
77 if (rrsig == nullptr) {
78 throw PDNSException("Invalid RRSIG record");
79 }
80 d_rrsigs[rrsig->d_type].emplace_back(rrsig);
81 if (rrsig->d_type == QType::NSEC) {
82 d_nsecs.signatures.emplace_back(rrsig);
83 }
84 // RRSIG on NEC3 handled below
85 break;
86 }
87 case QType::NSEC: {
88 auto nsec = getRR<NSECRecordContent>(record);
89 if (nsec == nullptr) {
90 throw PDNSException("Invalid NSEC record");
91 }
92 d_nsecs.records.emplace(nsec);
93 break;
94 }
95 case QType::NSEC3:
96 // Handled below
97 break;
98 case QType::NSEC3PARAM: {
99 auto param = getRR<NSEC3PARAMRecordContent>(record);
100 if (param == nullptr) {
101 throw PDNSException("Invalid NSEC3PARAM record");
102 }
103 if (g_maxNSEC3Iterations > 0 && param->d_iterations > g_maxNSEC3Iterations) {
104 return;
105 }
106 d_nsec3params.emplace_back(param);
107 d_nsec3label = d_zone;
108 d_nsec3label.prependRawLabel(toBase32Hex(hashQNameWithSalt(param->d_salt, param->d_iterations, d_zone)));
109 // Zap the NSEC3 at labels that we now know are not relevant
110 for (auto item = d_nsec3s.begin(); item != d_nsec3s.end();) {
111 if (item->first != d_nsec3label) {
112 item = d_nsec3s.erase(item);
113 }
114 else {
115 ++item;
116 }
117 }
118 break;
119 }
120 }
121 }
122 }
123
124 void pdns::ZoneMD::readRecord(const DNSRecord& record)
125 {
126 if (!record.d_name.isPartOf(d_zone) && record.d_name != d_zone) {
127 return;
128 }
129 if (record.d_class == QClass::IN && record.d_type == QType::SOA && d_soaRecordContent) {
130 return;
131 }
132
133 processRecord(record);
134
135 // Until we have seen the NSEC3PARAM record, we save all of them, as we do not know the label for the zone yet
136 if (record.d_class == QClass::IN && (d_nsec3label.empty() || record.d_name == d_nsec3label)) {
137 switch (record.d_type) {
138 case QType::NSEC3: {
139 auto nsec3 = getRR<NSEC3RecordContent>(record);
140 if (nsec3 == nullptr) {
141 throw PDNSException("Invalid NSEC3 record");
142 }
143 d_nsec3s[record.d_name].records.emplace(nsec3);
144 break;
145 }
146 case QType::RRSIG: {
147 auto rrsig = getRR<RRSIGRecordContent>(record);
148 if (rrsig == nullptr) {
149 throw PDNSException("Invalid RRSIG record");
150 }
151 if (rrsig->d_type == QType::NSEC3) {
152 d_nsec3s[record.d_name].signatures.emplace_back(rrsig);
153 }
154 break;
155 }
156 }
157 }
158 RRSetKey_t key = std::pair(record.d_name, record.d_type);
159 d_resourceRecordSets[key].push_back(record.getContent());
160 d_resourceRecordSetTTLs[key] = record.d_ttl;
161 }
162
163 void pdns::ZoneMD::verify(bool& validationDone, bool& validationOK)
164 {
165 validationDone = false;
166 validationOK = false;
167
168 if (!d_soaRecordContent) {
169 return;
170 }
171 // Get all records and remember RRSets and TTLs
172
173 // Determine which digests to compute based on accepted zonemd records present
174 unique_ptr<pdns::SHADigest> sha384digest{nullptr};
175 unique_ptr<pdns::SHADigest> sha512digest{nullptr};
176
177 for (const auto& item : d_zonemdRecords) {
178 // The SOA Serial field MUST exactly match the ZONEMD Serial
179 // field. If the fields do not match, digest verification MUST
180 // NOT be considered successful with this ZONEMD RR.
181
182 // The Scheme field MUST be checked. If the verifier does not
183 // support the given scheme, verification MUST NOT be considered
184 // successful with this ZONEMD RR.
185
186 // The Hash Algorithm field MUST be checked. If the verifier does
187 // not support the given hash algorithm, verification MUST NOT be
188 // considered successful with this ZONEMD RR.
189 const auto duplicate = item.second.duplicate;
190 const auto& record = item.second.record;
191 if (!duplicate && record->d_serial == d_soaRecordContent->d_st.serial && record->d_scheme == 1 && (record->d_hashalgo == 1 || record->d_hashalgo == 2)) {
192 // A supported ZONEMD record
193 if (record->d_hashalgo == 1) {
194 sha384digest = make_unique<pdns::SHADigest>(384);
195 }
196 else if (record->d_hashalgo == 2) {
197 sha512digest = make_unique<pdns::SHADigest>(512);
198 }
199 }
200 }
201
202 if (!sha384digest && !sha512digest) {
203 // No supported ZONEMD algo found, mismatch in SOA, mismatch in scheme or duplicate
204 return;
205 }
206
207 // A little helper
208 auto hash = [&sha384digest, &sha512digest](const std::string& msg) {
209 if (sha384digest) {
210 sha384digest->process(msg);
211 }
212 if (sha512digest) {
213 sha512digest->process(msg);
214 }
215 };
216
217 // Compute requested digests
218 for (auto& rrset : d_resourceRecordSets) {
219 const auto& qname = rrset.first.first;
220 const auto& qtype = rrset.first.second;
221 if (qtype == QType::ZONEMD && qname == d_zone) {
222 continue; // the apex ZONEMD is not digested
223 }
224
225 sortedRecords_t sorted;
226 for (auto& resourceRecord : rrset.second) {
227 if (qtype == QType::RRSIG) {
228 const auto rrsig = std::dynamic_pointer_cast<const RRSIGRecordContent>(resourceRecord);
229 if (rrsig->d_type == QType::ZONEMD && qname == d_zone) {
230 continue;
231 }
232 }
233 sorted.insert(resourceRecord);
234 }
235
236 if (sorted.empty()) {
237 continue;
238 }
239
240 if (qtype != QType::RRSIG) {
241 RRSIGRecordContent rrc;
242 rrc.d_originalttl = d_resourceRecordSetTTLs[rrset.first];
243 rrc.d_type = qtype;
244 auto msg = getMessageForRRSET(qname, rrc, sorted, false, false);
245 hash(msg);
246 }
247 else {
248 // RRSIG is special, since original TTL depends on qtype covered by RRSIG
249 // which can be different per record
250 for (const auto& rrsig : sorted) {
251 auto rrsigc = std::dynamic_pointer_cast<const RRSIGRecordContent>(rrsig);
252 RRSIGRecordContent rrc;
253 rrc.d_originalttl = d_resourceRecordSetTTLs[pair(rrset.first.first, rrsigc->d_type)];
254 rrc.d_type = qtype;
255 auto msg = getMessageForRRSET(qname, rrc, {rrsigc}, false, false);
256 hash(msg);
257 }
258 }
259 }
260
261 // Final verify
262 for (const auto& [k, v] : d_zonemdRecords) {
263 auto [zonemd, duplicate] = v;
264 if (zonemd->d_hashalgo == 1) {
265 validationDone = true;
266 auto computed = sha384digest->digest();
267 if (constantTimeStringEquals(zonemd->d_digest, computed)) {
268 validationOK = true;
269 break; // Per RFC: a single succeeding validation is enough
270 }
271 }
272 else if (zonemd->d_hashalgo == 2) {
273 validationDone = true;
274 auto computed = sha512digest->digest();
275 if (constantTimeStringEquals(zonemd->d_digest, computed)) {
276 validationOK = true;
277 break; // Per RFC: a single succeeding validation is enough
278 }
279 }
280 }
281 }