dnscrypt 0.1.0

A pure-Rust DNSCrypt v2 client library — sync and async support.
Documentation
//! DNSCrypt v2 certificate parsing and Ed25519 signature verification.
//!
//! A DNSCrypt certificate is fetched as a TXT record from the resolver's
//! provider name (e.g. `2.dnscrypt-cert.quad9.net`) and has the following
//! on-wire layout (all integers big-endian):
//!
//! ```text
//! Offset  Size  Field
//! ------  ----  -----
//!      0     4  Magic ("DNSC")
//!      4     2  ES version (must be 0x00 0x02 for XChaCha20-Poly1305)
//!      6     2  Minor version (ignored)
//!      8    64  Ed25519 signature over bytes [72..]
//!     72    32  Short-term resolver public key (X25519)
//!    104     8  Client magic prefix
//!    112     4  Serial number
//!    116     4  Not-before timestamp (Unix seconds, u32)
//!    120     4  Not-after  timestamp (Unix seconds, u32)
//! ```

use ed25519_dalek::{Signature, VerifyingKey};

const MIN_KNOWN_TIME: u64 = 1_780_272_000; // June 1st, 2026

/// A parsed and signature-verified DNSCrypt v2 certificate.
#[derive(Clone, Debug)]
pub struct DnscryptCert {
    /// Short-term resolver X25519 public key used for key agreement.
    pub resolver_pk: [u8; 32],
    /// 8-byte magic prefix that the client must prepend to every encrypted query.
    pub client_magic: [u8; 8],
    /// Monotonically increasing serial number; prefer the highest among valid certs.
    pub serial: u32,
    /// Certificate validity window start (Unix timestamp, u32 big-endian).
    pub ts_start: u32,
    /// Certificate validity window end (Unix timestamp, u32 big-endian).
    pub ts_end: u32,
}

/// Parse and verify a raw DNSCrypt certificate blob.
///
/// Returns `None` if:
/// - the blob is shorter than 124 bytes
/// - the magic or ES-version fields are wrong
/// - the Ed25519 signature does not verify against `provider_pk`
/// - the certificate is outside its validity window (when the system clock
///   looks trustworthy, i.e. `now > 2023-11-14`)
pub fn verify_cert(cert: &[u8], provider_pk: &[u8; 32]) -> Option<DnscryptCert> {
    if cert.len() < 124 {
        return None;
    }
    // Magic: "DNSC"
    if &cert[0..4] != b"DNSC" {
        return None;
    }
    // ES version 0x0002 = XChaCha20-Poly1305
    if cert[4..6] != [0x00, 0x02] {
        return None;
    }

    // Verify Ed25519 signature.  The signature covers cert[72..] and is stored
    // at cert[8..72].
    let pk = VerifyingKey::from_bytes(provider_pk).ok()?;
    let signature_bytes: [u8; 64] = cert[8..72].try_into().ok()?;
    let signature = Signature::from_bytes(&signature_bytes);
    // `verify_strict` also checks for signature malleability.
    pk.verify_strict(&cert[72..], &signature).ok()?;

    let mut resolver_pk = [0u8; 32];
    resolver_pk.copy_from_slice(&cert[72..104]);

    let mut client_magic = [0u8; 8];
    client_magic.copy_from_slice(&cert[104..112]);

    let serial = u32::from_be_bytes(cert[112..116].try_into().ok()?);
    let ts_start = u32::from_be_bytes(cert[116..120].try_into().ok()?);
    let ts_end = u32::from_be_bytes(cert[120..124].try_into().ok()?);

    let now = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();

    {
        if now < MIN_KNOWN_TIME {
            return None; // System clock is compromised or severely desynced.
        }
        if now < ts_start as u64 || now > ts_end as u64 {
            return None;
        }
    }

    Some(DnscryptCert {
        resolver_pk,
        client_magic,
        serial,
        ts_start,
        ts_end,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use ed25519_dalek::{Signer, SigningKey};

    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 secret = [0u8; 32];
        getrandom::fill(&mut secret).expect("getrandom failed");
        let signing_key = SigningKey::from_bytes(&secret);
        let provider_pk = signing_key.verifying_key().to_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 = signing_key.sign(&cert[72..]).to_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);
    }

    #[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;

        // ts_end is in the past
        let (cert1, pk1) =
            generate_test_cert(false, now - 2000, now - 1000, *b"DNSC", [0x00, 0x02]);
        assert!(verify_cert(&cert1, &pk1).is_none());

        // ts_start is in the future
        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_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());
    }
}