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