use serde::{Deserialize, Serialize};
use oas_crypto::encoding;
use oas_crypto::keypair::OasKeyPair;
use oas_crypto::CryptoError;
use crate::error::DocumentError;
pub const DOCUMENT_PROOF_TYPE: &str = "Ed25519Signature2020";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentProof {
#[serde(rename = "type")]
pub proof_type: String,
pub created: String,
pub verification_method: String,
pub proof_purpose: String,
pub proof_value: String,
}
impl DocumentProof {
pub fn generate(
document_json: &serde_json::Value,
keypair: &OasKeyPair,
verification_method_id: &str,
created: &str,
) -> Result<Self, DocumentError> {
let canonical_bytes = oas_crypto::jcs::canonicalize(document_json).map_err(|e| {
DocumentError::ProofGenerationFailed {
reason: format!("JCS canonicalization failed: {e}"),
}
})?;
let signature_bytes = keypair.sign(&canonical_bytes);
let proof_value = encoding::multibase_encode(&signature_bytes);
Ok(Self {
proof_type: DOCUMENT_PROOF_TYPE.to_string(),
created: created.to_string(),
verification_method: verification_method_id.to_string(),
proof_purpose: "assertionMethod".to_string(),
proof_value,
})
}
pub fn verify(
&self,
document_json: &serde_json::Value,
public_key_bytes: &[u8],
) -> Result<(), DocumentError> {
let canonical_bytes = oas_crypto::jcs::canonicalize(document_json).map_err(|e| {
DocumentError::ProofVerificationFailed {
reason: format!("JCS canonicalization failed: {e}"),
}
})?;
let signature_bytes = encoding::multibase_decode(&self.proof_value).map_err(|e| {
DocumentError::ProofVerificationFailed {
reason: format!("multibase decode failed: {e}"),
}
})?;
OasKeyPair::verify_with_key(public_key_bytes, &canonical_bytes, &signature_bytes).map_err(
|e: CryptoError| DocumentError::ProofVerificationFailed {
reason: format!("Ed25519 signature verification failed: {e}"),
},
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn generate_and_verify_round_trip() {
let keypair = OasKeyPair::generate();
let doc_json = serde_json::json!({
"id": "did:oas:test:hmr:alice",
"kind": "hmr",
"sequence": 1
});
let proof = DocumentProof::generate(
&doc_json,
&keypair,
"did:oas:test:hmr:alice#key-1",
"2026-01-15T00:00:00Z",
);
assert!(proof.is_ok());
let proof = proof.unwrap_or_else(|_| DocumentProof {
proof_type: String::new(),
created: String::new(),
verification_method: String::new(),
proof_purpose: String::new(),
proof_value: "z".to_string(),
});
assert_eq!(proof.proof_type, DOCUMENT_PROOF_TYPE);
assert_eq!(proof.proof_purpose, "assertionMethod");
let result = proof.verify(&doc_json, &keypair.verifying_key_bytes());
assert!(result.is_ok());
}
#[test]
fn verify_tampered_document_fails() {
let keypair = OasKeyPair::generate();
let doc_json = serde_json::json!({"id": "did:oas:test:hmr:alice", "sequence": 1});
let proof = DocumentProof::generate(
&doc_json,
&keypair,
"did:oas:test:hmr:alice#key-1",
"2026-01-15T00:00:00Z",
)
.unwrap_or_else(|_| DocumentProof {
proof_type: String::new(),
created: String::new(),
verification_method: String::new(),
proof_purpose: String::new(),
proof_value: "z".to_string(),
});
let tampered = serde_json::json!({"id": "did:oas:test:hmr:alice", "sequence": 999});
let result = proof.verify(&tampered, &keypair.verifying_key_bytes());
assert!(result.is_err());
}
#[test]
fn verify_wrong_key_fails() {
let keypair = OasKeyPair::generate();
let wrong_key = OasKeyPair::generate();
let doc_json = serde_json::json!({"id": "test", "sequence": 1});
let proof =
DocumentProof::generate(&doc_json, &keypair, "test#key-1", "2026-01-15T00:00:00Z")
.unwrap_or_else(|_| DocumentProof {
proof_type: String::new(),
created: String::new(),
verification_method: String::new(),
proof_purpose: String::new(),
proof_value: "z".to_string(),
});
let result = proof.verify(&doc_json, &wrong_key.verifying_key_bytes());
assert!(result.is_err());
}
#[test]
fn serde_round_trip() {
let keypair = OasKeyPair::generate();
let doc_json = serde_json::json!({"test": true});
let proof =
DocumentProof::generate(&doc_json, &keypair, "test#key-1", "2026-01-15T00:00:00Z")
.unwrap_or_else(|_| DocumentProof {
proof_type: String::new(),
created: String::new(),
verification_method: String::new(),
proof_purpose: String::new(),
proof_value: "z".to_string(),
});
let json = serde_json::to_string(&proof).unwrap_or_default();
let parsed: DocumentProof = serde_json::from_str(&json).unwrap_or_else(|_| proof.clone());
assert_eq!(proof, parsed);
}
}