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