use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use crate::{
challenge::{generate_challenge, MIN_CHALLENGE_BYTES},
crypto, Error, Result,
};
pub const PROTOCOL_VERSION: &str = "delve0.1";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum VerificationMode {
Delegate,
Direct,
}
#[derive(Debug, Clone)]
pub struct DnsConfig {
pub version: String,
pub mode: VerificationMode,
pub endpoint: Option<String>,
pub public_key: String,
}
impl DnsConfig {
pub fn to_dns_record(&self) -> String {
let mut parts = vec![
format!("v={}", self.version),
format!(
"mode={}",
match self.mode {
VerificationMode::Delegate => "delegate",
VerificationMode::Direct => "direct",
}
),
];
if let Some(ref endpoint) = self.endpoint {
parts.push(format!("endpoint={}", endpoint));
}
parts.push(format!("key={}", self.public_key));
parts.join("; ")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChallengeRequest {
pub domain: String,
pub verifier_id: String,
pub challenge: String,
pub expires_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<HashMap<String, serde_json::Value>>,
}
impl ChallengeRequest {
pub fn new(domain: &str, verifier_id: &str, expires_in: Duration) -> Result<Self> {
let (ch, exp) = generate_challenge(expires_in)?;
Ok(ChallengeRequest {
domain: domain.to_string(),
verifier_id: verifier_id.to_string(),
challenge: ch,
expires_at: exp,
metadata: None,
})
}
pub fn with_meta(self, metadata: HashMap<String, serde_json::Value>) -> Self {
Self {
metadata: Some(metadata),
..self
}
}
pub fn create_signing_payload(&self, signed_at: DateTime<Utc>) -> SigningPayload {
SigningPayload {
challenge: self.challenge.clone(),
domain: self.domain.clone(),
signed_at: signed_at.to_rfc3339(),
verifier_id: self.verifier_id.clone(),
}
}
pub fn sign_payload(&self, signed_at: DateTime<Utc>, private_key: &str) -> Result<String> {
crypto::sign_payload(private_key, &self.create_signing_payload(signed_at))
}
pub fn is_expired(&self) -> bool {
chrono::Utc::now() > self.expires_at
}
pub fn validate(&self) -> Result<()> {
let decoded = BASE64
.decode(&self.challenge)
.map_err(|e| Error::InvalidChallenge(format!("Invalid base64: {}", e)))?;
if decoded.len() < MIN_CHALLENGE_BYTES {
return Err(Error::InvalidChallenge(format!(
"Challenge too short: {} bytes (minimum {})",
decoded.len(),
MIN_CHALLENGE_BYTES
)));
}
if chrono::Utc::now() > self.expires_at {
Err(Error::ChallengeExpired)
} else {
Ok(())
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChallengeResponse {
pub request_id: String,
pub status: RequestStatus,
#[serde(skip_serializing_if = "Option::is_none")]
pub authorization_url: Option<String>,
pub expires_at: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<VerificationToken>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "camelCase")]
pub enum TokenResponse {
Authorized {
request_id: String,
token: VerificationToken,
},
Pending {
request_id: String,
authorization_url: String,
},
Rejected {
request_id: String,
rejected_at: DateTime<Utc>,
},
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum RequestStatus {
Pending,
Authorized,
Rejected,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerificationToken {
pub domain: String,
pub verifier_id: String,
pub challenge: String,
pub signature: String,
pub public_key: String,
pub key_id: String,
pub signed_at: DateTime<Utc>,
pub expires_at: DateTime<Utc>,
}
impl VerificationToken {
pub fn new(
request: &ChallengeRequest,
signature: String,
public_key: String,
key_id: String,
signed_at: DateTime<Utc>,
) -> Self {
Self {
domain: request.domain.clone(),
verifier_id: request.verifier_id.clone(),
challenge: request.challenge.clone(),
signature,
public_key,
key_id,
signed_at,
expires_at: request.expires_at,
}
}
pub fn validate(&self) -> Result<()> {
let decoded = BASE64
.decode(&self.challenge)
.map_err(|e| Error::InvalidChallenge(format!("Invalid base64: {}", e)))?;
if decoded.len() < MIN_CHALLENGE_BYTES {
return Err(Error::InvalidChallenge(format!(
"Challenge too short: {} bytes (minimum {})",
decoded.len(),
MIN_CHALLENGE_BYTES
)));
}
if chrono::Utc::now() > self.expires_at {
Err(Error::ChallengeExpired)
} else {
Ok(())
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SigningPayload {
pub challenge: String,
pub domain: String,
pub signed_at: String,
pub verifier_id: String,
}
impl SigningPayload {
pub fn to_canonical_json(&self) -> crate::Result<String> {
serde_json::to_string(self).map_err(Into::into)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorResponse {
pub error: String,
pub message: String,
}