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