#![allow(clippy::use_self)]
use alloc::{borrow::ToOwned, string::String, vec::Vec};
use core::{fmt, str};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use tracing::warn;
use url::Url;
use crate::{
error::ProtoResult,
rr::{RData, RecordData, RecordDataDecodable, RecordType, domain::Name},
serialize::{binary::*, txt::ParseError},
};
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
#[non_exhaustive]
pub struct CAA {
pub issuer_critical: bool,
pub reserved_flags: u8,
pub tag: String,
pub value: Vec<u8>,
}
impl CAA {
fn issue(
issuer_critical: bool,
tag: IssueProperty,
name: Option<Name>,
options: Vec<KeyValue>,
) -> Self {
let tag = tag.as_str().to_owned();
let value = encode_issuer_value(name.as_ref(), &options);
Self {
issuer_critical,
reserved_flags: 0,
tag,
value,
}
}
pub fn new_issue(issuer_critical: bool, name: Option<Name>, options: Vec<KeyValue>) -> Self {
Self::issue(issuer_critical, IssueProperty::Issue, name, options)
}
pub fn new_issuewild(
issuer_critical: bool,
name: Option<Name>,
options: Vec<KeyValue>,
) -> Self {
Self::issue(issuer_critical, IssueProperty::IssueWild, name, options)
}
pub fn new_iodef(issuer_critical: bool, url: Url) -> Self {
let value = url.as_str().as_bytes().to_vec();
Self {
issuer_critical,
reserved_flags: 0,
tag: "iodef".to_owned(),
value,
}
}
pub(crate) fn from_tokens<'i, I: Iterator<Item = &'i str>>(
mut tokens: I,
) -> Result<CAA, ParseError> {
let flags_str: &str = tokens
.next()
.ok_or(ParseError::Message("caa flags not present"))?;
let tag_str: &str = tokens
.next()
.ok_or(ParseError::Message("caa tag not present"))?;
let value_str: &str = tokens
.next()
.ok_or(ParseError::Message("caa value not present"))?;
let flags = flags_str.parse::<u8>()?;
let issuer_critical = (flags & 0b1000_0000) != 0;
let reserved_flags = flags & 0b0111_1111;
if reserved_flags != 0 {
warn!("unexpected flag values in caa (0 or 128): {}", flags);
}
let tag = tag_str.to_owned();
let value = value_str.as_bytes().to_vec();
Ok(CAA {
issuer_critical,
reserved_flags,
tag,
value,
})
}
pub fn flags(&self) -> u8 {
let mut flags = self.reserved_flags & 0b0111_1111;
if self.issuer_critical {
flags |= 0b1000_0000;
}
flags
}
pub fn set_issuer_value(
&mut self,
name: Option<&Name>,
key_values: &[KeyValue],
) -> ProtoResult<()> {
if !self.tag.eq_ignore_ascii_case("issue") && !self.tag.eq_ignore_ascii_case("issuewild") {
return Err("CAA property tag is not 'issue' or 'issuewild'".into());
}
self.value = encode_issuer_value(name, key_values);
Ok(())
}
pub fn set_iodef_value(&mut self, url: &Url) -> ProtoResult<()> {
if !self.tag.eq_ignore_ascii_case("iodef") {
return Err("CAA property tag is not 'iodef'".into());
}
self.value = url.as_str().as_bytes().to_vec();
Ok(())
}
pub fn value_as_issue(&self) -> ProtoResult<(Option<Name>, Vec<KeyValue>)> {
if !self.tag.eq_ignore_ascii_case("issue") && !self.tag.eq_ignore_ascii_case("issuewild") {
return Err("CAA property tag is not 'issue' or 'issuewild'".into());
}
read_issuer(&self.value)
}
pub fn value_as_iodef(&self) -> ProtoResult<Url> {
if !self.tag.eq_ignore_ascii_case("iodef") {
return Err("CAA property tag is not 'iodef'".into());
}
read_iodef(&self.value)
}
}
enum IssueProperty {
Issue,
IssueWild,
}
impl IssueProperty {
fn as_str(&self) -> &str {
match self {
Self::Issue => "issue",
Self::IssueWild => "issuewild",
}
}
}
fn encode_issuer_value(name: Option<&Name>, key_values: &[KeyValue]) -> Vec<u8> {
let mut output = Vec::new();
if let Some(name) = name {
let name = name.to_ascii();
output.extend_from_slice(name.as_bytes());
}
if name.is_none() && key_values.is_empty() {
output.push(b';');
return output;
}
for key_value in key_values {
output.push(b';');
output.push(b' ');
output.extend_from_slice(key_value.key.as_bytes());
output.push(b'=');
output.extend_from_slice(key_value.value.as_bytes());
}
output
}
enum ParseNameKeyPairState {
BeforeKey(Vec<KeyValue>),
Key {
first_char: bool,
key: String,
key_values: Vec<KeyValue>,
},
Value {
key: String,
value: String,
key_values: Vec<KeyValue>,
},
}
pub fn read_issuer(bytes: &[u8]) -> ProtoResult<(Option<Name>, Vec<KeyValue>)> {
let mut byte_iter = bytes.iter();
let name: Option<Name> = {
let take_name = byte_iter.by_ref().take_while(|ch| char::from(**ch) != ';');
let name_str = take_name.cloned().collect::<Vec<u8>>();
if !name_str.is_empty() {
let name_str = str::from_utf8(&name_str)?;
Some(Name::from_ascii(name_str)?)
} else {
None
}
};
let mut state = ParseNameKeyPairState::BeforeKey(vec![]);
for ch in byte_iter {
match state {
ParseNameKeyPairState::BeforeKey(key_values) => {
match char::from(*ch) {
';' | ' ' | '\u{0009}' => state = ParseNameKeyPairState::BeforeKey(key_values),
ch if ch.is_ascii_alphanumeric() && ch != '=' => {
let mut key = String::new();
key.push(ch);
state = ParseNameKeyPairState::Key {
first_char: true,
key,
key_values,
}
}
ch => return Err(format!("bad character in CAA issuer key: {ch}").into()),
}
}
ParseNameKeyPairState::Key {
first_char,
mut key,
key_values,
} => {
match char::from(*ch) {
'=' => {
let value = String::new();
state = ParseNameKeyPairState::Value {
key,
value,
key_values,
}
}
ch if (ch.is_ascii_alphanumeric() || (!first_char && ch == '-'))
&& ch != '='
&& ch != ';' =>
{
key.push(ch);
state = ParseNameKeyPairState::Key {
first_char: false,
key,
key_values,
}
}
ch => return Err(format!("bad character in CAA issuer key: {ch}").into()),
}
}
ParseNameKeyPairState::Value {
key,
mut value,
mut key_values,
} => {
match char::from(*ch) {
';' => {
key_values.push(KeyValue { key, value });
state = ParseNameKeyPairState::BeforeKey(key_values);
}
ch if ('\x21'..='\x3A').contains(&ch) || ('\x3C'..='\x7E').contains(&ch) => {
value.push(ch);
state = ParseNameKeyPairState::Value {
key,
value,
key_values,
}
}
ch => return Err(format!("bad character in CAA issuer value: '{ch}'").into()),
}
}
}
}
let key_values = match state {
ParseNameKeyPairState::BeforeKey(key_values) => key_values,
ParseNameKeyPairState::Value {
key,
value,
mut key_values,
} => {
key_values.push(KeyValue { key, value });
key_values
}
ParseNameKeyPairState::Key { key, .. } => {
return Err(format!("key missing value: {key}").into());
}
};
Ok((name, key_values))
}
pub fn read_iodef(url: &[u8]) -> ProtoResult<Url> {
let url = str::from_utf8(url)?;
let url = Url::parse(url)?;
Ok(url)
}
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
pub struct KeyValue {
key: String,
value: String,
}
impl KeyValue {
pub fn new<K: Into<String>, V: Into<String>>(key: K, value: V) -> Self {
Self {
key: key.into(),
value: value.into(),
}
}
pub fn key(&self) -> &str {
&self.key
}
pub fn value(&self) -> &str {
&self.value
}
}
fn read_tag(decoder: &mut BinDecoder<'_>, len: Restrict<u8>) -> Result<String, DecodeError> {
let len = len
.map(|len| len as usize)
.verify_unwrap(|len| *len > 0 && *len <= 15)
.map_err(|_| DecodeError::CaaTagInvalid)?;
let mut tag = String::with_capacity(len);
for _ in 0..len {
let ch = decoder
.pop()?
.map(char::from)
.verify_unwrap(|ch| matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9'))
.map_err(|_| DecodeError::CaaTagInvalid)?;
tag.push(ch);
}
Ok(tag)
}
fn emit_tag(buf: &mut [u8], tag: &str) -> ProtoResult<u8> {
let len = tag.len();
if len > u8::MAX as usize {
return Err(format!("CAA property too long: {len}").into());
}
if buf.len() < len {
return Err(format!(
"insufficient capacity in CAA buffer: {} for tag: {}",
buf.len(),
len
)
.into());
}
let buf = &mut buf[0..len];
buf.copy_from_slice(tag.as_bytes());
Ok(len as u8)
}
impl BinEncodable for CAA {
fn emit(&self, encoder: &mut BinEncoder<'_>) -> ProtoResult<()> {
let mut encoder = encoder.with_rdata_behavior(RDataEncoding::Other);
encoder.emit(self.flags())?;
let mut tag_buf = [0_u8; u8::MAX as usize];
let len = emit_tag(&mut tag_buf, &self.tag)?;
encoder.emit(len)?;
encoder.emit_vec(&tag_buf[0..len as usize])?;
encoder.emit_vec(&self.value)?;
Ok(())
}
}
impl<'r> RecordDataDecodable<'r> for CAA {
fn read_data(decoder: &mut BinDecoder<'r>, length: Restrict<u16>) -> Result<Self, DecodeError> {
let flags = decoder.read_u8()?.unverified();
let issuer_critical = (flags & 0b1000_0000) != 0;
let reserved_flags = flags & 0b0111_1111;
let tag_len = decoder.read_u8()?;
let value_len = length
.checked_sub(u16::from(tag_len.unverified()))
.checked_sub(2)
.map_err(|len| DecodeError::IncorrectRDataLengthRead {
read: len as usize,
len: u16::from(tag_len.unverified()) as usize + 2,
})?
.unverified();
let tag = read_tag(decoder, tag_len)?;
let value =
decoder.read_vec(value_len as usize)?.unverified();
Ok(CAA {
issuer_critical,
reserved_flags,
tag,
value,
})
}
}
impl RecordData for CAA {
fn try_borrow(data: &RData) -> Option<&Self> {
match data {
RData::CAA(csync) => Some(csync),
_ => None,
}
}
fn record_type(&self) -> RecordType {
RecordType::CAA
}
fn into_rdata(self) -> RData {
RData::CAA(self)
}
}
impl fmt::Display for CAA {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"{flags} {tag} \"{value}\"",
flags = self.flags(),
tag = &self.tag,
value = String::from_utf8_lossy(&self.value)
)
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::dbg_macro, clippy::print_stdout)]
use alloc::{str, string::ToString};
#[cfg(feature = "std")]
use std::println;
use super::*;
#[test]
fn test_read_tag() {
let ok_under15 = b"abcxyzABCXYZ019";
let mut decoder = BinDecoder::new(ok_under15);
let read = read_tag(&mut decoder, Restrict::new(ok_under15.len() as u8))
.expect("failed to read tag");
assert_eq!(str::from_utf8(ok_under15).unwrap(), read);
}
#[test]
fn test_bad_tag() {
let bad_under15 = b"-";
let mut decoder = BinDecoder::new(bad_under15);
assert!(read_tag(&mut decoder, Restrict::new(bad_under15.len() as u8)).is_err());
}
#[test]
fn test_too_short_tag() {
let too_short = b"";
let mut decoder = BinDecoder::new(too_short);
assert!(read_tag(&mut decoder, Restrict::new(too_short.len() as u8)).is_err());
}
#[test]
fn test_too_long_tag() {
let too_long = b"0123456789abcdef";
let mut decoder = BinDecoder::new(too_long);
assert!(read_tag(&mut decoder, Restrict::new(too_long.len() as u8)).is_err());
}
#[test]
fn test_read_issuer() {
assert_eq!(
read_issuer(b"ca.example.net; account=230123").unwrap(),
(
Some(Name::parse("ca.example.net", None).unwrap()),
vec![KeyValue {
key: "account".to_string(),
value: "230123".to_string(),
}],
)
);
assert_eq!(
read_issuer(b"ca.example.net").unwrap(),
(Some(Name::parse("ca.example.net", None,).unwrap(),), vec![],)
);
assert_eq!(
read_issuer(b"ca.example.net; policy=ev").unwrap(),
(
Some(Name::parse("ca.example.net", None).unwrap(),),
vec![KeyValue {
key: "policy".to_string(),
value: "ev".to_string(),
}],
)
);
assert_eq!(
read_issuer(b"ca.example.net; account=230123; policy=ev").unwrap(),
(
Some(Name::parse("ca.example.net", None).unwrap(),),
vec![
KeyValue {
key: "account".to_string(),
value: "230123".to_string(),
},
KeyValue {
key: "policy".to_string(),
value: "ev".to_string(),
},
],
)
);
assert_eq!(
read_issuer(b"example.net; account-uri=https://example.net/account/1234; validation-methods=dns-01").unwrap(),
(
Some(Name::parse("example.net", None).unwrap(),),
vec![
KeyValue {
key: "account-uri".to_string(),
value: "https://example.net/account/1234".to_string(),
},
KeyValue {
key: "validation-methods".to_string(),
value: "dns-01".to_string(),
},
],
)
);
assert_eq!(read_issuer(b";").unwrap(), (None, vec![]));
read_issuer(b"example.com; param=\xff").unwrap_err();
}
#[test]
fn test_read_iodef() {
assert_eq!(
read_iodef(b"mailto:security@example.com").unwrap(),
Url::parse("mailto:security@example.com").unwrap()
);
assert_eq!(
read_iodef(b"https://iodef.example.com/").unwrap(),
Url::parse("https://iodef.example.com/").unwrap()
);
}
fn test_encode_decode(rdata: CAA) {
let mut bytes = Vec::new();
let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut bytes);
rdata.emit(&mut encoder).expect("failed to emit caa");
let bytes = encoder.into_bytes();
#[cfg(feature = "std")]
println!("bytes: {bytes:?}");
let mut decoder: BinDecoder<'_> = BinDecoder::new(bytes);
let read_rdata = CAA::read_data(&mut decoder, Restrict::new(bytes.len() as u16))
.expect("failed to read back");
assert_eq!(rdata, read_rdata);
}
#[test]
fn test_encode_decode_issue() {
test_encode_decode(CAA::new_issue(true, None, vec![]));
test_encode_decode(CAA::new_issue(
true,
Some(Name::parse("example.com", None).unwrap()),
vec![],
));
test_encode_decode(CAA::new_issue(
true,
Some(Name::parse("example.com", None).unwrap()),
vec![KeyValue::new("key", "value")],
));
test_encode_decode(CAA::new_issue(
true,
None,
vec![KeyValue::new("key", "value")],
));
test_encode_decode(CAA::new_issue(
true,
Some(Name::parse("example.com.", None).unwrap()),
vec![],
));
test_encode_decode(CAA {
issuer_critical: false,
reserved_flags: 0,
tag: "issue".to_string(),
value: b"%%%%%".to_vec(),
});
}
#[test]
fn test_encode_decode_issuewild() {
test_encode_decode(CAA::new_issuewild(false, None, vec![]));
}
#[test]
fn test_encode_decode_iodef() {
test_encode_decode(CAA::new_iodef(
true,
Url::parse("https://www.example.com").unwrap(),
));
test_encode_decode(CAA::new_iodef(
false,
Url::parse("mailto:root@example.com").unwrap(),
));
test_encode_decode(CAA {
issuer_critical: false,
reserved_flags: 0,
tag: "iodef".to_string(),
value: vec![0xff],
});
}
#[test]
fn test_encode_decode_unknown() {
test_encode_decode(CAA {
issuer_critical: true,
reserved_flags: 0,
tag: "tbs".to_string(),
value: b"Unknown".to_vec(),
});
}
fn test_encode(rdata: CAA, encoded: &[u8]) {
let mut bytes = Vec::new();
let mut encoder: BinEncoder<'_> = BinEncoder::new(&mut bytes);
rdata.emit(&mut encoder).expect("failed to emit caa");
let bytes = encoder.into_bytes();
assert_eq!(bytes as &[u8], encoded);
}
#[test]
fn test_encode_non_fqdn() {
let name_bytes: &[u8] = b"issueexample.com";
let header: &[u8] = &[128, 5];
let encoded: Vec<u8> = header.iter().chain(name_bytes.iter()).cloned().collect();
test_encode(
CAA::new_issue(
true,
Some(Name::parse("example.com", None).unwrap()),
vec![],
),
&encoded,
);
}
#[test]
fn test_encode_fqdn() {
let name_bytes: &[u8] = b"issueexample.com.";
let header: [u8; 2] = [128, 5];
let encoded: Vec<u8> = header.iter().chain(name_bytes.iter()).cloned().collect();
test_encode(
CAA::new_issue(
true,
Some(Name::parse("example.com.", None).unwrap()),
vec![],
),
&encoded,
);
}
#[test]
fn test_to_string() {
let deny = CAA::new_issue(false, None, vec![]);
assert_eq!(deny.to_string(), "0 issue \";\"");
let empty_options = CAA::new_issue(
false,
Some(Name::parse("example.com", None).unwrap()),
vec![],
);
assert_eq!(empty_options.to_string(), "0 issue \"example.com\"");
let one_option = CAA::new_issue(
false,
Some(Name::parse("example.com", None).unwrap()),
vec![KeyValue::new("one", "1")],
);
assert_eq!(one_option.to_string(), "0 issue \"example.com; one=1\"");
let two_options = CAA::new_issue(
false,
Some(Name::parse("example.com", None).unwrap()),
vec![KeyValue::new("one", "1"), KeyValue::new("two", "2")],
);
assert_eq!(
two_options.to_string(),
"0 issue \"example.com; one=1; two=2\""
);
let flag_set = CAA::new_issue(
true,
Some(Name::parse("example.com", None).unwrap()),
vec![KeyValue::new("one", "1"), KeyValue::new("two", "2")],
);
assert_eq!(
flag_set.to_string(),
"128 issue \"example.com; one=1; two=2\""
);
let empty_domain = CAA::new_issue(
false,
None,
vec![KeyValue::new("one", "1"), KeyValue::new("two", "2")],
);
assert_eq!(empty_domain.to_string(), "0 issue \"; one=1; two=2\"");
assert_eq!(
CAA::new_issue(
false,
Some(Name::parse("ca.example.net", None).unwrap()),
vec![KeyValue::new("account", "230123")]
)
.to_string(),
"0 issue \"ca.example.net; account=230123\""
);
assert_eq!(
CAA::new_issue(
false,
Some(Name::parse("ca.example.net", None).unwrap()),
vec![KeyValue::new("policy", "ev")]
)
.to_string(),
"0 issue \"ca.example.net; policy=ev\""
);
assert_eq!(
CAA::new_iodef(false, Url::parse("mailto:security@example.com").unwrap()).to_string(),
"0 iodef \"mailto:security@example.com\""
);
assert_eq!(
CAA::new_iodef(false, Url::parse("https://iodef.example.com/").unwrap()).to_string(),
"0 iodef \"https://iodef.example.com/\""
);
let unknown = CAA {
issuer_critical: true,
reserved_flags: 0,
tag: "tbs".to_string(),
value: b"Unknown".to_vec(),
};
assert_eq!(unknown.to_string(), "128 tbs \"Unknown\"");
}
#[test]
fn test_unicode_kv() {
const MESSAGE: &[u8] = &[
32, 5, 105, 115, 115, 117, 101, 103, 103, 103, 102, 71, 46, 110, 110, 115, 115, 117,
48, 110, 45, 59, 32, 32, 255, 61, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
];
let mut decoder = BinDecoder::new(MESSAGE);
let caa = CAA::read_data(&mut decoder, Restrict::new(MESSAGE.len() as u16)).unwrap();
assert!(!caa.issuer_critical);
assert_eq!(caa.tag, "issue");
match (caa.value_as_issue(), caa.value_as_iodef()) {
(Err(_), Err(_)) => {}
_ => panic!("wrong value type"),
}
assert_eq!(caa.value, &MESSAGE[7..]);
}
#[test]
fn test_name_non_ascii_character_escaped_dots_roundtrip() {
const MESSAGE: &[u8] = b"\x00\x05issue\xe5\x85\x9edomain\\.\\.name";
let caa = CAA::read_data(
&mut BinDecoder::new(MESSAGE),
Restrict::new(u16::try_from(MESSAGE.len()).unwrap()),
)
.unwrap();
let mut encoded = Vec::new();
caa.emit(&mut BinEncoder::new(&mut encoded)).unwrap();
let caa_round_trip = CAA::read_data(
&mut BinDecoder::new(&encoded),
Restrict::new(u16::try_from(encoded.len()).unwrap()),
)
.unwrap();
assert_eq!(caa, caa_round_trip);
}
#[test]
fn test_reserved_flags_round_trip() {
let mut original = *b"\x00\x05issueexample.com";
for flags in 0..=u8::MAX {
original[0] = flags;
let caa = CAA::read_data(
&mut BinDecoder::new(&original),
Restrict::new(u16::try_from(original.len()).unwrap()),
)
.unwrap();
let mut encoded = Vec::new();
caa.emit(&mut BinEncoder::new(&mut encoded)).unwrap();
assert_eq!(original.as_slice(), &encoded);
}
}
#[test]
fn test_parsing() {
assert!(CAA::from_tokens(vec!["0", "issue", ";"].into_iter()).is_ok());
assert!(CAA::from_tokens(vec!["0", "issue", "example.net"].into_iter()).is_ok());
test_to_string_parse_is_reversible(CAA::new_issue(true, None, vec![]), "128 issue \";\"");
test_to_string_parse_is_reversible(CAA::new_issue(false, None, vec![]), "0 issue \";\"");
test_to_string_parse_is_reversible(
CAA::new_issue(
false,
Some(Name::parse("example.com", None).unwrap()),
vec![],
),
"0 issue \"example.com\"",
);
test_to_string_parse_is_reversible(
CAA::new_issue(
false,
Some(Name::parse("example.com", None).unwrap()),
vec![KeyValue::new("one", "1")],
),
"0 issue \"example.com; one=1\"",
);
test_to_string_parse_is_reversible(
CAA::new_issue(
false,
Some(Name::parse("example.com", None).unwrap()),
vec![KeyValue::new("one", "1"), KeyValue::new("two", "2")],
),
"0 issue \"example.com; one=1; two=2\"",
);
test_to_string_parse_is_reversible(
CAA::new_issue(false, None, vec![KeyValue::new("one", "1")]),
"0 issue \"; one=1\"",
);
test_to_string_parse_is_reversible(
CAA::new_issue(
false,
None,
vec![KeyValue::new("one", "1"), KeyValue::new("two", "2")],
),
"0 issue \"; one=1; two=2\"",
);
}
fn test_to_string_parse_is_reversible(expected_rdata: CAA, input_string: &str) {
let expected_rdata_string = expected_rdata.to_string();
assert_eq!(
input_string, expected_rdata_string,
"input string does not match expected_rdata.to_string()"
);
match RData::try_from_str(RecordType::CAA, input_string).expect("CAA rdata parse failed") {
RData::CAA(parsed_rdata) => assert_eq!(
expected_rdata, parsed_rdata,
"CAA rdata was not parsed as expected. input={input_string:?} expected_rdata={expected_rdata:?} parsed_rdata={parsed_rdata:?}",
),
parsed_rdata => panic!("Parsed RData is not CAA: {:?}", parsed_rdata),
}
}
}