hickory_proto/rr/rdata/caa.rs
1// Copyright 2015-2023 Benjamin Fry <benjaminfry@me.com>
2//
3// Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
4// https://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
5// https://opensource.org/licenses/MIT>, at your option. This file may not be
6// copied, modified, or distributed except according to those terms.
7
8//! allows a DNS domain name holder to specify one or more Certification
9//! Authorities (CAs) authorized to issue certificates for that domain.
10//!
11//! [RFC 8659, DNS Certification Authority Authorization, November 2019](https://www.rfc-editor.org/rfc/rfc8659)
12//!
13//! ```text
14//! The Certification Authority Authorization (CAA) DNS Resource Record
15//! allows a DNS domain name holder to specify one or more Certification
16//! Authorities (CAs) authorized to issue certificates for that domain
17//! name. CAA Resource Records allow a public CA to implement additional
18//! controls to reduce the risk of unintended certificate mis-issue.
19//! This document defines the syntax of the CAA record and rules for
20//! processing CAA records by CAs.
21//! ```
22#![allow(clippy::use_self)]
23
24use alloc::{borrow::ToOwned, string::String, vec::Vec};
25use core::{fmt, str};
26
27#[cfg(feature = "serde")]
28use serde::{Deserialize, Serialize};
29use tracing::warn;
30use url::Url;
31
32use crate::{
33 error::ProtoResult,
34 rr::{RData, RecordData, RecordDataDecodable, RecordType, domain::Name},
35 serialize::{binary::*, txt::ParseError},
36};
37
38/// The CAA RR Type
39///
40/// [RFC 8659, DNS Certification Authority Authorization, November 2019](https://www.rfc-editor.org/rfc/rfc8659)
41#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
42#[derive(Debug, PartialEq, Eq, Hash, Clone)]
43#[non_exhaustive]
44pub struct CAA {
45 /// Indicates that the corresponding property tag MUST be understood if the semantics
46 /// of the CAA record are to be correctly interpreted by an issuer
47 pub issuer_critical: bool,
48
49 /// The flags of the record minus the issuer_critical flag
50 pub reserved_flags: u8,
51
52 /// The property tag
53 pub tag: String,
54
55 /// The raw value of the CAA record
56 pub value: Vec<u8>,
57}
58
59impl CAA {
60 fn issue(
61 issuer_critical: bool,
62 tag: IssueProperty,
63 name: Option<Name>,
64 options: Vec<KeyValue>,
65 ) -> Self {
66 let tag = tag.as_str().to_owned();
67 let value = encode_issuer_value(name.as_ref(), &options);
68
69 Self {
70 issuer_critical,
71 reserved_flags: 0,
72 tag,
73 value,
74 }
75 }
76
77 /// Creates a new CAA issue record data, the tag is `issue`
78 ///
79 /// # Arguments
80 ///
81 /// * `issuer_critical` - indicates that the corresponding property tag MUST be understood if the semantics of the CAA record are to be correctly interpreted by an issuer
82 /// * `name` - authorized to issue certificates for the associated record label
83 /// * `options` - additional options for the issuer, e.g. 'account', etc.
84 pub fn new_issue(issuer_critical: bool, name: Option<Name>, options: Vec<KeyValue>) -> Self {
85 Self::issue(issuer_critical, IssueProperty::Issue, name, options)
86 }
87
88 /// Creates a new CAA issue record data, the tag is `issuewild`
89 ///
90 /// # Arguments
91 ///
92 /// * `issuer_critical` - indicates that the corresponding property tag MUST be understood if the semantics of the CAA record are to be correctly interpreted by an issuer
93 /// * `name` - authorized to issue certificates for the associated record label
94 /// * `options` - additional options for the issuer, e.g. 'account', etc.
95 pub fn new_issuewild(
96 issuer_critical: bool,
97 name: Option<Name>,
98 options: Vec<KeyValue>,
99 ) -> Self {
100 Self::issue(issuer_critical, IssueProperty::IssueWild, name, options)
101 }
102
103 /// Creates a new CAA issue record data, the tag is `iodef`
104 ///
105 /// # Arguments
106 ///
107 /// * `issuer_critical` - indicates that the corresponding property tag MUST be understood if the semantics of the CAA record are to be correctly interpreted by an issuer
108 /// * `url` - Url where issuer errors should be reported
109 pub fn new_iodef(issuer_critical: bool, url: Url) -> Self {
110 let value = url.as_str().as_bytes().to_vec();
111 Self {
112 issuer_critical,
113 reserved_flags: 0,
114 tag: "iodef".to_owned(),
115 value,
116 }
117 }
118
119 /// Parse the RData from a set of Tokens
120 ///
121 /// [RFC 6844, DNS Certification Authority Authorization, January 2013](https://tools.ietf.org/html/rfc6844#section-5.1)
122 ///
123 /// ```text
124 /// 5.1.1. Canonical Presentation Format
125 ///
126 /// The canonical presentation format of the CAA record is:
127 ///
128 /// CAA <flags> <tag> <value>
129 ///
130 /// Where:
131 ///
132 /// Flags: Is an unsigned integer between 0 and 255.
133 ///
134 /// Tag: Is a non-zero sequence of US-ASCII letters and numbers in lower
135 /// case.
136 ///
137 /// Value: Is the <character-string> encoding of the value field as
138 /// specified in [RFC1035], Section 5.1.
139 /// ```
140 pub(crate) fn from_tokens<'i, I: Iterator<Item = &'i str>>(
141 mut tokens: I,
142 ) -> Result<CAA, ParseError> {
143 let flags_str: &str = tokens
144 .next()
145 .ok_or(ParseError::Message("caa flags not present"))?;
146 let tag_str: &str = tokens
147 .next()
148 .ok_or(ParseError::Message("caa tag not present"))?;
149 let value_str: &str = tokens
150 .next()
151 .ok_or(ParseError::Message("caa value not present"))?;
152
153 // parse the flags
154 let flags = flags_str.parse::<u8>()?;
155 let issuer_critical = (flags & 0b1000_0000) != 0;
156 let reserved_flags = flags & 0b0111_1111;
157 if reserved_flags != 0 {
158 warn!("unexpected flag values in caa (0 or 128): {}", flags);
159 }
160
161 let tag = tag_str.to_owned();
162 let value = value_str.as_bytes().to_vec();
163
164 // return the new CAA record
165 Ok(CAA {
166 issuer_critical,
167 reserved_flags,
168 tag,
169 value,
170 })
171 }
172
173 /// Returns the Flags field of the resource record
174 pub fn flags(&self) -> u8 {
175 let mut flags = self.reserved_flags & 0b0111_1111;
176 if self.issuer_critical {
177 flags |= 0b1000_0000;
178 }
179 flags
180 }
181
182 /// Set the value associated with an `issue` or `issuewild` tag.
183 ///
184 /// This returns an error if the tag is not `issue` or `issuewild`.
185 pub fn set_issuer_value(
186 &mut self,
187 name: Option<&Name>,
188 key_values: &[KeyValue],
189 ) -> ProtoResult<()> {
190 if !self.tag.eq_ignore_ascii_case("issue") && !self.tag.eq_ignore_ascii_case("issuewild") {
191 return Err("CAA property tag is not 'issue' or 'issuewild'".into());
192 }
193 self.value = encode_issuer_value(name, key_values);
194 Ok(())
195 }
196
197 /// Set the value associated with an `iodef` tag.
198 ///
199 /// This returns an error if the tag is not `iodef`.
200 pub fn set_iodef_value(&mut self, url: &Url) -> ProtoResult<()> {
201 if !self.tag.eq_ignore_ascii_case("iodef") {
202 return Err("CAA property tag is not 'iodef'".into());
203 }
204 self.value = url.as_str().as_bytes().to_vec();
205 Ok(())
206 }
207
208 /// Get the value of an `issue` or `issuewild` CAA record.
209 ///
210 /// This returns an error if the record's tag is not `issue` or `issuewild`, or if the value
211 /// does not match the expected syntax.
212 pub fn value_as_issue(&self) -> ProtoResult<(Option<Name>, Vec<KeyValue>)> {
213 if !self.tag.eq_ignore_ascii_case("issue") && !self.tag.eq_ignore_ascii_case("issuewild") {
214 return Err("CAA property tag is not 'issue' or 'issuewild'".into());
215 }
216 read_issuer(&self.value)
217 }
218
219 /// Get the value of an `iodef` CAA record.
220 ///
221 /// This returns an error if the record's tag is not `iodef`, or if the value is an invalid URL.
222 pub fn value_as_iodef(&self) -> ProtoResult<Url> {
223 if !self.tag.eq_ignore_ascii_case("iodef") {
224 return Err("CAA property tag is not 'iodef'".into());
225 }
226 read_iodef(&self.value)
227 }
228}
229
230enum IssueProperty {
231 Issue,
232 IssueWild,
233}
234
235impl IssueProperty {
236 fn as_str(&self) -> &str {
237 match self {
238 Self::Issue => "issue",
239 Self::IssueWild => "issuewild",
240 }
241 }
242}
243
244fn encode_issuer_value(name: Option<&Name>, key_values: &[KeyValue]) -> Vec<u8> {
245 let mut output = Vec::new();
246
247 // output the name
248 if let Some(name) = name {
249 let name = name.to_ascii();
250 output.extend_from_slice(name.as_bytes());
251 }
252
253 // if there was no name, then we just output ';'
254 if name.is_none() && key_values.is_empty() {
255 output.push(b';');
256 return output;
257 }
258
259 for key_value in key_values {
260 output.push(b';');
261 output.push(b' ');
262 output.extend_from_slice(key_value.key.as_bytes());
263 output.push(b'=');
264 output.extend_from_slice(key_value.value.as_bytes());
265 }
266
267 output
268}
269
270enum ParseNameKeyPairState {
271 BeforeKey(Vec<KeyValue>),
272 Key {
273 first_char: bool,
274 key: String,
275 key_values: Vec<KeyValue>,
276 },
277 Value {
278 key: String,
279 value: String,
280 key_values: Vec<KeyValue>,
281 },
282}
283
284/// Reads the issuer field according to the spec
285///
286/// [RFC 8659, DNS Certification Authority Authorization, November 2019](https://www.rfc-editor.org/rfc/rfc8659),
287/// and [errata 7139](https://www.rfc-editor.org/errata/eid7139)
288///
289/// ```text
290/// 4.2. CAA issue Property
291///
292/// If the issue Property Tag is present in the Relevant RRset for an
293/// FQDN, it is a request that Issuers:
294///
295/// 1. Perform CAA issue restriction processing for the FQDN, and
296///
297/// 2. Grant authorization to issue certificates containing that FQDN to
298/// the holder of the issuer-domain-name or a party acting under the
299/// explicit authority of the holder of the issuer-domain-name.
300///
301/// The CAA issue Property Value has the following sub-syntax (specified
302/// in ABNF as per [RFC5234]).
303///
304/// issue-value = *WSP [issuer-domain-name *WSP]
305/// [";" *WSP [parameters *WSP]]
306///
307/// issuer-domain-name = label *("." label)
308/// label = (ALPHA / DIGIT) *( *("-") (ALPHA / DIGIT))
309///
310/// parameters = (parameter *WSP ";" *WSP parameters) / parameter
311/// parameter = parameter-tag *WSP "=" *WSP parameter-value
312/// parameter-tag = (ALPHA / DIGIT) *( *("-") (ALPHA / DIGIT))
313/// parameter-value = *(%x21-3A / %x3C-7E)
314///
315/// For consistency with other aspects of DNS administration, FQDN values
316/// are specified in letter-digit-hyphen Label (LDH-Label) form.
317///
318/// The following CAA RRset requests that no certificates be issued for
319/// the FQDN "certs.example.com" by any Issuer other than ca1.example.net
320/// or ca2.example.org.
321///
322/// certs.example.com CAA 0 issue "ca1.example.net"
323/// certs.example.com CAA 0 issue "ca2.example.org"
324///
325/// Because the presence of an issue Property Tag in the Relevant RRset
326/// for an FQDN restricts issuance, FQDN owners can use an issue Property
327/// Tag with no issuer-domain-name to request no issuance.
328///
329/// For example, the following RRset requests that no certificates be
330/// issued for the FQDN "nocerts.example.com" by any Issuer.
331///
332/// nocerts.example.com CAA 0 issue ";"
333///
334/// An issue Property Tag where the issue-value does not match the ABNF
335/// grammar MUST be treated the same as one specifying an empty
336/// issuer-domain-name. For example, the following malformed CAA RRset
337/// forbids issuance:
338///
339/// malformed.example.com CAA 0 issue "%%%%%"
340///
341/// CAA authorizations are additive; thus, the result of specifying both
342/// an empty issuer-domain-name and a non-empty issuer-domain-name is the
343/// same as specifying just the non-empty issuer-domain-name.
344///
345/// An Issuer MAY choose to specify parameters that further constrain the
346/// issue of certificates by that Issuer -- for example, specifying that
347/// certificates are to be subject to specific validation policies,
348/// billed to certain accounts, or issued under specific trust anchors.
349///
350/// For example, if ca1.example.net has requested that its customer
351/// account.example.com specify their account number "230123" in each of
352/// the customer's CAA records using the (CA-defined) "account"
353/// parameter, it would look like this:
354///
355/// account.example.com CAA 0 issue "ca1.example.net; account=230123"
356///
357/// The semantics of parameters to the issue Property Tag are determined
358/// by the Issuer alone.
359/// ```
360///
361/// Updated parsing rules:
362///
363/// [RFC8659 Canonical presentation form and ABNF](https://www.rfc-editor.org/rfc/rfc8659#name-canonical-presentation-form)
364///
365/// This explicitly allows `-` in property tags, diverging from the original RFC. To support this,
366/// property tags will allow `-` as non-starting characters. Additionally, this significantly
367/// relaxes the characters allowed in the value to allow URL like characters (it does not validate
368/// URL syntax).
369pub fn read_issuer(bytes: &[u8]) -> ProtoResult<(Option<Name>, Vec<KeyValue>)> {
370 let mut byte_iter = bytes.iter();
371
372 // we want to reuse the name parsing rules
373 let name: Option<Name> = {
374 let take_name = byte_iter.by_ref().take_while(|ch| char::from(**ch) != ';');
375 let name_str = take_name.cloned().collect::<Vec<u8>>();
376
377 if !name_str.is_empty() {
378 let name_str = str::from_utf8(&name_str)?;
379 Some(Name::from_ascii(name_str)?)
380 } else {
381 None
382 }
383 };
384
385 // initial state is looking for a key ';' is valid...
386 let mut state = ParseNameKeyPairState::BeforeKey(vec![]);
387
388 // run the state machine through all remaining data, collecting all parameter tag/value pairs.
389 for ch in byte_iter {
390 match state {
391 // Name was already successfully parsed, otherwise we couldn't get here.
392 ParseNameKeyPairState::BeforeKey(key_values) => {
393 match char::from(*ch) {
394 // gobble ';', ' ', and tab
395 ';' | ' ' | '\u{0009}' => state = ParseNameKeyPairState::BeforeKey(key_values),
396 ch if ch.is_ascii_alphanumeric() && ch != '=' => {
397 // We found the beginning of a new Key
398 let mut key = String::new();
399 key.push(ch);
400
401 state = ParseNameKeyPairState::Key {
402 first_char: true,
403 key,
404 key_values,
405 }
406 }
407 ch => return Err(format!("bad character in CAA issuer key: {ch}").into()),
408 }
409 }
410 ParseNameKeyPairState::Key {
411 first_char,
412 mut key,
413 key_values,
414 } => {
415 match char::from(*ch) {
416 // transition to value
417 '=' => {
418 let value = String::new();
419 state = ParseNameKeyPairState::Value {
420 key,
421 value,
422 key_values,
423 }
424 }
425 // push onto the existing key
426 ch if (ch.is_ascii_alphanumeric() || (!first_char && ch == '-'))
427 && ch != '='
428 && ch != ';' =>
429 {
430 key.push(ch);
431 state = ParseNameKeyPairState::Key {
432 first_char: false,
433 key,
434 key_values,
435 }
436 }
437 ch => return Err(format!("bad character in CAA issuer key: {ch}").into()),
438 }
439 }
440 ParseNameKeyPairState::Value {
441 key,
442 mut value,
443 mut key_values,
444 } => {
445 match char::from(*ch) {
446 // transition back to find another pair
447 ';' => {
448 key_values.push(KeyValue { key, value });
449 state = ParseNameKeyPairState::BeforeKey(key_values);
450 }
451 // If the next byte is a visible character, excluding ';', push it onto the
452 // existing value. See the ABNF production rule for `parameter-value` in the
453 // documentation above.
454 ch if ('\x21'..='\x3A').contains(&ch) || ('\x3C'..='\x7E').contains(&ch) => {
455 value.push(ch);
456
457 state = ParseNameKeyPairState::Value {
458 key,
459 value,
460 key_values,
461 }
462 }
463 ch => return Err(format!("bad character in CAA issuer value: '{ch}'").into()),
464 }
465 }
466 }
467 }
468
469 // valid final states are BeforeKey, where there was a final ';' but nothing followed it.
470 // Value, where we collected the final chars of the value, but no more data
471 let key_values = match state {
472 ParseNameKeyPairState::BeforeKey(key_values) => key_values,
473 ParseNameKeyPairState::Value {
474 key,
475 value,
476 mut key_values,
477 } => {
478 key_values.push(KeyValue { key, value });
479 key_values
480 }
481 ParseNameKeyPairState::Key { key, .. } => {
482 return Err(format!("key missing value: {key}").into());
483 }
484 };
485
486 Ok((name, key_values))
487}
488
489/// Incident Object Description Exchange Format
490///
491/// [RFC 8659, DNS Certification Authority Authorization, November 2019](https://www.rfc-editor.org/rfc/rfc8659#section-4.4)
492///
493/// ```text
494/// 4.4. CAA iodef Property
495///
496/// The iodef Property specifies a means of reporting certificate issue
497/// requests or cases of certificate issue for domains for which the
498/// Property appears in the Relevant RRset, when those requests or
499/// issuances violate the security policy of the Issuer or the FQDN
500/// holder.
501///
502/// The Incident Object Description Exchange Format (IODEF) [RFC7970] is
503/// used to present the incident report in machine-readable form.
504///
505/// The iodef Property Tag takes a URL as its Property Value. The URL
506/// scheme type determines the method used for reporting:
507///
508/// mailto: The IODEF report is reported as a MIME email attachment to
509/// an SMTP email that is submitted to the mail address specified.
510/// The mail message sent SHOULD contain a brief text message to alert
511/// the recipient to the nature of the attachment.
512///
513/// http or https: The IODEF report is submitted as a web service
514/// request to the HTTP address specified using the protocol specified
515/// in [RFC6546].
516///
517/// These are the only supported URL schemes.
518///
519/// The following RRset specifies that reports may be made by means of
520/// email with the IODEF data as an attachment, a web service [RFC6546],
521/// or both:
522///
523/// report.example.com CAA 0 issue "ca1.example.net"
524/// report.example.com CAA 0 iodef "mailto:security@example.com"
525/// report.example.com CAA 0 iodef "https://iodef.example.com/"
526/// ```
527pub fn read_iodef(url: &[u8]) -> ProtoResult<Url> {
528 let url = str::from_utf8(url)?;
529 let url = Url::parse(url)?;
530 Ok(url)
531}
532
533/// Issuer parameter key-value pairs.
534///
535/// [RFC 8659, DNS Certification Authority Authorization, November 2019](https://www.rfc-editor.org/rfc/rfc8659#section-4.2)
536/// for more explanation.
537#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
538#[derive(Debug, PartialEq, Eq, Hash, Clone)]
539pub struct KeyValue {
540 key: String,
541 value: String,
542}
543
544impl KeyValue {
545 /// Construct a new KeyValue pair
546 pub fn new<K: Into<String>, V: Into<String>>(key: K, value: V) -> Self {
547 Self {
548 key: key.into(),
549 value: value.into(),
550 }
551 }
552
553 /// Gets a reference to the key of the pair.
554 pub fn key(&self) -> &str {
555 &self.key
556 }
557
558 /// Gets a reference to the value of the pair.
559 pub fn value(&self) -> &str {
560 &self.value
561 }
562}
563
564// TODO: change this to return &str
565fn read_tag(decoder: &mut BinDecoder<'_>, len: Restrict<u8>) -> Result<String, DecodeError> {
566 let len = len
567 .map(|len| len as usize)
568 .verify_unwrap(|len| *len > 0 && *len <= 15)
569 .map_err(|_| DecodeError::CaaTagInvalid)?;
570 let mut tag = String::with_capacity(len);
571
572 for _ in 0..len {
573 let ch = decoder
574 .pop()?
575 .map(char::from)
576 .verify_unwrap(|ch| matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9'))
577 .map_err(|_| DecodeError::CaaTagInvalid)?;
578
579 tag.push(ch);
580 }
581
582 Ok(tag)
583}
584
585/// writes out the tag in binary form to the buffer, returning the number of bytes written
586fn emit_tag(buf: &mut [u8], tag: &str) -> ProtoResult<u8> {
587 let len = tag.len();
588 if len > u8::MAX as usize {
589 return Err(format!("CAA property too long: {len}").into());
590 }
591 if buf.len() < len {
592 return Err(format!(
593 "insufficient capacity in CAA buffer: {} for tag: {}",
594 buf.len(),
595 len
596 )
597 .into());
598 }
599
600 // copy into the buffer
601 let buf = &mut buf[0..len];
602 buf.copy_from_slice(tag.as_bytes());
603
604 Ok(len as u8)
605}
606
607impl BinEncodable for CAA {
608 fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> {
609 let mut encoder = encoder.with_rdata_behavior(RDataEncoding::Other);
610 encoder.emit(self.flags())?;
611 // TODO: it might be interesting to use the new place semantics here to output all the data, then place the length back to the beginning...
612 let mut tag_buf = [0_u8; u8::MAX as usize];
613 let len = emit_tag(&mut tag_buf, &self.tag)?;
614
615 // now write to the encoder
616 encoder.emit(len)?;
617 encoder.emit_vec(&tag_buf[0..len as usize])?;
618 encoder.emit_vec(&self.value)?;
619
620 Ok(())
621 }
622}
623
624impl<'r> RecordDataDecodable<'r> for CAA {
625 /// Read the binary CAA format
626 ///
627 /// [RFC 8659, DNS Certification Authority Authorization, November 2019](https://www.rfc-editor.org/rfc/rfc8659#section-4.1)
628 ///
629 /// ```text
630 /// 4.1. Syntax
631 ///
632 /// A CAA RR contains a single Property consisting of a tag-value pair.
633 /// An FQDN MAY have multiple CAA RRs associated with it, and a given
634 /// Property Tag MAY be specified more than once across those RRs.
635 ///
636 /// The RDATA section for a CAA RR contains one Property. A Property
637 /// consists of the following:
638 ///
639 /// +0-1-2-3-4-5-6-7-|0-1-2-3-4-5-6-7-|
640 /// | Flags | Tag Length = n |
641 /// +----------------|----------------+...+---------------+
642 /// | Tag char 0 | Tag char 1 |...| Tag char n-1 |
643 /// +----------------|----------------+...+---------------+
644 /// +----------------|----------------+.....+----------------+
645 /// | Value byte 0 | Value byte 1 |.....| Value byte m-1 |
646 /// +----------------|----------------+.....+----------------+
647 ///
648 /// Where n is the length specified in the Tag Length field and m is the
649 /// number of remaining octets in the Value field. They are related by
650 /// (m = d - n - 2) where d is the length of the RDATA section.
651 ///
652 /// The fields are defined as follows:
653 ///
654 /// Flags: One octet containing the following field:
655 ///
656 /// Bit 0, Issuer Critical Flag: If the value is set to "1", the
657 /// Property is critical. A CA MUST NOT issue certificates for any
658 /// FQDN if the Relevant RRset for that FQDN contains a CAA
659 /// critical Property for an unknown or unsupported Property Tag.
660 ///
661 /// Note that according to the conventions set out in [RFC1035], bit 0 is
662 /// the Most Significant Bit and bit 7 is the Least Significant Bit.
663 /// Thus, according to those conventions, the Flags value 1 means that
664 /// bit 7 is set, while a value of 128 means that bit 0 is set.
665 ///
666 /// All other bit positions are reserved for future use.
667 ///
668 /// To ensure compatibility with future extensions to CAA, DNS records
669 /// compliant with this version of the CAA specification MUST clear (set
670 /// to "0") all reserved flag bits. Applications that interpret CAA
671 /// records MUST ignore the value of all reserved flag bits.
672 ///
673 /// Tag Length: A single octet containing an unsigned integer specifying
674 /// the tag length in octets. The tag length MUST be at least 1.
675 ///
676 /// Tag: The Property identifier -- a sequence of ASCII characters.
677 ///
678 /// Tags MAY contain ASCII characters "a" through "z", "A" through "Z",
679 /// and the numbers 0 through 9. Tags MUST NOT contain any other
680 /// characters. Matching of tags is case insensitive.
681 ///
682 /// Tags submitted for registration by IANA MUST NOT contain any
683 /// characters other than the (lowercase) ASCII characters "a" through
684 /// "z" and the numbers 0 through 9.
685 ///
686 /// Value: A sequence of octets representing the Property Value.
687 /// Property Values are encoded as binary values and MAY employ
688 /// sub-formats.
689 ///
690 /// The length of the Value field is specified implicitly as the
691 /// remaining length of the enclosing RDATA section.
692 /// ```
693 fn read_data(decoder: &mut BinDecoder<'r>, length: Restrict<u16>) -> Result<Self, DecodeError> {
694 let flags = decoder.read_u8()?.unverified(/*used as bitfield*/);
695
696 let issuer_critical = (flags & 0b1000_0000) != 0;
697 let reserved_flags = flags & 0b0111_1111;
698
699 let tag_len = decoder.read_u8()?;
700 let value_len = length
701 .checked_sub(u16::from(tag_len.unverified(/*safe usage here*/)))
702 .checked_sub(2)
703 .map_err(|len| DecodeError::IncorrectRDataLengthRead {
704 read: len as usize,
705 len: u16::from(tag_len.unverified(/*safe usage here*/)) as usize + 2,
706 })?
707 .unverified(/* used only as length safely */);
708
709 let tag = read_tag(decoder, tag_len)?;
710
711 let value =
712 decoder.read_vec(value_len as usize)?.unverified(/* stored as uninterpreted data */);
713
714 Ok(CAA {
715 issuer_critical,
716 reserved_flags,
717 tag,
718 value,
719 })
720 }
721}
722
723impl RecordData for CAA {
724 fn try_borrow(data: &RData) -> Option<&Self> {
725 match data {
726 RData::CAA(csync) => Some(csync),
727 _ => None,
728 }
729 }
730
731 fn record_type(&self) -> RecordType {
732 RecordType::CAA
733 }
734
735 fn into_rdata(self) -> RData {
736 RData::CAA(self)
737 }
738}
739
740// FIXME: this needs to be verified to be correct, add tests...
741impl fmt::Display for CAA {
742 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
743 write!(
744 f,
745 "{flags} {tag} \"{value}\"",
746 flags = self.flags(),
747 tag = &self.tag,
748 value = String::from_utf8_lossy(&self.value)
749 )
750 }
751}
752
753#[cfg(test)]
754mod tests {
755 #![allow(clippy::dbg_macro, clippy::print_stdout)]
756
757 use alloc::{str, string::ToString};
758 #[cfg(feature = "std")]
759 use std::println;
760
761 use super::*;
762
763 #[test]
764 fn test_read_tag() {
765 let ok_under15 = b"abcxyzABCXYZ019";
766 let mut decoder = BinDecoder::new(ok_under15);
767
768 let read = read_tag(&mut decoder, Restrict::new(ok_under15.len() as u8))
769 .expect("failed to read tag");
770
771 assert_eq!(str::from_utf8(ok_under15).unwrap(), read);
772 }
773
774 #[test]
775 fn test_bad_tag() {
776 let bad_under15 = b"-";
777 let mut decoder = BinDecoder::new(bad_under15);
778
779 assert!(read_tag(&mut decoder, Restrict::new(bad_under15.len() as u8)).is_err());
780 }
781
782 #[test]
783 fn test_too_short_tag() {
784 let too_short = b"";
785 let mut decoder = BinDecoder::new(too_short);
786
787 assert!(read_tag(&mut decoder, Restrict::new(too_short.len() as u8)).is_err());
788 }
789
790 #[test]
791 fn test_too_long_tag() {
792 let too_long = b"0123456789abcdef";
793 let mut decoder = BinDecoder::new(too_long);
794
795 assert!(read_tag(&mut decoder, Restrict::new(too_long.len() as u8)).is_err());
796 }
797
798 #[test]
799 fn test_read_issuer() {
800 // (Option<Name>, Vec<KeyValue>)
801 assert_eq!(
802 read_issuer(b"ca.example.net; account=230123").unwrap(),
803 (
804 Some(Name::parse("ca.example.net", None).unwrap()),
805 vec![KeyValue {
806 key: "account".to_string(),
807 value: "230123".to_string(),
808 }],
809 )
810 );
811
812 assert_eq!(
813 read_issuer(b"ca.example.net").unwrap(),
814 (Some(Name::parse("ca.example.net", None,).unwrap(),), vec![],)
815 );
816 assert_eq!(
817 read_issuer(b"ca.example.net; policy=ev").unwrap(),
818 (
819 Some(Name::parse("ca.example.net", None).unwrap(),),
820 vec![KeyValue {
821 key: "policy".to_string(),
822 value: "ev".to_string(),
823 }],
824 )
825 );
826 assert_eq!(
827 read_issuer(b"ca.example.net; account=230123; policy=ev").unwrap(),
828 (
829 Some(Name::parse("ca.example.net", None).unwrap(),),
830 vec![
831 KeyValue {
832 key: "account".to_string(),
833 value: "230123".to_string(),
834 },
835 KeyValue {
836 key: "policy".to_string(),
837 value: "ev".to_string(),
838 },
839 ],
840 )
841 );
842 assert_eq!(
843 read_issuer(b"example.net; account-uri=https://example.net/account/1234; validation-methods=dns-01").unwrap(),
844 (
845 Some(Name::parse("example.net", None).unwrap(),),
846 vec![
847 KeyValue {
848 key: "account-uri".to_string(),
849 value: "https://example.net/account/1234".to_string(),
850 },
851 KeyValue {
852 key: "validation-methods".to_string(),
853 value: "dns-01".to_string(),
854 },
855 ],
856 )
857 );
858 assert_eq!(read_issuer(b";").unwrap(), (None, vec![]));
859 read_issuer(b"example.com; param=\xff").unwrap_err();
860 }
861
862 #[test]
863 fn test_read_iodef() {
864 assert_eq!(
865 read_iodef(b"mailto:security@example.com").unwrap(),
866 Url::parse("mailto:security@example.com").unwrap()
867 );
868 assert_eq!(
869 read_iodef(b"https://iodef.example.com/").unwrap(),
870 Url::parse("https://iodef.example.com/").unwrap()
871 );
872 }
873
874 fn test_encode_decode(rdata: CAA) {
875 let mut bytes = Vec::new();
876 let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut bytes);
877 rdata.emit(&mut encoder).expect("failed to emit caa");
878 let bytes = encoder.into_bytes();
879
880 #[cfg(feature = "std")]
881 println!("bytes: {bytes:?}");
882
883 let mut decoder: BinDecoder<'_> = BinDecoder::new(bytes);
884 let read_rdata = CAA::read_data(&mut decoder, Restrict::new(bytes.len() as u16))
885 .expect("failed to read back");
886 assert_eq!(rdata, read_rdata);
887 }
888
889 #[test]
890 fn test_encode_decode_issue() {
891 test_encode_decode(CAA::new_issue(true, None, vec![]));
892 test_encode_decode(CAA::new_issue(
893 true,
894 Some(Name::parse("example.com", None).unwrap()),
895 vec![],
896 ));
897 test_encode_decode(CAA::new_issue(
898 true,
899 Some(Name::parse("example.com", None).unwrap()),
900 vec![KeyValue::new("key", "value")],
901 ));
902 // technically the this parser supports this case, though it's not clear it's something the spec allows for
903 test_encode_decode(CAA::new_issue(
904 true,
905 None,
906 vec![KeyValue::new("key", "value")],
907 ));
908 // test fqdn
909 test_encode_decode(CAA::new_issue(
910 true,
911 Some(Name::parse("example.com.", None).unwrap()),
912 vec![],
913 ));
914 // invalid name
915 test_encode_decode(CAA {
916 issuer_critical: false,
917 reserved_flags: 0,
918 tag: "issue".to_string(),
919 value: b"%%%%%".to_vec(),
920 });
921 }
922
923 #[test]
924 fn test_encode_decode_issuewild() {
925 test_encode_decode(CAA::new_issuewild(false, None, vec![]));
926 // other variants handled in test_encode_decode_issue
927 }
928
929 #[test]
930 fn test_encode_decode_iodef() {
931 test_encode_decode(CAA::new_iodef(
932 true,
933 Url::parse("https://www.example.com").unwrap(),
934 ));
935 test_encode_decode(CAA::new_iodef(
936 false,
937 Url::parse("mailto:root@example.com").unwrap(),
938 ));
939 // invalid UTF-8
940 test_encode_decode(CAA {
941 issuer_critical: false,
942 reserved_flags: 0,
943 tag: "iodef".to_string(),
944 value: vec![0xff],
945 });
946 }
947
948 #[test]
949 fn test_encode_decode_unknown() {
950 test_encode_decode(CAA {
951 issuer_critical: true,
952 reserved_flags: 0,
953 tag: "tbs".to_string(),
954 value: b"Unknown".to_vec(),
955 });
956 }
957
958 fn test_encode(rdata: CAA, encoded: &[u8]) {
959 let mut bytes = Vec::new();
960 let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut bytes);
961 rdata.emit(&mut encoder).expect("failed to emit caa");
962 let bytes = encoder.into_bytes();
963 assert_eq!(bytes as &[u8], encoded);
964 }
965
966 #[test]
967 fn test_encode_non_fqdn() {
968 let name_bytes: &[u8] = b"issueexample.com";
969 let header: &[u8] = &[128, 5];
970 let encoded: Vec<u8> = header.iter().chain(name_bytes.iter()).cloned().collect();
971
972 test_encode(
973 CAA::new_issue(
974 true,
975 Some(Name::parse("example.com", None).unwrap()),
976 vec![],
977 ),
978 &encoded,
979 );
980 }
981
982 #[test]
983 fn test_encode_fqdn() {
984 let name_bytes: &[u8] = b"issueexample.com.";
985 let header: [u8; 2] = [128, 5];
986 let encoded: Vec<u8> = header.iter().chain(name_bytes.iter()).cloned().collect();
987
988 test_encode(
989 CAA::new_issue(
990 true,
991 Some(Name::parse("example.com.", None).unwrap()),
992 vec![],
993 ),
994 &encoded,
995 );
996 }
997
998 #[test]
999 fn test_to_string() {
1000 let deny = CAA::new_issue(false, None, vec![]);
1001 assert_eq!(deny.to_string(), "0 issue \";\"");
1002
1003 let empty_options = CAA::new_issue(
1004 false,
1005 Some(Name::parse("example.com", None).unwrap()),
1006 vec![],
1007 );
1008 assert_eq!(empty_options.to_string(), "0 issue \"example.com\"");
1009
1010 let one_option = CAA::new_issue(
1011 false,
1012 Some(Name::parse("example.com", None).unwrap()),
1013 vec![KeyValue::new("one", "1")],
1014 );
1015 assert_eq!(one_option.to_string(), "0 issue \"example.com; one=1\"");
1016
1017 let two_options = CAA::new_issue(
1018 false,
1019 Some(Name::parse("example.com", None).unwrap()),
1020 vec![KeyValue::new("one", "1"), KeyValue::new("two", "2")],
1021 );
1022 assert_eq!(
1023 two_options.to_string(),
1024 "0 issue \"example.com; one=1; two=2\""
1025 );
1026
1027 let flag_set = CAA::new_issue(
1028 true,
1029 Some(Name::parse("example.com", None).unwrap()),
1030 vec![KeyValue::new("one", "1"), KeyValue::new("two", "2")],
1031 );
1032 assert_eq!(
1033 flag_set.to_string(),
1034 "128 issue \"example.com; one=1; two=2\""
1035 );
1036
1037 let empty_domain = CAA::new_issue(
1038 false,
1039 None,
1040 vec![KeyValue::new("one", "1"), KeyValue::new("two", "2")],
1041 );
1042 assert_eq!(empty_domain.to_string(), "0 issue \"; one=1; two=2\"");
1043
1044 // Examples from RFC 6844, with added quotes
1045 assert_eq!(
1046 CAA::new_issue(
1047 false,
1048 Some(Name::parse("ca.example.net", None).unwrap()),
1049 vec![KeyValue::new("account", "230123")]
1050 )
1051 .to_string(),
1052 "0 issue \"ca.example.net; account=230123\""
1053 );
1054 assert_eq!(
1055 CAA::new_issue(
1056 false,
1057 Some(Name::parse("ca.example.net", None).unwrap()),
1058 vec![KeyValue::new("policy", "ev")]
1059 )
1060 .to_string(),
1061 "0 issue \"ca.example.net; policy=ev\""
1062 );
1063 assert_eq!(
1064 CAA::new_iodef(false, Url::parse("mailto:security@example.com").unwrap()).to_string(),
1065 "0 iodef \"mailto:security@example.com\""
1066 );
1067 assert_eq!(
1068 CAA::new_iodef(false, Url::parse("https://iodef.example.com/").unwrap()).to_string(),
1069 "0 iodef \"https://iodef.example.com/\""
1070 );
1071 let unknown = CAA {
1072 issuer_critical: true,
1073 reserved_flags: 0,
1074 tag: "tbs".to_string(),
1075 value: b"Unknown".to_vec(),
1076 };
1077 assert_eq!(unknown.to_string(), "128 tbs \"Unknown\"");
1078 }
1079
1080 #[test]
1081 fn test_unicode_kv() {
1082 const MESSAGE: &[u8] = &[
1083 32, 5, 105, 115, 115, 117, 101, 103, 103, 103, 102, 71, 46, 110, 110, 115, 115, 117,
1084 48, 110, 45, 59, 32, 32, 255, 61, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
1085 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
1086 ];
1087
1088 let mut decoder = BinDecoder::new(MESSAGE);
1089 let caa = CAA::read_data(&mut decoder, Restrict::new(MESSAGE.len() as u16)).unwrap();
1090 assert!(!caa.issuer_critical);
1091 assert_eq!(caa.tag, "issue");
1092 match (caa.value_as_issue(), caa.value_as_iodef()) {
1093 (Err(_), Err(_)) => {}
1094 _ => panic!("wrong value type"),
1095 }
1096 assert_eq!(caa.value, &MESSAGE[7..]);
1097 }
1098
1099 #[test]
1100 fn test_name_non_ascii_character_escaped_dots_roundtrip() {
1101 const MESSAGE: &[u8] = b"\x00\x05issue\xe5\x85\x9edomain\\.\\.name";
1102 let caa = CAA::read_data(
1103 &mut BinDecoder::new(MESSAGE),
1104 Restrict::new(u16::try_from(MESSAGE.len()).unwrap()),
1105 )
1106 .unwrap();
1107
1108 let mut encoded = Vec::new();
1109 caa.emit(&mut BinEncoder::new(&mut encoded)).unwrap();
1110
1111 let caa_round_trip = CAA::read_data(
1112 &mut BinDecoder::new(&encoded),
1113 Restrict::new(u16::try_from(encoded.len()).unwrap()),
1114 )
1115 .unwrap();
1116
1117 assert_eq!(caa, caa_round_trip);
1118 }
1119
1120 #[test]
1121 fn test_reserved_flags_round_trip() {
1122 let mut original = *b"\x00\x05issueexample.com";
1123 for flags in 0..=u8::MAX {
1124 original[0] = flags;
1125 let caa = CAA::read_data(
1126 &mut BinDecoder::new(&original),
1127 Restrict::new(u16::try_from(original.len()).unwrap()),
1128 )
1129 .unwrap();
1130
1131 let mut encoded = Vec::new();
1132 caa.emit(&mut BinEncoder::new(&mut encoded)).unwrap();
1133 assert_eq!(original.as_slice(), &encoded);
1134 }
1135 }
1136
1137 #[test]
1138 fn test_parsing() {
1139 //nocerts CAA 0 issue \";\"
1140 assert!(CAA::from_tokens(vec!["0", "issue", ";"].into_iter()).is_ok());
1141
1142 // certs CAA 0 issuewild \"example.net\"
1143 assert!(CAA::from_tokens(vec!["0", "issue", "example.net"].into_iter()).is_ok());
1144
1145 // issuer critical = true
1146 test_to_string_parse_is_reversible(CAA::new_issue(true, None, vec![]), "128 issue \";\"");
1147
1148 // deny
1149 test_to_string_parse_is_reversible(CAA::new_issue(false, None, vec![]), "0 issue \";\"");
1150
1151 // only hostname
1152 test_to_string_parse_is_reversible(
1153 CAA::new_issue(
1154 false,
1155 Some(Name::parse("example.com", None).unwrap()),
1156 vec![],
1157 ),
1158 "0 issue \"example.com\"",
1159 );
1160
1161 // hostname and one parameter
1162 test_to_string_parse_is_reversible(
1163 CAA::new_issue(
1164 false,
1165 Some(Name::parse("example.com", None).unwrap()),
1166 vec![KeyValue::new("one", "1")],
1167 ),
1168 "0 issue \"example.com; one=1\"",
1169 );
1170
1171 // hostname and two parameters
1172 test_to_string_parse_is_reversible(
1173 CAA::new_issue(
1174 false,
1175 Some(Name::parse("example.com", None).unwrap()),
1176 vec![KeyValue::new("one", "1"), KeyValue::new("two", "2")],
1177 ),
1178 "0 issue \"example.com; one=1; two=2\"",
1179 );
1180
1181 // no hostname and one parameter
1182 test_to_string_parse_is_reversible(
1183 CAA::new_issue(false, None, vec![KeyValue::new("one", "1")]),
1184 "0 issue \"; one=1\"",
1185 );
1186
1187 // no hostname and two parameters
1188 test_to_string_parse_is_reversible(
1189 CAA::new_issue(
1190 false,
1191 None,
1192 vec![KeyValue::new("one", "1"), KeyValue::new("two", "2")],
1193 ),
1194 "0 issue \"; one=1; two=2\"",
1195 );
1196 }
1197
1198 fn test_to_string_parse_is_reversible(expected_rdata: CAA, input_string: &str) {
1199 let expected_rdata_string = expected_rdata.to_string();
1200 assert_eq!(
1201 input_string, expected_rdata_string,
1202 "input string does not match expected_rdata.to_string()"
1203 );
1204
1205 match RData::try_from_str(RecordType::CAA, input_string).expect("CAA rdata parse failed") {
1206 RData::CAA(parsed_rdata) => assert_eq!(
1207 expected_rdata, parsed_rdata,
1208 "CAA rdata was not parsed as expected. input={input_string:?} expected_rdata={expected_rdata:?} parsed_rdata={parsed_rdata:?}",
1209 ),
1210 parsed_rdata => panic!("Parsed RData is not CAA: {:?}", parsed_rdata),
1211 }
1212 }
1213}