oas_document/
proof_format.rs1use serde::{Deserialize, Serialize};
8
9use oas_crypto::encoding;
10use oas_crypto::keypair::OasKeyPair;
11use oas_crypto::CryptoError;
12
13use crate::error::DocumentError;
14
15pub const DOCUMENT_PROOF_TYPE: &str = "Ed25519Signature2020";
17
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct DocumentProof {
24 #[serde(rename = "type")]
26 pub proof_type: String,
27
28 pub created: String,
30
31 pub verification_method: String,
33
34 pub proof_purpose: String,
36
37 pub proof_value: String,
39}
40
41impl DocumentProof {
42 pub fn generate(
64 document_json: &serde_json::Value,
65 keypair: &OasKeyPair,
66 verification_method_id: &str,
67 created: &str,
68 ) -> Result<Self, DocumentError> {
69 let canonical_bytes = oas_crypto::jcs::canonicalize(document_json).map_err(|e| {
70 DocumentError::ProofGenerationFailed {
71 reason: format!("JCS canonicalization failed: {e}"),
72 }
73 })?;
74
75 let signature_bytes = keypair.sign(&canonical_bytes);
76 let proof_value = encoding::multibase_encode(&signature_bytes);
77
78 Ok(Self {
79 proof_type: DOCUMENT_PROOF_TYPE.to_string(),
80 created: created.to_string(),
81 verification_method: verification_method_id.to_string(),
82 proof_purpose: "assertionMethod".to_string(),
83 proof_value,
84 })
85 }
86
87 pub fn verify(
107 &self,
108 document_json: &serde_json::Value,
109 public_key_bytes: &[u8],
110 ) -> Result<(), DocumentError> {
111 let canonical_bytes = oas_crypto::jcs::canonicalize(document_json).map_err(|e| {
112 DocumentError::ProofVerificationFailed {
113 reason: format!("JCS canonicalization failed: {e}"),
114 }
115 })?;
116
117 let signature_bytes = encoding::multibase_decode(&self.proof_value).map_err(|e| {
118 DocumentError::ProofVerificationFailed {
119 reason: format!("multibase decode failed: {e}"),
120 }
121 })?;
122
123 OasKeyPair::verify_with_key(public_key_bytes, &canonical_bytes, &signature_bytes).map_err(
124 |e: CryptoError| DocumentError::ProofVerificationFailed {
125 reason: format!("Ed25519 signature verification failed: {e}"),
126 },
127 )
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn generate_and_verify_round_trip() {
137 let keypair = OasKeyPair::generate();
138 let doc_json = serde_json::json!({
139 "id": "did:oas:test:hmr:alice",
140 "kind": "hmr",
141 "sequence": 1
142 });
143
144 let proof = DocumentProof::generate(
145 &doc_json,
146 &keypair,
147 "did:oas:test:hmr:alice#key-1",
148 "2026-01-15T00:00:00Z",
149 );
150 assert!(proof.is_ok());
151 let proof = proof.unwrap_or_else(|_| DocumentProof {
152 proof_type: String::new(),
153 created: String::new(),
154 verification_method: String::new(),
155 proof_purpose: String::new(),
156 proof_value: "z".to_string(),
157 });
158
159 assert_eq!(proof.proof_type, DOCUMENT_PROOF_TYPE);
160 assert_eq!(proof.proof_purpose, "assertionMethod");
161
162 let result = proof.verify(&doc_json, &keypair.verifying_key_bytes());
163 assert!(result.is_ok());
164 }
165
166 #[test]
167 fn verify_tampered_document_fails() {
168 let keypair = OasKeyPair::generate();
169 let doc_json = serde_json::json!({"id": "did:oas:test:hmr:alice", "sequence": 1});
170
171 let proof = DocumentProof::generate(
172 &doc_json,
173 &keypair,
174 "did:oas:test:hmr:alice#key-1",
175 "2026-01-15T00:00:00Z",
176 )
177 .unwrap_or_else(|_| DocumentProof {
178 proof_type: String::new(),
179 created: String::new(),
180 verification_method: String::new(),
181 proof_purpose: String::new(),
182 proof_value: "z".to_string(),
183 });
184
185 let tampered = serde_json::json!({"id": "did:oas:test:hmr:alice", "sequence": 999});
186 let result = proof.verify(&tampered, &keypair.verifying_key_bytes());
187 assert!(result.is_err());
188 }
189
190 #[test]
191 fn verify_wrong_key_fails() {
192 let keypair = OasKeyPair::generate();
193 let wrong_key = OasKeyPair::generate();
194 let doc_json = serde_json::json!({"id": "test", "sequence": 1});
195
196 let proof =
197 DocumentProof::generate(&doc_json, &keypair, "test#key-1", "2026-01-15T00:00:00Z")
198 .unwrap_or_else(|_| DocumentProof {
199 proof_type: String::new(),
200 created: String::new(),
201 verification_method: String::new(),
202 proof_purpose: String::new(),
203 proof_value: "z".to_string(),
204 });
205
206 let result = proof.verify(&doc_json, &wrong_key.verifying_key_bytes());
207 assert!(result.is_err());
208 }
209
210 #[test]
211 fn serde_round_trip() {
212 let keypair = OasKeyPair::generate();
213 let doc_json = serde_json::json!({"test": true});
214 let proof =
215 DocumentProof::generate(&doc_json, &keypair, "test#key-1", "2026-01-15T00:00:00Z")
216 .unwrap_or_else(|_| DocumentProof {
217 proof_type: String::new(),
218 created: String::new(),
219 verification_method: String::new(),
220 proof_purpose: String::new(),
221 proof_value: "z".to_string(),
222 });
223
224 let json = serde_json::to_string(&proof).unwrap_or_default();
225 let parsed: DocumentProof = serde_json::from_str(&json).unwrap_or_else(|_| proof.clone());
226 assert_eq!(proof, parsed);
227 }
228}