use aws_lc_rs::signature::{self, UnparsedPublicKey};
use crate::crypto::CipherSuite;
const MIN_KNOWN_TIME: u64 = 1_782_864_000;
#[derive(Clone, Debug)]
pub struct DnscryptCert {
pub resolver_pk: [u8; 32],
pub client_magic: [u8; 8],
pub serial: u32,
pub ts_start: u32,
pub ts_end: u32,
pub cipher_suite: CipherSuite,
}
#[must_use]
pub fn verify_cert(cert: &[u8], provider_pk: &[u8; 32]) -> Option<DnscryptCert> {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
verify_cert_at(cert, provider_pk, now)
}
fn verify_cert_at(cert: &[u8], provider_pk: &[u8; 32], now: u64) -> Option<DnscryptCert> {
if cert.len() < 124 {
return None;
}
if cert.get(0..4)? != b"DNSC" {
return None;
}
let cipher_suite = match cert.get(4..6)? {
[0x00, 0x01] => CipherSuite::XSalsa20Poly1305,
[0x00, 0x02] => CipherSuite::XChaCha20Poly1305,
_ => return None,
};
let pk = UnparsedPublicKey::new(&signature::ED25519, provider_pk);
pk.verify(cert.get(72..)?, cert.get(8..72)?).ok()?;
let resolver_pk: [u8; 32] = cert.get(72..104)?.try_into().ok()?;
let client_magic: [u8; 8] = cert.get(104..112)?.try_into().ok()?;
let serial = u32::from_be_bytes(cert.get(112..116)?.try_into().ok()?);
let ts_start = u32::from_be_bytes(cert.get(116..120)?.try_into().ok()?);
let ts_end = u32::from_be_bytes(cert.get(120..124)?.try_into().ok()?);
if now < MIN_KNOWN_TIME {
return None; }
if now < u64::from(ts_start) || now > u64::from(ts_end) {
return None;
}
Some(DnscryptCert {
resolver_pk,
client_magic,
serial,
ts_start,
ts_end,
cipher_suite,
})
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::cast_possible_truncation,
clippy::indexing_slicing,
clippy::arithmetic_side_effects,
clippy::as_conversions
)]
mod tests {
use super::*;
use aws_lc_rs::signature::{Ed25519KeyPair, KeyPair};
fn generate_test_cert(
modify_signature: bool,
ts_start: u32,
ts_end: u32,
magic: [u8; 4],
es_version: [u8; 2],
) -> (Vec<u8>, [u8; 32]) {
let mut seed = [0u8; 32];
aws_lc_rs::rand::fill(&mut seed).expect("aws-lc-rs RNG failed");
let signing_key = Ed25519KeyPair::from_seed_unchecked(&seed).expect("valid seed");
let provider_pk: [u8; 32] = signing_key
.public_key()
.as_ref()
.try_into()
.expect("ed25519 public key is 32 bytes");
let mut cert = vec![0u8; 124];
cert[0..4].copy_from_slice(&magic);
cert[4..6].copy_from_slice(&es_version);
cert[72..104].copy_from_slice(&[1u8; 32]);
cert[104..112].copy_from_slice(&[2u8; 8]);
cert[112..116].copy_from_slice(&1u32.to_be_bytes());
cert[116..120].copy_from_slice(&ts_start.to_be_bytes());
cert[120..124].copy_from_slice(&ts_end.to_be_bytes());
let mut signature: [u8; 64] = signing_key
.sign(&cert[72..])
.as_ref()
.try_into()
.expect("ed25519 signature is 64 bytes");
if modify_signature {
signature[0] ^= 0xFF;
}
cert[8..72].copy_from_slice(&signature);
(cert, provider_pk)
}
#[test]
fn test_verify_cert_valid() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let (cert, pk) = generate_test_cert(false, now - 1000, now + 1000, *b"DNSC", [0x00, 0x02]);
let parsed = verify_cert(&cert, &pk);
assert!(parsed.is_some());
let parsed = parsed.unwrap();
assert_eq!(parsed.client_magic, [2u8; 8]);
assert_eq!(parsed.serial, 1);
assert_eq!(parsed.cipher_suite, CipherSuite::XChaCha20Poly1305);
}
#[test]
fn test_verify_cert_xsalsa20_suite() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let (cert, pk) = generate_test_cert(false, now - 1000, now + 1000, *b"DNSC", [0x00, 0x01]);
let parsed = verify_cert(&cert, &pk).unwrap();
assert_eq!(parsed.cipher_suite, CipherSuite::XSalsa20Poly1305);
}
#[test]
fn test_verify_cert_invalid_signature() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let (cert, pk) = generate_test_cert(true, now - 1000, now + 1000, *b"DNSC", [0x00, 0x02]);
let parsed = verify_cert(&cert, &pk);
assert!(parsed.is_none());
}
#[test]
fn test_verify_cert_expired() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let (cert1, pk1) =
generate_test_cert(false, now - 2000, now - 1000, *b"DNSC", [0x00, 0x02]);
assert!(verify_cert(&cert1, &pk1).is_none());
let (cert2, pk2) =
generate_test_cert(false, now + 1000, now + 2000, *b"DNSC", [0x00, 0x02]);
assert!(verify_cert(&cert2, &pk2).is_none());
}
#[test]
fn test_verify_cert_at_rejects_untrustworthy_clock() {
let (cert, pk) = generate_test_cert(false, 0, u32::MAX, *b"DNSC", [0x00, 0x02]);
assert!(verify_cert_at(&cert, &pk, MIN_KNOWN_TIME - 1).is_none());
assert!(verify_cert_at(&cert, &pk, MIN_KNOWN_TIME).is_some());
}
#[test]
fn test_verify_cert_invalid_magic_or_version() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let (cert1, pk1) =
generate_test_cert(false, now - 1000, now + 1000, *b"DNXX", [0x00, 0x02]);
assert!(verify_cert(&cert1, &pk1).is_none());
let (cert2, pk2) =
generate_test_cert(false, now - 1000, now + 1000, *b"DNSC", [0x00, 0x03]);
assert!(verify_cert(&cert2, &pk2).is_none());
}
#[test]
fn test_verify_cert_truncated() {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as u32;
let (mut cert, pk) =
generate_test_cert(false, now - 1000, now + 1000, *b"DNSC", [0x00, 0x02]);
cert.truncate(123);
assert!(verify_cert(&cert, &pk).is_none());
}
}