first commit

This commit is contained in:
Sven Sauleau
2022-01-20 20:47:29 +00:00
commit bf3a778cdc
15 changed files with 2707 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
target/
Generated
+1111
View File
File diff suppressed because it is too large Load Diff
+29
View File
@@ -0,0 +1,29 @@
[package]
name = "cfdkim"
version = "0.1.0"
authors = ["Sven Sauleau <sven@cloudflare.com>"]
edition = "2021"
description = "DKIM (RFC6376) implementation"
repository = "https://github.com/cloudflare/dkim"
documentation = "https://docs.rs/cfdkim"
categories = ["email"]
keywords = ["email", "dkim", "authentification"]
readme = "README.md"
license = "MIT"
[dependencies]
mailparse = "0.13.7"
quick-error = "2.0.1"
nom = "7.1.0"
chrono = "0.4.19"
trust-dns-resolver = "0.20.3"
futures = "0.3.18"
sha-1 = "0.9"
sha2 = "0.9"
base64 = "0.13.0"
rsa = "0.5.0"
slog = "2.7.0"
indexmap = "1.8.0"
[dev-dependencies]
tokio = { version = "1.14.0", features = ["macros"] }
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Cloudflare
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+32
View File
@@ -0,0 +1,32 @@
# cfdkim
> DKIM ([RFC6376]) implementation
## Features
### Verifying email signatures
Example:
```rust
let res: DKIMResult = cfdkim::verify_email(&logger, &from_domain, &parsed_email).await?;
if let Some(err) = &res.error() {
error!(logger, "dkim verify fail: {}", err);
}
println!("dkim={}", res.with_detail());
```
The `verify_email` arguments are the following:
- `logger`: [slog]::Logger
- `from_domain`: &str ([RFC5322].From's domain)
- `parsed_email`: [mailparse]::ParsedMail
### Signing an email
Work in progress.
[RFC5322]: https://datatracker.ietf.org/doc/html/rfc5322
[RFC6376]: https://datatracker.ietf.org/doc/html/rfc6376
[slog]: https://crates.io/crates/slog
[mailparse]: https://crates.io/crates/mailparse
+62
View File
@@ -0,0 +1,62 @@
///! Various utility functions to operate on bytes
pub(crate) fn get_all_after<'a>(bytes: &'a [u8], end: &[u8]) -> &'a [u8] {
if let Some(mut end_index) = find(bytes, end) {
end_index += end.len();
return &bytes[end_index..];
} else {
return &[];
}
}
/// Find the offset of specific bytes in bytes
pub(crate) fn find(bytes: &[u8], search: &[u8]) -> Option<usize> {
bytes
.windows(search.len())
.position(|window| window == search)
}
pub(crate) fn replace(bytes: &mut [u8], from: char, to: char) {
for byte in bytes.iter_mut() {
if *byte == from as u8 {
*byte = to as u8;
}
}
}
pub(crate) fn replace_slice(source: &[u8], from: &[u8], to: &[u8]) -> Vec<u8> {
let mut result = source.to_vec();
let from_len = from.len();
let to_len = to.len();
let mut i = 0;
while i + from_len <= result.len() {
if result[i..].starts_with(from) {
result.splice(i..i + from_len, to.iter().cloned());
i += to_len;
} else {
i += 1;
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_find() {
assert_eq!(find(&[97, 98, 99], &[1]), None);
assert_eq!(find(&[97, 98, 99], &[97]), Some(0));
assert_eq!(find(&[97, 98, 99], &[97, 98]), Some(0));
}
#[test]
fn it_replace_slice() {
let source = "aba".as_bytes();
assert_eq!(replace_slice(&source, &[97], &[99]), "cbc".as_bytes());
assert_eq!(replace_slice(&source, &[97, 98], &[]), "a".as_bytes());
}
}
+152
View File
@@ -0,0 +1,152 @@
// Inspired from https://docs.rs/dkim/latest/src/dkim/canonicalization.rs.html
use crate::bytes;
#[derive(PartialEq, Clone, Debug)]
pub enum Type {
Simple,
Relaxed,
}
/// Canonicalize body using the simple canonicalization algorithm.
///
/// The first argument **must** be the body of the mail.
pub(crate) fn canonicalize_body_simple(mut body: &[u8]) -> Vec<u8> {
if body.is_empty() {
return b"\r\n".to_vec();
}
while body.ends_with(b"\r\n\r\n") {
body = &body[..body.len() - 2];
}
body.to_vec()
}
/// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.3
/// Canonicalize body using the relaxed canonicalization algorithm.
///
/// The first argument **must** be the body of the mail.
pub(crate) fn canonicalize_body_relaxed(body: &[u8]) -> Vec<u8> {
let mut body = body.to_vec();
// See https://tools.ietf.org/html/rfc6376#section-3.4.4 for implementation details
// Reduce all sequences of WSP within a line to a single SP character.
bytes::replace(&mut body, '\t', ' ');
let mut previous = false;
body.retain(|c| {
if *c == b' ' {
if previous {
false
} else {
previous = true;
true
}
} else {
previous = false;
true
}
});
// Ignore all whitespace at the end of lines. Implementations MUST NOT remove the CRLF at the end of the line.
while let Some(idx) = bytes::find(&body, b" \r\n") {
body.remove(idx);
}
// Ignore all empty lines at the end of the message body. "Empty line" is defined in Section 3.4.3.
while body.ends_with(b"\r\n\r\n") {
body.remove(body.len() - 1);
body.remove(body.len() - 1);
}
// If the body is non-empty but does not end with a CRLF, a CRLF is added. (For email, this is only possible when using extensions to SMTP or non-SMTP transport mechanisms.)
if !body.is_empty() && !body.ends_with(b"\r\n") {
body.push(b'\r');
body.push(b'\n');
}
body
}
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.1
pub(crate) fn canonicalize_header_simple(key: &str, value: &[u8]) -> Vec<u8> {
// TODO: according to the spec whitespace MUST NOT be changed? pydkim does
// change it too.
let mut out = Vec::new();
out.extend_from_slice(&key.as_bytes());
out.extend_from_slice(b":");
out.extend_from_slice(value);
out.extend_from_slice(b"\r\n");
out
}
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.2
pub(crate) fn canonicalize_header_relaxed(key: &str, value: &[u8]) -> Vec<u8> {
let key = key.to_lowercase();
let key = key.trim_end();
let value = canonicalize_header_value_relaxed(value);
let mut out = Vec::new();
out.extend_from_slice(&key.as_bytes());
out.extend_from_slice(b":");
out.extend_from_slice(&value);
out.extend_from_slice(b"\r\n");
out
}
fn canonicalize_header_value_relaxed(value: &[u8]) -> Vec<u8> {
let mut value = value.to_vec();
bytes::replace(&mut value, '\t', ' ');
value = bytes::replace_slice(&value, b"\r\n", b"");
while value.ends_with(b" ") {
value.remove(value.len() - 1);
}
while value.starts_with(b" ") {
value.remove(0);
}
let mut previous = false;
value.retain(|c| {
if *c == b' ' {
if previous {
false
} else {
previous = true;
true
}
} else {
previous = false;
true
}
});
value
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canonicalize_header_relaxed() {
assert_eq!(
canonicalize_header_relaxed("SUBJect", b" AbC\r\n"),
b"subject:AbC\r\n"
);
assert_eq!(
canonicalize_header_relaxed("Subject \t", b"\t Your Name\t \r\n"),
b"subject:Your Name\r\n"
);
assert_eq!(
canonicalize_header_relaxed("Subject \t", b"\t Kimi \t \r\n No \t\r\n Na Wa\r\n"),
b"subject:Kimi No Na Wa\r\n"
);
}
#[test]
fn test_canonicalize_body_relaxed() {
assert_eq!(canonicalize_body_relaxed(b"\r\n"), b"\r\n");
assert_eq!(canonicalize_body_relaxed(b"hey \r\n"), b"hey\r\n");
}
}
+45
View File
@@ -0,0 +1,45 @@
use crate::DKIMError;
use futures::future::BoxFuture;
use std::sync::Arc;
use trust_dns_resolver::error::{ResolveError, ResolveErrorKind};
use trust_dns_resolver::TokioAsyncResolver;
/// A trait for entities that perform DNS resolution.
pub trait Lookup: Sync + Send {
fn lookup_txt<'a>(&'a self, name: &'a str) -> BoxFuture<'a, Result<Vec<String>, DKIMError>>;
}
fn to_lookup_error(err: ResolveError) -> DKIMError {
match err.kind() {
ResolveErrorKind::NoRecordsFound { .. } => DKIMError::NoKeyForSignature,
_ => DKIMError::KeyUnavailable(format!("failed to query DNS: {}", err)),
}
}
// Technically we should be able to implemement Lookup for TokioAsyncResolver
// directly but it's failing for some reason.
struct TokioAsyncResolverWrapper {
inner: TokioAsyncResolver,
}
impl Lookup for TokioAsyncResolverWrapper {
fn lookup_txt<'a>(&'a self, name: &'a str) -> BoxFuture<'a, Result<Vec<String>, DKIMError>> {
Box::pin(async move {
self.inner
.txt_lookup(name)
.await
.map_err(to_lookup_error)?
.into_iter()
.map(|txt| {
Ok(txt
.iter()
.map(|data| String::from_utf8_lossy(data))
.collect())
})
.collect()
})
}
}
pub(crate) fn from_tokio_resolver(resolver: TokioAsyncResolver) -> Arc<dyn Lookup> {
Arc::new(TokioAsyncResolverWrapper { inner: resolver })
}
+91
View File
@@ -0,0 +1,91 @@
/// DKIM error status
pub enum Status {
Permfail,
Tempfail,
}
quick_error! {
#[derive(Debug, PartialEq, Clone)]
/// DKIM errors
pub enum DKIMError {
UnsupportedHashAlgorithm(value: String) {
display("unsupported hash algorithm: {}", value)
}
UnsupportedCanonicalizationType(value: String) {
display("unsupported canonicalization: {}", value)
}
SignatureSyntaxError(err: String) {
display("signature syntax error: {}", err)
}
SignatureMissingRequiredTag(name: &'static str) {
display("signature missing required tag ({})", name)
}
IncompatibleVersion {
display("incompatible version")
}
DomainMismatch {
display("domain mismatch")
}
FromFieldNotSigned {
display("From field not signed")
}
SignatureExpired {
display("signature expired")
}
UnacceptableSignatureHeader {
display("unacceptable signature header")
}
UnsupportedQueryMethod {
display("unsupported query method")
}
KeyUnavailable(err: String) {
display("key unavailable: {}", err)
}
UnknownInternalError(err: String) {
display("internal error: {}", err)
}
NoKeyForSignature {
display("no key for signature")
}
KeySyntaxError {
display("key syntax error")
}
KeyIncompatibleVersion {
display("key incompatible version")
}
InappropriateKeyAlgorithm {
display("inappropriate key algorithm")
}
SignatureDidNotVerify {
display("signature did not verify")
}
BodyHashDidNotVerify {
display("body hash did not verify")
}
}
}
impl DKIMError {
pub fn status(self) -> Status {
use DKIMError::*;
match self {
SignatureSyntaxError(_)
| SignatureMissingRequiredTag(_)
| IncompatibleVersion
| DomainMismatch
| FromFieldNotSigned
| SignatureExpired
| UnacceptableSignatureHeader
| UnsupportedQueryMethod
| NoKeyForSignature
| KeySyntaxError
| KeyIncompatibleVersion
| InappropriateKeyAlgorithm
| SignatureDidNotVerify
| BodyHashDidNotVerify
| UnsupportedCanonicalizationType(_)
| UnsupportedHashAlgorithm(_) => Status::Permfail,
KeyUnavailable(_) | UnknownInternalError(_) => Status::Tempfail,
}
}
}
+379
View File
@@ -0,0 +1,379 @@
use indexmap::set::IndexSet;
use mailparse::MailHeaderMap;
use slog::debug;
use crate::canonicalization::{
self, canonicalize_body_relaxed, canonicalize_body_simple, canonicalize_header_relaxed,
canonicalize_header_simple,
};
use crate::{bytes, DKIMError, DKIMHeader};
#[derive(Debug, Clone)]
pub enum HashAlgo {
RsaSha1,
RsaSha256,
}
/// Get the body part of an email
fn get_body<'a>(email: &'a mailparse::ParsedMail<'a>) -> &'a [u8] {
let body = email.raw_bytes;
bytes::get_all_after(body, b"\r\n\r\n")
}
fn hash_sha1<T: AsRef<[u8]>>(data: T) -> Vec<u8> {
use sha1::{Digest, Sha1};
let mut hasher = Sha1::new();
hasher.update(data);
hasher.finalize().to_vec()
}
fn hash_sha256<T: AsRef<[u8]>>(data: T) -> Vec<u8> {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(data);
hasher.finalize().to_vec()
}
/// Returns the hash of message's body
/// https://datatracker.ietf.org/doc/html/rfc6376#section-3.7
pub(crate) fn compute_body_hash<'a>(
canonicalization_type: canonicalization::Type,
length: Option<String>,
hash_algo: HashAlgo,
email: &'a mailparse::ParsedMail<'a>,
) -> Result<String, DKIMError> {
let body = get_body(email);
let mut canonicalized_body = if canonicalization_type == canonicalization::Type::Simple {
canonicalize_body_simple(body)
} else {
canonicalize_body_relaxed(body)
};
if let Some(length) = length {
let length = length
.parse::<usize>()
.map_err(|err| DKIMError::SignatureSyntaxError(format!("invalid length: {}", err)))?;
canonicalized_body.truncate(length);
};
let hash = match hash_algo {
HashAlgo::RsaSha1 => hash_sha1(&canonicalized_body),
HashAlgo::RsaSha256 => hash_sha256(&canonicalized_body),
};
Ok(base64::encode(&hash))
}
fn select_headers<'a, 'b>(
headers: &'b str,
email: &'a mailparse::ParsedMail<'a>,
) -> Result<Vec<(String, &'a [u8])>, DKIMError> {
let mut signed_headers = vec![];
// Transform the header list into a ordered set to deduplicate the headers
// while precerving the order
let headers: IndexSet<&str> = IndexSet::from_iter(headers.split(":"));
for name in headers {
let name = name.trim();
if let Some(header) = email.headers.get_first_header(name) {
signed_headers.push((name.to_owned(), header.get_value_raw()));
}
}
Ok(signed_headers)
}
pub(crate) fn compute_headers_hash<'a, 'b>(
logger: &slog::Logger,
canonicalization_type: canonicalization::Type,
headers: &'b str,
hash_algo: HashAlgo,
dkim_header: &'b DKIMHeader<'b>,
email: &'a mailparse::ParsedMail<'a>,
) -> Result<Vec<u8>, DKIMError> {
let mut input = Vec::new();
// Add the headers defined in `h=` in the hash
for (key, value) in select_headers(headers, email)? {
let canonicalized_value = if canonicalization_type == canonicalization::Type::Simple {
canonicalize_header_simple(&key, &value)
} else {
canonicalize_header_relaxed(&key, &value)
};
input.extend_from_slice(&canonicalized_value);
}
// Add the DKIM-Signature header in the hash. Remove the value of the
// signature (b) first.
{
let sign = dkim_header.get_raw_tag("b").unwrap();
let value = dkim_header.raw_bytes.replace(&sign, "");
input.extend_from_slice(&"dkim-signature:".as_bytes());
input.extend_from_slice(&value.as_bytes());
}
debug!(logger, "headers to hash: {:?}", input);
let hash = match hash_algo {
HashAlgo::RsaSha1 => hash_sha1(&input),
HashAlgo::RsaSha256 => hash_sha256(&input),
};
Ok(hash)
}
#[cfg(test)]
mod tests {
use super::*;
fn dkim_header() -> DKIMHeader<'static> {
crate::validate_header("v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; s=smtp; d=test.com; t=1641506955; h=content-type:to: subject:date:from:mime-version:sender; bh=PU2XIErWsXvhvt1W96ntPWZ2VImjVZ3vBY2T/A+wA3A=; b=PIO0A014nyntOGKdTdtvCJor9ZxvP1M3hoLeEh8HqZ+RvAyEKdAc7VOg+/g/OTaZgsmw6U sZCoN0YNVp+2o9nkaeUslsVz3M4I55HcZnarxl+fhplIMcJ/3s0nIhXL51MfGPRqPbB7/M Gjg9/07/2vFoid6Kitg6Z+CfoD2wlSRa8xDfmeyA2cHpeVuGQhGxu7BXuU8kGbeM4+weit Ql3t9zalhikEPI5Pr7dzYFrgWNOEO6w6rQfG7niKON1BimjdbJlGanC7cO4UL361hhXT4X iXLnC9TG39xKFPT/+4nkHy8pp6YvWkD3wKlBjwkYNm0JvKGwTskCMDeTwxXhAg==").unwrap()
}
#[test]
fn test_compute_body_hash_simple() {
let email = mailparse::parse_mail(
r#"To: test@sauleau.com
Subject: subject
From: Sven Sauleau <sven@cloudflare.com>
Hello Alice
"#
.as_bytes(),
)
.unwrap();
let canonicalization_type = canonicalization::Type::Simple;
let length = None;
let hash_algo = HashAlgo::RsaSha1;
assert_eq!(
compute_body_hash(
canonicalization_type.clone(),
length.clone(),
hash_algo,
&email
)
.unwrap(),
"uoq1oCgLlTqpdDX/iUbLy7J1Wic="
);
let hash_algo = HashAlgo::RsaSha256;
assert_eq!(
compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
"frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY="
)
}
#[test]
fn test_compute_body_hash_relaxed() {
let email = mailparse::parse_mail(
r#"To: test@sauleau.com
Subject: subject
From: Sven Sauleau <sven@cloudflare.com>
Hello Alice
"#
.as_bytes(),
)
.unwrap();
let canonicalization_type = canonicalization::Type::Relaxed;
let length = None;
let hash_algo = HashAlgo::RsaSha1;
assert_eq!(
compute_body_hash(
canonicalization_type.clone(),
length.clone(),
hash_algo,
&email
)
.unwrap(),
"2jmj7l5rSw0yVb/vlWAYkK/YBwk="
);
let hash_algo = HashAlgo::RsaSha256;
assert_eq!(
compute_body_hash(canonicalization_type, length, hash_algo, &email).unwrap(),
"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
)
}
#[test]
fn test_compute_body_hash_length() {
let email = mailparse::parse_mail(
r#"To: test@sauleau.com
Subject: subject
From: Sven Sauleau <sven@cloudflare.com>
Hello Alice
"#
.as_bytes(),
)
.unwrap();
let canonicalization_type = canonicalization::Type::Relaxed;
let length = Some("3".to_owned());
let hash_algo = HashAlgo::RsaSha1;
assert_eq!(
compute_body_hash(
canonicalization_type.clone(),
length.clone(),
hash_algo,
&email
)
.unwrap(),
"2jmj7l5rSw0yVb/vlWAYkK/YBwk="
);
let hash_algo = HashAlgo::RsaSha256;
assert_eq!(
compute_body_hash(canonicalization_type, length.clone(), hash_algo, &email).unwrap(),
"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
)
}
#[test]
fn test_compute_body_hash_empty_simple() {
let email = mailparse::parse_mail(&[]).unwrap();
let canonicalization_type = canonicalization::Type::Simple;
let length = None;
let hash_algo = HashAlgo::RsaSha1;
assert_eq!(
compute_body_hash(
canonicalization_type.clone(),
length.clone(),
hash_algo,
&email
)
.unwrap(),
"uoq1oCgLlTqpdDX/iUbLy7J1Wic="
);
let hash_algo = HashAlgo::RsaSha256;
assert_eq!(
compute_body_hash(canonicalization_type, length.clone(), hash_algo, &email).unwrap(),
"frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY="
)
}
#[test]
fn test_compute_body_hash_empty_relaxed() {
let email = mailparse::parse_mail(&[]).unwrap();
let canonicalization_type = canonicalization::Type::Relaxed;
let length = None;
let hash_algo = HashAlgo::RsaSha1;
assert_eq!(
compute_body_hash(
canonicalization_type.clone(),
length.clone(),
hash_algo,
&email
)
.unwrap(),
"2jmj7l5rSw0yVb/vlWAYkK/YBwk="
);
let hash_algo = HashAlgo::RsaSha256;
assert_eq!(
compute_body_hash(canonicalization_type, length.clone(), hash_algo, &email).unwrap(),
"47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU="
)
}
#[test]
fn test_compute_headers_hash_simple() {
let email = mailparse::parse_mail(
r#"To: test@sauleau.com
Subject: subject
From: Sven Sauleau <sven@cloudflare.com>
Hello Alice
"#
.as_bytes(),
)
.unwrap();
let canonicalization_type = canonicalization::Type::Simple;
let hash_algo = HashAlgo::RsaSha1;
let headers = "To: Subject".to_owned();
let logger = slog::Logger::root(slog::Discard, slog::o!());
assert_eq!(
compute_headers_hash(
&logger,
canonicalization_type.clone(),
&headers,
hash_algo,
&dkim_header(),
&email
)
.unwrap(),
&[
139, 181, 80, 152, 144, 190, 55, 167, 172, 184, 152, 202, 222, 81, 169, 121, 20, 5,
213, 151
],
);
let hash_algo = HashAlgo::RsaSha256;
assert_eq!(
compute_headers_hash(
&logger,
canonicalization_type.clone(),
&headers,
hash_algo,
&dkim_header(),
&email
)
.unwrap(),
&[
34, 222, 85, 83, 216, 70, 124, 226, 60, 174, 156, 184, 140, 247, 178, 88, 76, 99,
182, 251, 149, 224, 243, 172, 54, 202, 138, 72, 45, 45, 88, 9
]
)
}
#[test]
fn test_compute_headers_hash_relaxed() {
let email = mailparse::parse_mail(
r#"To: test@sauleau.com
Subject: subject
From: Sven Sauleau <sven@cloudflare.com>
Hello Alice
"#
.as_bytes(),
)
.unwrap();
let canonicalization_type = canonicalization::Type::Relaxed;
let hash_algo = HashAlgo::RsaSha1;
let headers = "To: Subject".to_owned();
let logger = slog::Logger::root(slog::Discard, slog::o!());
assert_eq!(
compute_headers_hash(
&logger,
canonicalization_type.clone(),
&headers,
hash_algo,
&dkim_header(),
&email
)
.unwrap(),
&[
14, 171, 230, 1, 77, 117, 47, 207, 243, 167, 179, 5, 150, 82, 154, 25, 125, 124,
44, 164
]
);
let hash_algo = HashAlgo::RsaSha256;
assert_eq!(
compute_headers_hash(
&logger,
canonicalization_type.clone(),
&headers,
hash_algo,
&dkim_header(),
&email
)
.unwrap(),
&[
45, 186, 211, 81, 49, 111, 18, 147, 180, 245, 207, 39, 9, 9, 118, 137, 248, 204,
70, 214, 16, 98, 216, 111, 230, 130, 196, 3, 60, 201, 166, 224
]
)
}
}
+28
View File
@@ -0,0 +1,28 @@
use crate::parser;
use std::collections::HashMap;
pub(crate) const HEADER: &str = "DKIM-Signature";
pub(crate) const REQUIRED_TAGS: &[&str] = &["v", "a", "b", "bh", "d", "h", "s"];
#[derive(Debug)]
pub struct DKIMHeader<'a> {
pub(crate) tags: HashMap<String, parser::Tag>,
pub(crate) raw_bytes: &'a str,
}
impl<'a> DKIMHeader<'a> {
pub(crate) fn get_tag(&self, name: &str) -> Option<String> {
self.tags.get(name).map(|v| v.value.clone())
}
pub(crate) fn get_raw_tag(&self, name: &str) -> Option<String> {
self.tags.get(name).map(|v| v.raw_value.clone())
}
pub(crate) fn get_required_tag(&self, name: &str) -> String {
// Required tags are guaranteed by the parser to be present so it's safe
// to assert and unwrap.
debug_assert!(REQUIRED_TAGS.contains(&name));
self.tags.get(name).unwrap().value.clone()
}
}
+309
View File
@@ -0,0 +1,309 @@
// Implementation of DKIM: https://datatracker.ietf.org/doc/html/rfc6376
use rsa::PublicKey;
use slog::debug;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use trust_dns_resolver::TokioAsyncResolver;
use mailparse::MailHeaderMap;
#[macro_use]
extern crate quick_error;
mod bytes;
mod canonicalization;
mod dns;
mod errors;
mod hash;
mod header;
mod parser;
mod public_key;
mod result;
pub use errors::DKIMError;
use header::*;
pub use parser::tag_list as parse_tag_list;
pub use parser::Tag;
pub use result::DKIMResult;
const SIGN_EXPIRATION_DRIFT_MINS: i64 = 15;
const DNS_NAMESPACE: &str = "_domainkey";
// https://datatracker.ietf.org/doc/html/rfc6376#section-6.1.1
fn validate_header<'a>(value: &'a str) -> Result<DKIMHeader<'a>, DKIMError> {
let (_, tags) =
parser::tag_list(value).map_err(|err| DKIMError::SignatureSyntaxError(err.to_string()))?;
// Check presence of required tags
{
let mut tag_names: HashSet<String> = HashSet::new();
for tag in &tags {
tag_names.insert(tag.name.clone());
}
for required in REQUIRED_TAGS {
if tag_names.get(*required).is_none() {
return Err(DKIMError::SignatureMissingRequiredTag(required));
}
}
}
let mut tags_map = HashMap::new();
for tag in &tags {
tags_map.insert(tag.name.clone(), tag.clone());
}
let header = DKIMHeader {
tags: tags_map,
raw_bytes: value,
};
// FIXME: we could get the keys instead of generating tag_names ourselves
// Check version
{
let version = header.get_required_tag("v");
if version != "1" {
return Err(DKIMError::IncompatibleVersion);
}
}
// Check that "d=" tag is the same as or a parent domain of the domain part
// of the "i=" tag
if let Some(user) = header.get_tag("i") {
let signing_domain = header.get_required_tag("d");
// TODO: naive check, should switch to parsing the domains/email
if !user.ends_with(&signing_domain) {
return Err(DKIMError::DomainMismatch);
}
}
// Check that "h=" tag includes the From header
{
let value = header.get_required_tag("h");
let headers = value.split(":");
let headers: Vec<String> = headers.map(|h| h.to_lowercase()).collect();
if !headers.contains(&"from".to_string()) {
return Err(DKIMError::FromFieldNotSigned);
}
}
if let Some(query_method) = header.get_tag("q") {
if query_method != "dns/txt" {
return Err(DKIMError::UnsupportedQueryMethod);
}
}
// Check that "x=" tag isn't expired
if let Some(expiration) = header.get_tag("x") {
let mut expiration =
chrono::NaiveDateTime::from_timestamp(expiration.parse::<i64>().unwrap_or_default(), 0);
expiration += chrono::Duration::minutes(SIGN_EXPIRATION_DRIFT_MINS);
let now = chrono::Utc::now().naive_utc();
if now > expiration {
return Err(DKIMError::SignatureExpired);
}
}
Ok(header)
}
// https://datatracker.ietf.org/doc/html/rfc6376#section-6.1.3 Step 4
// TODO: implement verification with ed25519 keys
fn verify_signature(
hash_algo: hash::HashAlgo,
header_hash: Vec<u8>,
signature: Vec<u8>,
public_key: impl PublicKey,
) -> Result<bool, DKIMError> {
Ok(public_key
.verify(
rsa::PaddingScheme::PKCS1v15Sign {
hash: Some(match hash_algo {
hash::HashAlgo::RsaSha1 => rsa::hash::Hash::SHA1,
hash::HashAlgo::RsaSha256 => rsa::hash::Hash::SHA2_256,
}),
},
&header_hash,
&signature,
)
.is_ok())
}
async fn verify_email_header<'a>(
logger: &'a slog::Logger,
resolver: Arc<dyn dns::Lookup>,
dkim_header: &'a DKIMHeader<'a>,
email: &'a mailparse::ParsedMail<'a>,
) -> Result<(), DKIMError> {
let public_key = public_key::retrieve_public_key(
logger,
Arc::clone(&resolver),
dkim_header.get_required_tag("d"),
dkim_header.get_required_tag("s"),
dkim_header.get_tag("k"),
)
.await?;
let (header_canonicalization_type, body_canonicalization_type) =
parser::parse_canonicalization(dkim_header.get_tag("c"))?;
let hash_algo = parser::parse_hash_algo(&dkim_header.get_required_tag("a"))?;
let computed_body_hash = hash::compute_body_hash(
body_canonicalization_type,
dkim_header.get_tag("l"),
hash_algo.clone(),
email,
)?;
let computed_headers_hash = hash::compute_headers_hash(
logger,
header_canonicalization_type,
&dkim_header.get_required_tag("h"),
hash_algo.clone(),
&dkim_header,
email,
)?;
debug!(logger, "body_hash {:?}", computed_body_hash);
let header_body_hash = dkim_header.get_required_tag("bh").clone();
if header_body_hash != computed_body_hash {
return Err(DKIMError::BodyHashDidNotVerify);
}
let signature = base64::decode(dkim_header.get_required_tag("b")).map_err(|err| {
DKIMError::SignatureSyntaxError(format!("failed to decode signature: {}", err))
})?;
if !verify_signature(hash_algo, computed_headers_hash, signature, public_key)? {
return Err(DKIMError::SignatureDidNotVerify);
}
Ok(())
}
/// Run the DKIM verification on the email
pub async fn verify_email<'a>(
logger: &slog::Logger,
from_domain: &str,
email: &'a mailparse::ParsedMail<'a>,
) -> Result<DKIMResult, DKIMError> {
let resolver = TokioAsyncResolver::tokio_from_system_conf().map_err(|err| {
DKIMError::UnknownInternalError(format!("failed to create DNS resolver: {}", err))
})?;
let resolver = dns::from_tokio_resolver(resolver);
let mut last_error = None;
for h in email.headers.get_all_headers(HEADER) {
let value = h.get_value();
debug!(logger, "checking signature {:?}", value);
let dkim_header = match validate_header(&value) {
Ok(v) => v,
Err(err) => {
debug!(logger, "failed to verify: {}", err);
last_error = Some(err);
continue;
}
};
// Select the signature corresponding to the email sender
let signing_domain = dkim_header.get_required_tag("d");
if signing_domain != from_domain {
continue;
}
match verify_email_header(logger, Arc::clone(&resolver), &dkim_header, email).await {
Ok(()) => return Ok(DKIMResult::pass(signing_domain)),
Err(err) => {
debug!(logger, "failed to verify: {}", err);
last_error = Some(err);
continue;
}
}
}
if let Some(err) = last_error {
Ok(DKIMResult::fail(err, from_domain.to_owned()))
} else {
Ok(DKIMResult::neutral(from_domain.to_owned()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_header() {
let header = r#"v=1; a=rsa-sha256; d=example.net; s=brisbane;
c=relaxed/simple; q=dns/txt; i=foo@eng.example.net;
t=1117574938; x=9118006938; l=200;
h=from:to:subject:date:keywords:keywords;
z=From:foo@eng.example.net|To:joe@example.com|
Subject:demo=20run|Date:July=205,=202005=203:44:08=20PM=20-0700;
bh=MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI=;
b=dzdVyOfAKCdLXdJOc9G2q8LoXSlEniSbav+yuU4zGeeruD00lszZ
VoG4ZHRNiYzR
"#;
validate_header(header).unwrap();
}
#[test]
fn test_validate_header_missing_tag() {
let header = "v=1; a=rsa-sha256; bh=a; b=b";
assert_eq!(
validate_header(header).unwrap_err(),
DKIMError::SignatureMissingRequiredTag("d")
);
}
#[test]
fn test_validate_header_domain_mismatch() {
let header = r#"v=1; a=rsa-sha256; d=example.net; s=brisbane; i=foo@hein.com; h=headers; bh=hash; b=hash
"#;
assert_eq!(
validate_header(header).unwrap_err(),
DKIMError::DomainMismatch
);
}
#[test]
fn test_validate_header_incompatible_version() {
let header = r#"v=3; a=rsa-sha256; d=example.net; s=brisbane; i=foo@example.net; h=headers; bh=hash; b=hash
"#;
assert_eq!(
validate_header(header).unwrap_err(),
DKIMError::IncompatibleVersion
);
}
#[test]
fn test_validate_header_missing_from_in_headers_signature() {
let header = r#"v=1; a=rsa-sha256; d=example.net; s=brisbane; i=foo@example.net; h=Subject:A:B; bh=hash; b=hash
"#;
assert_eq!(
validate_header(header).unwrap_err(),
DKIMError::FromFieldNotSigned
);
}
#[test]
fn test_validate_header_expired_in_drift() {
let mut now = chrono::Utc::now().naive_utc();
now -= chrono::Duration::seconds(1);
let header = format!("v=1; a=rsa-sha256; d=example.net; s=brisbane; i=foo@example.net; h=From:B; bh=hash; b=hash; x={}", now.timestamp());
assert!(validate_header(&header).is_ok());
}
#[test]
fn test_validate_header_expired() {
let mut now = chrono::Utc::now().naive_utc();
now -= chrono::Duration::hours(3);
let header = format!("v=1; a=rsa-sha256; d=example.net; s=brisbane; i=foo@example.net; h=From:B; bh=hash; b=hash; x={}", now.timestamp());
assert_eq!(
validate_header(&header).unwrap_err(),
DKIMError::SignatureExpired
);
}
}
+240
View File
@@ -0,0 +1,240 @@
use crate::{canonicalization, hash, DKIMError};
use nom::bytes::complete::tag;
use nom::bytes::complete::take_while1;
use nom::character::complete::alpha1;
use nom::combinator::opt;
use nom::multi::fold_many0;
use nom::sequence::delimited;
use nom::sequence::pair;
use nom::sequence::preceded;
use nom::sequence::terminated;
use nom::IResult;
#[derive(Clone, Debug, PartialEq)]
/// DKIM signature tag
pub struct Tag {
/// Name of the tag (v, i, a, h, ...)
pub name: String,
/// Value of the tag with spaces removed
pub value: String,
/// Value of the tag as seen in the text
pub raw_value: String,
}
/// Main entrypoint of the parser. Parses the DKIM signature tag list
/// as specified https://datatracker.ietf.org/doc/html/rfc6376#section-3.6.1.
/// tag-list = tag-spec *( ";" tag-spec ) [ ";" ]
pub fn tag_list(input: &str) -> IResult<&str, Vec<Tag>> {
let (input, start) = tag_spec(input)?;
terminated(
fold_many0(
preceded(tag(";"), tag_spec),
move || vec![start.clone()],
|mut acc: Vec<Tag>, item| {
acc.push(item);
acc
},
),
opt(tag(";")),
)(input)
}
/// tag-spec = [FWS] tag-name [FWS] "=" [FWS] tag-value [FWS]
fn tag_spec(input: &str) -> IResult<&str, Tag> {
let (input, name) = delimited(opt(fws), tag_name, opt(fws))(input)?;
let (input, _) = tag("=")(input)?;
// Parse the twice to keep the original text
let value_input = input;
let (_, raw_value) = delimited(opt(fws), raw_tag_value, opt(fws))(value_input)?;
let (input, value) = delimited(opt(fws), tag_value, opt(fws))(value_input)?;
Ok((
input,
Tag {
name: name.to_owned(),
value,
raw_value,
},
))
}
/// tag-name = ALPHA *ALNUMPUNC
/// ALNUMPUNC = ALPHA / DIGIT / "_"
fn tag_name(input: &str) -> IResult<&str, &str> {
alpha1(input)
}
/// tag-value = [ tval *( 1*(WSP / FWS) tval ) ]
/// tval = 1*VALCHAR
/// VALCHAR = %x21-3A / %x3C-7E
fn tag_value(input: &str) -> IResult<&str, String> {
let is_valchar = |c| (c >= '!' && c <= ':') || (c >= '<' && c <= '~');
match opt(take_while1(is_valchar))(input)? {
(input, Some(start)) => fold_many0(
preceded(fws, take_while1(is_valchar)),
|| start.to_owned(),
|mut acc: String, item| {
acc += item;
acc
},
)(input),
(input, None) => Ok((input, "".to_string())),
}
}
fn raw_tag_value(input: &str) -> IResult<&str, String> {
let is_valchar = |c| (c >= '!' && c <= ':') || (c >= '<' && c <= '~');
match opt(take_while1(is_valchar))(input)? {
(input, Some(start)) => fold_many0(
pair(fws, take_while1(is_valchar)),
|| start.to_owned(),
|mut acc: String, item| {
acc += &(item.0.to_owned() + item.1);
acc
},
)(input),
(input, None) => Ok((input, "".to_string())),
}
}
/// FWS is folding whitespace. It allows multiple lines separated by CRLF followed by at least one whitespace, to be joined.
fn fws(input: &str) -> IResult<&str, &str> {
take_while1(|c| c == ' ' || c == '\t' || c == '\r' || c == '\n')(input)
}
pub(crate) fn parse_hash_algo(value: &String) -> Result<hash::HashAlgo, DKIMError> {
use hash::HashAlgo;
match value.as_str() {
"rsa-sha1" => Ok(HashAlgo::RsaSha1),
"rsa-sha256" => Ok(HashAlgo::RsaSha256),
e => Err(DKIMError::UnsupportedHashAlgorithm(e.to_string())),
}
}
/// Parses the canonicalization value (passed in c=) and returns canonicalization
/// for (Header, Body)
pub(crate) fn parse_canonicalization(
value: Option<String>,
) -> Result<(canonicalization::Type, canonicalization::Type), DKIMError> {
use canonicalization::Type::{Relaxed, Simple};
if value.is_none() {
return Ok((Simple, Simple));
}
match value.unwrap().as_str() {
"simple/simple" => Ok((Simple, Simple)),
"relaxed/simple" => Ok((Relaxed, Simple)),
"simple/relaxed" => Ok((Simple, Relaxed)),
"relaxed/relaxed" => Ok((Relaxed, Relaxed)),
"relaxed" => Ok((Relaxed, Simple)),
"simple" => Ok((Simple, Simple)),
v => Err(DKIMError::UnsupportedCanonicalizationType(v.to_owned())),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_canonicalization_empty() {
use canonicalization::Type::Simple;
assert_eq!(parse_canonicalization(None).unwrap(), (Simple, Simple));
}
#[test]
fn test_canonicalization_one_algo() {
use canonicalization::Type::{Relaxed, Simple};
assert_eq!(
parse_canonicalization(Some("simple".to_string())).unwrap(),
(Simple, Simple)
);
assert_eq!(
parse_canonicalization(Some("relaxed".to_string())).unwrap(),
(Relaxed, Simple)
);
}
#[test]
fn test_tag_list() {
assert_eq!(
tag_list("a = a/1@.-:= ").unwrap(),
(
"",
vec![Tag {
name: "a".to_string(),
value: "a/1@.-:=".to_string(),
raw_value: "a/1@.-:=".to_string()
}]
)
);
assert_eq!(
tag_list("a= a ; b = a\n bc").unwrap(),
(
"",
vec![
Tag {
name: "a".to_string(),
value: "a".to_string(),
raw_value: "a".to_string()
},
Tag {
name: "b".to_string(),
value: "abc".to_string(),
raw_value: "a\n bc".to_string()
}
]
)
);
}
#[test]
fn test_tag_spec() {
assert_eq!(
tag_spec("a=b").unwrap(),
(
"",
Tag {
name: "a".to_string(),
value: "b".to_string(),
raw_value: "b".to_string()
}
)
);
assert_eq!(
tag_spec("a=b c d e f").unwrap(),
(
"",
Tag {
name: "a".to_string(),
value: "bcdef".to_string(),
raw_value: "b c d e f".to_string()
}
)
);
}
#[test]
fn test_tag_list_dns() {
assert_eq!(
tag_list("k=rsa; p=kEy+/").unwrap(),
(
"",
vec![
Tag {
name: "k".to_string(),
value: "rsa".to_string(),
raw_value: "rsa".to_string()
},
Tag {
name: "p".to_string(),
value: "kEy+/".to_string(),
raw_value: "kEy+/".to_string()
}
]
)
);
}
}
+148
View File
@@ -0,0 +1,148 @@
use rsa::{pkcs8, RsaPublicKey};
use slog::{debug, warn};
use std::collections::HashMap;
use std::sync::Arc;
use crate::{dns, parser, DKIMError, DNS_NAMESPACE};
// https://datatracker.ietf.org/doc/html/rfc6376#section-6.1.2
pub(crate) async fn retrieve_public_key(
logger: &slog::Logger,
resolver: Arc<dyn dns::Lookup>,
domain: String,
subdomain: String,
key_type: Option<String>,
) -> Result<RsaPublicKey, DKIMError> {
let dns_name = format!("{}.{}.{}", subdomain, DNS_NAMESPACE, domain);
let res = resolver.lookup_txt(&dns_name).await?;
// TODO: Return multiple keys for when verifiying the signatures. During key
// rotation they are often multiple keys to consider.
let txt = res.first().ok_or(DKIMError::NoKeyForSignature)?;
debug!(logger, "DKIM TXT: {:?}", txt);
// Parse the tags inside the DKIM TXT DNS record
let (_, tags) = parser::tag_list(txt).map_err(|err| {
warn!(logger, "key syntax error: {}", err);
DKIMError::KeySyntaxError
})?;
let mut tags_map = HashMap::new();
for tag in &tags {
tags_map.insert(tag.name.clone(), tag.clone());
}
// Check version
if let Some(version) = tags_map.get("v") {
if version.value != "DKIM1" {
return Err(DKIMError::KeyIncompatibleVersion);
}
}
// Check key has right type
if let Some(v) = tags_map.get("k") {
let key_type = key_type.unwrap_or_else(|| "rsa".to_string());
if v.value != key_type {
return Err(DKIMError::InappropriateKeyAlgorithm);
}
}
let tag = tags_map.get("p").ok_or(DKIMError::NoKeyForSignature)?;
let bytes = base64::decode(&tag.value).map_err(|err| {
DKIMError::KeyUnavailable(format!("failed to decode public key: {}", err))
})?;
let key = pkcs8::FromPublicKey::from_public_key_der(&bytes)
.map_err(|err| DKIMError::KeyUnavailable(format!("failed to parse public key: {}", err)))?;
Ok(key)
}
#[cfg(test)]
mod tests {
use super::*;
use futures::future::BoxFuture;
#[tokio::test]
async fn test_retrieve_public_key() {
struct TestResolver {}
impl dns::Lookup for TestResolver {
fn lookup_txt<'a>(
&'a self,
name: &'a str,
) -> BoxFuture<'a, Result<Vec<String>, DKIMError>> {
Box::pin(async move {
assert_eq!(name, "dkim._domainkey.cloudflare.com");
Ok(vec!["v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6gmVDBSBJ0l1/33uAF0gwIsrjQV6nnYjL9DMX6+ez4NNJ2um0InYy128Rd+OlIhmdSld6g3tj3O6R+BwsYsQgU8RWE8VJaRybvPw2P3Asgms4uPrFWHSFiWMPH0P9i/oPwnUO9jZKHiz4+MzFC3bG8BacX7YIxCuWnDU8XNmNsRaLmrv9CHX4/3GHyoHSmDA1ETtyz9JHRCOC8ho8C7b4f2Auwedlau9Lid9LGBhozhgRFhrFwFMe93y34MO1clPbY6HwxpudKWBkMQCTlmXVRnkKxHlJ+fYCyC2jjpCIbGWj2oLxBtFOASWMESR4biW0ph2bsZXslcUSPMTVTkFxQIDAQAB".to_string()])
})
}
}
let resolver = Arc::new(TestResolver {});
let logger = slog::Logger::root(slog::Discard, slog::o!());
retrieve_public_key(
&logger,
resolver,
"cloudflare.com".to_string(),
"dkim".to_string(),
None,
)
.await
.unwrap();
}
#[tokio::test]
async fn test_retrieve_public_key_incompatible_version() {
struct TestResolver {}
impl dns::Lookup for TestResolver {
fn lookup_txt<'a>(
&'a self,
name: &'a str,
) -> BoxFuture<'a, Result<Vec<String>, DKIMError>> {
Box::pin(async move {
assert_eq!(name, "dkim._domainkey.cloudflare.com");
Ok(vec!["v=DKIM6; p=key".to_string()])
})
}
}
let resolver = Arc::new(TestResolver {});
let logger = slog::Logger::root(slog::Discard, slog::o!());
let key = retrieve_public_key(
&logger,
resolver,
"cloudflare.com".to_string(),
"dkim".to_string(),
None,
)
.await
.unwrap_err();
assert_eq!(key, DKIMError::KeyIncompatibleVersion);
}
#[tokio::test]
async fn test_retrieve_public_key_inappropriate_key_algorithm() {
struct TestResolver {}
impl dns::Lookup for TestResolver {
fn lookup_txt<'a>(
&'a self,
name: &'a str,
) -> BoxFuture<'a, Result<Vec<String>, DKIMError>> {
Box::pin(async move {
assert_eq!(name, "dkim._domainkey.cloudflare.com");
Ok(vec!["v=DKIM1; p=key; k=foo".to_string()])
})
}
}
let resolver = Arc::new(TestResolver {});
let logger = slog::Logger::root(slog::Discard, slog::o!());
let key = retrieve_public_key(
&logger,
resolver,
"cloudflare.com".to_string(),
"dkim".to_string(),
None,
)
.await
.unwrap_err();
assert_eq!(key, DKIMError::InappropriateKeyAlgorithm);
}
}
+59
View File
@@ -0,0 +1,59 @@
use crate::DKIMError;
#[derive(Clone)]
/// Result of the DKIM verification
pub struct DKIMResult {
value: &'static str,
error: Option<DKIMError>,
domain_used: String,
}
impl DKIMResult {
/// Constructs a `pass` result
pub fn pass(domain_used: String) -> Self {
DKIMResult {
value: "pass",
error: None,
domain_used,
}
}
/// Constructs a `neutral` result
pub fn neutral(domain_used: String) -> Self {
DKIMResult {
value: "neutral",
error: None,
domain_used,
}
}
/// Constructs a `fail` result with a reason
pub fn fail(reason: DKIMError, domain_used: String) -> Self {
DKIMResult {
value: "fail",
error: Some(reason),
domain_used,
}
}
pub fn error(&self) -> Option<DKIMError> {
self.error.clone()
}
/// Returns the domain used to pass the DKIM verification
pub fn domain_used(&self) -> String {
self.domain_used.to_lowercase()
}
/// Returns the verification result as a summary: fail, neutral or pass.
pub fn summary(&self) -> &'static str {
self.value
}
/// Similar to `summary` but with detail on fail. Typically used for the
/// `Authentication-Results` header.
pub fn with_detail(&self) -> String {
if let Some(err) = self.error() {
format!("{} ({})", self.value, err)
} else {
self.value.to_owned()
}
}
}