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}