oxirs_did/proof/
mod.rs

1//! Cryptographic proof module
2
3pub mod ed25519;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8/// Proof type
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10pub enum ProofType {
11    /// Ed25519 Signature 2020
12    #[serde(rename = "Ed25519Signature2020")]
13    Ed25519Signature2020,
14    /// Data Integrity Proof
15    #[serde(rename = "DataIntegrityProof")]
16    DataIntegrityProof,
17    /// JSON Web Signature 2020
18    #[serde(rename = "JsonWebSignature2020")]
19    JsonWebSignature2020,
20}
21
22impl ProofType {
23    pub fn as_str(&self) -> &'static str {
24        match self {
25            ProofType::Ed25519Signature2020 => "Ed25519Signature2020",
26            ProofType::DataIntegrityProof => "DataIntegrityProof",
27            ProofType::JsonWebSignature2020 => "JsonWebSignature2020",
28        }
29    }
30}
31
32/// Proof purpose
33#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
34#[serde(rename_all = "camelCase")]
35pub enum ProofPurpose {
36    /// Assertion method (for credentials)
37    AssertionMethod,
38    /// Authentication
39    Authentication,
40    /// Key agreement
41    KeyAgreement,
42    /// Capability invocation
43    CapabilityInvocation,
44    /// Capability delegation
45    CapabilityDelegation,
46}
47
48impl ProofPurpose {
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            ProofPurpose::AssertionMethod => "assertionMethod",
52            ProofPurpose::Authentication => "authentication",
53            ProofPurpose::KeyAgreement => "keyAgreement",
54            ProofPurpose::CapabilityInvocation => "capabilityInvocation",
55            ProofPurpose::CapabilityDelegation => "capabilityDelegation",
56        }
57    }
58}
59
60/// Cryptographic proof
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(rename_all = "camelCase")]
63pub struct Proof {
64    /// Proof type
65    #[serde(rename = "type")]
66    pub proof_type: String,
67
68    /// Creation timestamp
69    pub created: DateTime<Utc>,
70
71    /// Verification method (key ID)
72    pub verification_method: String,
73
74    /// Proof purpose
75    pub proof_purpose: String,
76
77    /// Proof value (signature in multibase)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub proof_value: Option<String>,
80
81    /// JWS (for JsonWebSignature2020)
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub jws: Option<String>,
84
85    /// Challenge (for authentication)
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub challenge: Option<String>,
88
89    /// Domain
90    #[serde(skip_serializing_if = "Option::is_none")]
91    pub domain: Option<String>,
92
93    /// Nonce
94    #[serde(skip_serializing_if = "Option::is_none")]
95    pub nonce: Option<String>,
96
97    /// Cryptosuite (for DataIntegrityProof)
98    #[serde(skip_serializing_if = "Option::is_none")]
99    pub cryptosuite: Option<String>,
100}
101
102impl Proof {
103    /// Create a new Ed25519Signature2020 proof
104    pub fn ed25519(verification_method: &str, purpose: ProofPurpose, signature: &[u8]) -> Self {
105        // Encode signature in multibase (base58btc with 'z' prefix)
106        let proof_value = format!("z{}", bs58::encode(signature).into_string());
107
108        Self {
109            proof_type: ProofType::Ed25519Signature2020.as_str().to_string(),
110            created: Utc::now(),
111            verification_method: verification_method.to_string(),
112            proof_purpose: purpose.as_str().to_string(),
113            proof_value: Some(proof_value),
114            jws: None,
115            challenge: None,
116            domain: None,
117            nonce: None,
118            cryptosuite: None,
119        }
120    }
121
122    /// Create a DataIntegrityProof
123    pub fn data_integrity(
124        cryptosuite: &str,
125        verification_method: &str,
126        purpose: ProofPurpose,
127        signature: &[u8],
128    ) -> Self {
129        let proof_value = format!("z{}", bs58::encode(signature).into_string());
130
131        Self {
132            proof_type: ProofType::DataIntegrityProof.as_str().to_string(),
133            created: Utc::now(),
134            verification_method: verification_method.to_string(),
135            proof_purpose: purpose.as_str().to_string(),
136            proof_value: Some(proof_value),
137            jws: None,
138            challenge: None,
139            domain: None,
140            nonce: None,
141            cryptosuite: Some(cryptosuite.to_string()),
142        }
143    }
144
145    /// Get signature bytes from proof value
146    pub fn get_signature_bytes(&self) -> crate::DidResult<Vec<u8>> {
147        if let Some(ref proof_value) = self.proof_value {
148            if let Some(stripped) = proof_value.strip_prefix('z') {
149                bs58::decode(stripped)
150                    .into_vec()
151                    .map_err(|e| crate::DidError::InvalidProof(e.to_string()))
152            } else {
153                Err(crate::DidError::InvalidProof(
154                    "Unknown proof value encoding".to_string(),
155                ))
156            }
157        } else if let Some(ref _jws) = self.jws {
158            // TODO: Parse JWS
159            Err(crate::DidError::InvalidProof(
160                "JWS parsing not yet implemented".to_string(),
161            ))
162        } else {
163            Err(crate::DidError::InvalidProof("No proof value".to_string()))
164        }
165    }
166
167    /// Set challenge (for authentication proofs)
168    pub fn with_challenge(mut self, challenge: &str) -> Self {
169        self.challenge = Some(challenge.to_string());
170        self
171    }
172
173    /// Set domain
174    pub fn with_domain(mut self, domain: &str) -> Self {
175        self.domain = Some(domain.to_string());
176        self
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn test_ed25519_proof() {
186        let signature = vec![1, 2, 3, 4, 5, 6, 7, 8];
187        let proof = Proof::ed25519(
188            "did:key:z123#key-1",
189            ProofPurpose::AssertionMethod,
190            &signature,
191        );
192
193        assert_eq!(proof.proof_type, "Ed25519Signature2020");
194        assert!(proof.proof_value.is_some());
195
196        let recovered = proof.get_signature_bytes().unwrap();
197        assert_eq!(recovered, signature);
198    }
199
200    #[test]
201    fn test_data_integrity_proof() {
202        let signature = vec![1, 2, 3, 4];
203        let proof = Proof::data_integrity(
204            "eddsa-rdfc-2022",
205            "did:key:z456#key-1",
206            ProofPurpose::Authentication,
207            &signature,
208        );
209
210        assert_eq!(proof.proof_type, "DataIntegrityProof");
211        assert_eq!(proof.cryptosuite, Some("eddsa-rdfc-2022".to_string()));
212    }
213}