Skip to main content

libdelve/
types.rs

1//! Core types for the DelVe protocol
2
3use 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
13/// Protocol version
14pub const PROTOCOL_VERSION: &str = "delve0.1";
15
16/// Verification mode
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub enum VerificationMode {
20    Delegate,
21    Direct,
22}
23
24/// DNS record configuration
25#[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    /// Format as a DNS TXT record value
35    ///
36    /// Returns a string in the format: `v=delve0.1; mode=delegate; endpoint=https://...; key=<base64>`
37    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/// Challenge request to delegate service
60#[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    /// Create a signing payload for this challenge request
91    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        // Check minimum size (timestamp + separator + 32 random bytes)
114        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/// Challenge response from delegate service
131#[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/// Token response from delegate service
144#[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/// Status of a verification request
162#[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/// Verification token with signature
171#[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    /// Create a new verification token from a challenge request
186    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        // Check minimum size (timestamp + separator + 32 random bytes)
211        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/// Canonical signing payload
228#[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    /// Create canonical JSON representation for signing
239    pub fn to_canonical_json(&self) -> crate::Result<String> {
240        // Serialize with keys in alphabetical order (serde_json maintains field order)
241        serde_json::to_string(self).map_err(Into::into)
242    }
243}
244
245/// Error response from delegate service
246#[derive(Debug, Clone, Serialize, Deserialize)]
247pub struct ErrorResponse {
248    pub error: String,
249    pub message: String,
250}