libdelve/
verifier.rs

1//! Verifier implementation for validating domain ownership
2
3use crate::{
4    challenge::generate_challenge, crypto::verify_signature, discovery::discover_dns_config, Error,
5    Result, SigningPayload, VerificationToken,
6};
7use chrono::{DateTime, Duration, Utc};
8
9#[cfg(feature = "delegate")]
10use crate::{delegate_client::DelegateClient, ChallengeRequest};
11
12/// Verifier for validating domain ownership through DelVe protocol
13pub struct Verifier {
14    verifier_id: String,
15    challenge_duration: Duration,
16}
17
18impl Verifier {
19    /// Create a new verifier
20    ///
21    /// # Arguments
22    ///
23    /// * `verifier_name` - Human-readable name of the verifier service
24    /// * `verifier_id` - Unique identifier for this verifier instance
25    /// * `challenge_duration` - How long challenges remain valid (recommended: 15-60 minutes)
26    pub fn new(verifier_id: impl Into<String>, challenge_duration: Duration) -> Self {
27        Self {
28            verifier_id: verifier_id.into(),
29            challenge_duration,
30        }
31    }
32
33    /// Discover how a domain performs verification
34    ///
35    /// # Arguments
36    ///
37    /// * `domain` - The domain to verify
38    ///
39    /// # Returns
40    ///
41    /// DNS configuration including mode (delegate/direct) and public key
42    pub async fn discover(&self, domain: &str) -> Result<crate::DnsConfig> {
43        discover_dns_config(domain).await
44    }
45
46    /// Generate a challenge for a domain
47    ///
48    /// # Arguments
49    ///
50    /// * `domain` - The domain to verify
51    ///
52    /// # Returns
53    ///
54    /// A tuple of (challenge_string, expiration_time)
55    pub fn create_challenge(&self, _domain: &str) -> Result<(String, DateTime<Utc>)> {
56        generate_challenge(self.challenge_duration)
57    }
58
59    /// Submit a challenge to a delegate service
60    ///
61    /// # Arguments
62    ///
63    /// * `domain` - The domain being verified
64    /// * `delegate_endpoint` - The delegate service endpoint URL
65    /// * `challenge` - The challenge string
66    /// * `expires_at` - When the challenge expires
67    /// * `user_identifier` - Optional user identifier for display (e.g., "[email protected]")
68    ///
69    /// # Returns
70    ///
71    /// A tuple of (request_id, optional_token). If the challenge was immediately approved,
72    /// the token will be present. Otherwise, you need to poll for it.
73    #[cfg(feature = "delegate")]
74    pub async fn submit_challenge_to_delegate(
75        &self,
76        domain: &str,
77        delegate_endpoint: &str,
78        challenge: &str,
79        expires_at: DateTime<Utc>,
80    ) -> Result<(String, Option<VerificationToken>)> {
81        let client = DelegateClient::new(delegate_endpoint);
82
83        let request = ChallengeRequest {
84            domain: domain.to_string(),
85            verifier_id: self.verifier_id.clone(),
86            challenge: challenge.to_string(),
87            expires_at,
88            metadata: None,
89        };
90
91        let response = client.submit_challenge(&request).await?;
92
93        Ok((response.request_id, response.token))
94    }
95
96    /// Poll for a verification token from a delegate service
97    ///
98    /// # Arguments
99    ///
100    /// * `delegate_endpoint` - The delegate service endpoint URL
101    /// * `request_id` - The request ID from challenge submission
102    /// * `max_attempts` - Maximum number of polling attempts (default: 60)
103    /// * `poll_interval_secs` - Seconds to wait between polls (default: 5)
104    ///
105    /// # Returns
106    ///
107    /// The verification token if authorized
108    #[cfg(feature = "delegate")]
109    pub async fn poll_for_token(
110        &self,
111        delegate_endpoint: &str,
112        request_id: &str,
113        max_attempts: Option<u32>,
114        poll_interval_secs: Option<u64>,
115    ) -> Result<VerificationToken> {
116        let client = DelegateClient::new(delegate_endpoint);
117
118        let attempts = max_attempts.unwrap_or(60);
119        let interval = std::time::Duration::from_secs(poll_interval_secs.unwrap_or(5));
120
121        client.poll_for_token(request_id, attempts, interval).await
122    }
123
124    /// Verify a verification token
125    ///
126    /// This validates:
127    /// - Challenge format
128    /// - Challenge hasn't expired
129    /// - Signature is valid
130    /// - Domain and verifier ID match
131    ///
132    /// # Arguments
133    ///
134    /// * `token` - The verification token to validate
135    /// * `expected_domain` - The domain we expect to be verified
136    /// * `expected_challenge` - The challenge we originally issued
137    /// * `dns_public_key` - The public key from DNS discovery
138    ///
139    /// # Returns
140    ///
141    /// Ok(()) if verification succeeds
142    pub fn verify_token(
143        &self,
144        token: &VerificationToken,
145        expected_domain: &str,
146        expected_challenge: &str,
147        dns_public_key: &str,
148    ) -> Result<()> {
149        token.validate()?;
150
151        // Verify domain matches
152        if token.domain != expected_domain {
153            return Err(Error::InvalidResponse(format!(
154                "Domain mismatch: expected {}, got {}",
155                expected_domain, token.domain
156            )));
157        }
158
159        // Verify challenge matches
160        if token.challenge != expected_challenge {
161            return Err(Error::InvalidResponse("Challenge mismatch".to_string()));
162        }
163
164        // Verify verifier ID matches
165        if token.verifier_id != self.verifier_id {
166            return Err(Error::InvalidResponse(format!(
167                "Verifier ID mismatch: expected {}, got {}",
168                self.verifier_id, token.verifier_id
169            )));
170        }
171
172        // Verify public key matches DNS record
173        if token.public_key != dns_public_key {
174            return Err(Error::InvalidResponse(
175                "Public key mismatch with DNS record".to_string(),
176            ));
177        }
178
179        // Construct signing payload
180        let payload = SigningPayload {
181            challenge: token.challenge.clone(),
182            domain: token.domain.clone(),
183            signed_at: token.signed_at.to_rfc3339(),
184            verifier_id: token.verifier_id.clone(),
185        };
186
187        // Verify signature
188        verify_signature(&token.public_key, &token.signature, &payload)?;
189
190        Ok(())
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn test_create_challenge() {
200        let verifier = Verifier::new("service.example.com", Duration::minutes(30));
201
202        let (challenge, expires_at) = verifier.create_challenge("example.com").unwrap();
203
204        // Challenge should be non-empty
205        assert!(!challenge.is_empty());
206
207        // Expiration should be in the future
208        assert!(expires_at > Utc::now());
209    }
210
211    // More comprehensive tests would require mocking DNS and HTTP calls
212}