toluol_proto/rdata/
caa.rs

1//! `CAA` RDATA definition.
2
3use std::fmt::Display;
4use std::io::{Read, Write};
5
6use byteorder::{ReadBytesExt, WriteBytesExt};
7use url::Url;
8
9use crate::error::{EncodeError, ParseError};
10use crate::name::Name;
11
12use super::{encode_string_into, Rdata, RdataTrait};
13
14#[cfg(feature = "serde")]
15use serde::Serialize;
16
17/// The type of [`Value`] stored in a [`CAA`] record.
18/// [\[RFC 6844\]](https://www.rfc-editor.org/rfc/rfc6844)
19#[cfg_attr(feature = "serde", derive(Serialize))]
20#[derive(PartialEq, Eq, Clone, Debug)]
21pub enum Property {
22    /// The issue property entry authorizes the holder of the domain name stored in [`CAA`]'s value
23    /// or a party acting under the explicit authority of the holder of that domain name to issue
24    /// certificates for the domain in which the property is published.
25    Issue,
26    /// The issuewild property entry authorizes the holder of the domain name stored in [`CAA`]'s
27    /// value or a party acting under the explicit authority of the holder of that domain name to
28    /// issue wildcard certificates for the domain in which the property is published.
29    IssueWild,
30    /// [`CAA`]'s value specifies a URL to which an issuer MAY report certificate issue requests
31    /// that are inconsistent with the issuer's Certification Practices or Certificate Policy, or
32    /// that a Certificate Evaluator may use to report observation of a possible policy violation.
33    /// The Incident Object Description Exchange Format (IODEF) format is used (see
34    /// [RFC 5070](https://www.rfc-editor.org/rfc/rfc5070)).
35    Iodef,
36    Unknown(String),
37}
38
39/// The value stored in a [`CAA`] record.
40#[cfg_attr(feature = "serde", derive(Serialize))]
41#[derive(PartialEq, Eq, Clone, Debug)]
42pub enum Value {
43    /// See [`Property::Issue`] and [`Property::IssueWild`].
44    Issuer {
45        /// If [`None`], indicates that no certificates are to be issued for the domain in question.
46        name: Option<Name>,
47        parameters: Vec<(String, String)>,
48    },
49    /// See [`Property::Iodef`].
50    IodefUrl(Url),
51    /// For [`Property::Unknown`].
52    Unknown(String),
53}
54
55/// This record allows a DNS domain name holder to specify one or more Certification Authorities
56/// (CAs) authorized to issue certificates for that domain. CAA Resource Records allow a public
57/// Certification Authority to implement additional controls to reduce the risk of unintended
58/// certificate mis-issue. [\[RFC 6844\]](https://www.rfc-editor.org/rfc/rfc6844)
59#[cfg_attr(feature = "serde", derive(Serialize))]
60#[derive(PartialEq, Eq, Clone, Debug)]
61pub struct CAA {
62    /// If true, indicates that the corresponding property tag MUST be understood if the semantics
63    /// of the `CAA` record are to be correctly interpreted by an issuer.
64    ///
65    /// Issuers MUST NOT issue certificates for a domain if the relevant CAA Resource Record set
66    /// contains unknown property tags that have this set to true.
67    pub issuer_critical: bool,
68    /// The type of [`Self::value`] stored in this record.
69    ///
70    /// This is private to prevent constructing invalid `CAA` records (mismatch between tag and
71    /// value).
72    tag: Property,
73    /// The value stored in this record, as defined by [`Self::tag`].
74    ///
75    /// This is private to prevent constructing invalid `CAA` records (mismatch between tag and
76    /// value).
77    value: Value,
78}
79
80impl Property {
81    /// Encodes the `Property` into the given `buf`, preceded by the tag length. No spaces are
82    /// written to `buf`.
83    ///
84    /// Returns the number of bytes written on success.
85    pub(crate) fn encode_into(&self, buf: &mut impl Write) -> Result<u16, EncodeError> {
86        encode_string_into(self.to_string(), buf)
87    }
88}
89
90impl Display for Property {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        match self {
93            Self::Iodef => write!(f, "iodef"),
94            Self::Issue => write!(f, "issue"),
95            Self::IssueWild => write!(f, "iodef"),
96            Self::Unknown(unknown) => write!(f, "{}", unknown),
97        }
98    }
99}
100
101impl Value {
102    /// Encodes the `Value` into the given `buf`, preceded by the value length. No spaces are
103    /// written to `buf`.
104    ///
105    /// Returns the number of bytes written on success.
106    pub(crate) fn encode_into(&self, buf: &mut impl Write) -> Result<u16, EncodeError> {
107        encode_string_into(self.to_string(), buf)
108    }
109}
110
111impl Display for Value {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        match self {
114            Self::Issuer { name, parameters } => {
115                if parameters.is_empty() {
116                    if let Some(name) = name {
117                        write!(f, "{}", name)
118                    } else {
119                        write!(f, ";")
120                    }
121                } else {
122                    let parameters = parameters
123                        .iter()
124                        .map(|(tag, value)| format!("{}={}", tag, value))
125                        .collect::<Vec<_>>()
126                        .join(" ");
127                    let name = name.as_ref().map(|n| n.to_string()).unwrap_or_default();
128                    write!(f, "{}; {}", name, parameters)
129                }
130            }
131            Self::IodefUrl(url) => write!(f, "{}", url),
132
133            Self::Unknown(unknown) => write!(f, "{}", unknown),
134        }
135    }
136}
137
138impl From<&str> for Property {
139    fn from(value: &str) -> Self {
140        match value.to_ascii_lowercase().as_str() {
141            "issue" => Self::Issue,
142            "issuewild" => Self::IssueWild,
143            "iodef" => Self::Iodef,
144            _ => Self::Unknown(value.to_string()),
145        }
146    }
147}
148
149impl CAA {
150    /// Creates a new `CAA` record with tag [`Property::Issue`].
151    pub fn issue(
152        issuer_critical: bool,
153        name: Option<Name>,
154        parameters: Vec<(String, String)>,
155    ) -> Self {
156        Self {
157            issuer_critical,
158            tag: Property::Issue,
159            value: Value::Issuer { name, parameters },
160        }
161    }
162
163    /// Creates a new `CAA` record with tag [`Property::IssueWild`].
164    pub fn issue_wild(
165        issuer_critical: bool,
166        name: Option<Name>,
167        parameters: Vec<(String, String)>,
168    ) -> Self {
169        Self {
170            issuer_critical,
171            tag: Property::IssueWild,
172            value: Value::Issuer { name, parameters },
173        }
174    }
175
176    /// Creates a new `CAA` record with tag [`Property::Iodef`].
177    pub fn iodef(issuer_critical: bool, url: Url) -> Self {
178        Self {
179            issuer_critical,
180            tag: Property::Iodef,
181            value: Value::IodefUrl(url),
182        }
183    }
184
185    /// The type of [`Self::value()`] stored in this record.
186    pub fn tag(&self) -> &Property {
187        &self.tag
188    }
189
190    /// The value stored in this record, as defined by [`Self::tag()`].
191    pub fn value(&self) -> &Value {
192        &self.value
193    }
194}
195
196impl RdataTrait for CAA {
197    fn parse_rdata(rdata: &mut std::io::Cursor<&[u8]>, rdlength: u16) -> Result<Rdata, ParseError> {
198        let flags = rdata.read_u8()?;
199        let issuer_critical = (flags & (1 << 7)) != 0;
200        let tag_length = rdata.read_u8()?;
201        let mut tag = vec![0; tag_length as usize];
202        rdata.read_exact(&mut tag)?;
203        // we already read: u8 (1) + u8 (1) + tag_length = 2 + tag_length bytes
204        let bytes_read = 2 + tag_length;
205        let value_length = rdlength - bytes_read as u16;
206        let mut value = vec![0; value_length as usize];
207        rdata.read_exact(&mut value)?;
208
209        let tag = String::from_utf8_lossy(&tag);
210        if !tag.is_ascii() {
211            return Err(ParseError::NonAsciiCaa(tag.into_owned()));
212        }
213        let value_cow = String::from_utf8_lossy(&value);
214        let tag = Property::from(&*tag);
215        let caa = match &tag {
216            Property::Unknown(_) => Self {
217                issuer_critical,
218                tag,
219                value: Value::Unknown(value_cow.into_owned()),
220            },
221            Property::Iodef => {
222                let url = Url::parse(&value_cow)?;
223                Self {
224                    issuer_critical,
225                    tag,
226                    value: Value::IodefUrl(url),
227                }
228            }
229            Property::Issue | Property::IssueWild => {
230                let value = value_cow.trim();
231                // check if we have issue/issuewild tag first
232                let (name, parameters) = if let Some((name, parameters)) = value.split_once(';') {
233                    let name = name.trim();
234                    let name = if name.is_empty() {
235                        None
236                    } else {
237                        Some(
238                            Name::from_ascii(name)
239                                .map_err(|_| ParseError::InvalidCaaIssueName(name.to_string()))?,
240                        )
241                    };
242                    let parameters = parameters.trim();
243                    let tag_values: Result<Vec<_>, _> = parameters
244                        .split(&[' ', '\t'])
245                        // may be separated by multiple spaces/tabs
246                        .filter(|s| !s.is_empty())
247                        .map(|tag_value| {
248                            tag_value.split_once('=').ok_or_else(|| {
249                                ParseError::InvalidCaaParameter(parameters.to_string())
250                            })
251                        })
252                        .collect();
253                    let tag_values: Vec<_> = tag_values?
254                        .iter()
255                        .map(|(tag, value)| (tag.to_string(), value.to_string()))
256                        .collect();
257                    (name, tag_values)
258                } else {
259                    let name = Name::from_ascii(value)
260                        .map_err(|_| ParseError::InvalidCaaIssueName(value_cow.into_owned()))?;
261                    (Some(name), vec![])
262                };
263                Self {
264                    issuer_critical,
265                    tag,
266                    value: Value::Issuer { name, parameters },
267                }
268            }
269        };
270
271        Ok(Rdata::CAA(caa))
272    }
273
274    fn encode_rdata_into(&self, buf: &mut impl Write) -> Result<u16, EncodeError> {
275        let flags = if self.issuer_critical { 1 << 7 } else { 0 };
276        buf.write_u8(flags)?;
277        let tag_byte_count = self.tag.encode_into(buf)?;
278        let value_byte_count = self.value.encode_into(buf)?;
279
280        Ok(1 + tag_byte_count + value_byte_count)
281    }
282}
283
284impl Display for CAA {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        if self.issuer_critical {
287            write!(f, "1 ")?;
288        } else {
289            write!(f, "0 ")?;
290        }
291        write!(f, "{} \"{}\"", self.tag, self.value)
292    }
293}