1use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
4use chrono::{DateTime, Duration, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7
8use crate::{
9 challenge::{generate_challenge, MIN_CHALLENGE_BYTES},
10 crypto, Error, Result,
11};
12
13pub const PROTOCOL_VERSION: &str = "delve0.1";
15
16#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub enum VerificationMode {
20 Delegate,
21 Direct,
22}
23
24#[derive(Debug, Clone)]
26pub struct DnsConfig {
27 pub version: String,
28 pub mode: VerificationMode,
29 pub endpoint: Option<String>,
30 pub public_key: String,
31}
32
33impl DnsConfig {
34 pub fn to_dns_record(&self) -> String {
38 let mut parts = vec![
39 format!("v={}", self.version),
40 format!(
41 "mode={}",
42 match self.mode {
43 VerificationMode::Delegate => "delegate",
44 VerificationMode::Direct => "direct",
45 }
46 ),
47 ];
48
49 if let Some(ref endpoint) = self.endpoint {
50 parts.push(format!("endpoint={}", endpoint));
51 }
52
53 parts.push(format!("key={}", self.public_key));
54
55 parts.join("; ")
56 }
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct ChallengeRequest {
63 pub domain: String,
64 pub verifier_id: String,
65 pub challenge: String,
66 pub expires_at: DateTime<Utc>,
67 #[serde(skip_serializing_if = "Option::is_none")]
68 pub metadata: Option<HashMap<String, serde_json::Value>>,
69}
70
71impl ChallengeRequest {
72 pub fn new(domain: &str, verifier_id: &str, expires_in: Duration) -> Result<Self> {
73 let (ch, exp) = generate_challenge(expires_in)?;
74 Ok(ChallengeRequest {
75 domain: domain.to_string(),
76 verifier_id: verifier_id.to_string(),
77 challenge: ch,
78 expires_at: exp,
79 metadata: None,
80 })
81 }
82
83 pub fn with_meta(self, metadata: HashMap<String, serde_json::Value>) -> Self {
84 Self {
85 metadata: Some(metadata),
86 ..self
87 }
88 }
89
90 pub fn create_signing_payload(&self, signed_at: DateTime<Utc>) -> SigningPayload {
92 SigningPayload {
93 challenge: self.challenge.clone(),
94 domain: self.domain.clone(),
95 signed_at: signed_at.to_rfc3339(),
96 verifier_id: self.verifier_id.clone(),
97 }
98 }
99
100 pub fn sign_payload(&self, signed_at: DateTime<Utc>, private_key: &str) -> Result<String> {
101 crypto::sign_payload(private_key, &self.create_signing_payload(signed_at))
102 }
103
104 pub fn is_expired(&self) -> bool {
105 chrono::Utc::now() > self.expires_at
106 }
107
108 pub fn validate(&self) -> Result<()> {
109 let decoded = BASE64
110 .decode(&self.challenge)
111 .map_err(|e| Error::InvalidChallenge(format!("Invalid base64: {}", e)))?;
112
113 if decoded.len() < MIN_CHALLENGE_BYTES {
115 return Err(Error::InvalidChallenge(format!(
116 "Challenge too short: {} bytes (minimum {})",
117 decoded.len(),
118 MIN_CHALLENGE_BYTES
119 )));
120 }
121
122 if chrono::Utc::now() > self.expires_at {
123 Err(Error::ChallengeExpired)
124 } else {
125 Ok(())
126 }
127 }
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "camelCase")]
133pub struct ChallengeResponse {
134 pub request_id: String,
135 pub status: RequestStatus,
136 #[serde(skip_serializing_if = "Option::is_none")]
137 pub authorization_url: Option<String>,
138 pub expires_at: DateTime<Utc>,
139 #[serde(skip_serializing_if = "Option::is_none")]
140 pub token: Option<VerificationToken>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize)]
145#[serde(tag = "status", rename_all = "camelCase")]
146pub enum TokenResponse {
147 Authorized {
148 request_id: String,
149 token: VerificationToken,
150 },
151 Pending {
152 request_id: String,
153 authorization_url: String,
154 },
155 Rejected {
156 request_id: String,
157 rejected_at: DateTime<Utc>,
158 },
159}
160
161#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub enum RequestStatus {
165 Pending,
166 Authorized,
167 Rejected,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct VerificationToken {
174 pub domain: String,
175 pub verifier_id: String,
176 pub challenge: String,
177 pub signature: String,
178 pub public_key: String,
179 pub key_id: String,
180 pub signed_at: DateTime<Utc>,
181 pub expires_at: DateTime<Utc>,
182}
183
184impl VerificationToken {
185 pub fn new(
187 request: &ChallengeRequest,
188 signature: String,
189 public_key: String,
190 key_id: String,
191 signed_at: DateTime<Utc>,
192 ) -> Self {
193 Self {
194 domain: request.domain.clone(),
195 verifier_id: request.verifier_id.clone(),
196 challenge: request.challenge.clone(),
197 signature,
198 public_key,
199 key_id,
200 signed_at,
201 expires_at: request.expires_at,
202 }
203 }
204
205 pub fn validate(&self) -> Result<()> {
206 let decoded = BASE64
207 .decode(&self.challenge)
208 .map_err(|e| Error::InvalidChallenge(format!("Invalid base64: {}", e)))?;
209
210 if decoded.len() < MIN_CHALLENGE_BYTES {
212 return Err(Error::InvalidChallenge(format!(
213 "Challenge too short: {} bytes (minimum {})",
214 decoded.len(),
215 MIN_CHALLENGE_BYTES
216 )));
217 }
218
219 if chrono::Utc::now() > self.expires_at {
220 Err(Error::ChallengeExpired)
221 } else {
222 Ok(())
223 }
224 }
225}
226
227#[derive(Debug, Clone, Serialize, Deserialize)]
229#[serde(rename_all = "camelCase")]
230pub struct SigningPayload {
231 pub challenge: String,
232 pub domain: String,
233 pub signed_at: String,
234 pub verifier_id: String,
235}
236
237impl SigningPayload {
238 pub fn to_canonical_json(&self) -> crate::Result<String> {
240 serde_json::to_string(self).map_err(Into::into)
242 }
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct ErrorResponse {
248 pub error: String,
249 pub message: String,
250}