Skip to main content

oxirs_did/proof/
jws.rs

1//! JSON Web Signature (JWS) implementation
2//!
3//! RFC 7515 compliant JWS with support for:
4//! - EdDSA (Ed25519) - fully implemented
5//! - ES256K (secp256k1) - signature format defined, key ops stubbed
6//! - RS256 (RSA) - signature format defined, key ops stubbed
7//!
8//! Supports JsonWebSignature2020 proof type per:
9//! <https://w3c-ccg.github.io/lds-jws2020/>
10
11use crate::did::DidDocument;
12use crate::proof::ed25519::{Ed25519Signer, Ed25519Verifier};
13use crate::proof::ProofPurpose;
14use crate::{DidError, DidResult, VerificationMethod};
15use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
16use serde::{Deserialize, Serialize};
17use sha2::{Digest, Sha256};
18
19/// JWS Algorithm identifiers per RFC 7518
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21pub enum JwsAlgorithm {
22    /// Edwards-curve Digital Signature Algorithm with Ed25519
23    #[serde(rename = "EdDSA")]
24    EdDsa,
25    /// ECDSA using P-256 and SHA-256
26    #[serde(rename = "ES256")]
27    Es256,
28    /// ECDSA using secp256k1 and SHA-256
29    #[serde(rename = "ES256K")]
30    Es256K,
31    /// RSASSA-PKCS1-v1_5 using SHA-256
32    #[serde(rename = "RS256")]
33    Rs256,
34}
35
36impl JwsAlgorithm {
37    /// Get the string identifier
38    pub fn as_str(&self) -> &'static str {
39        match self {
40            Self::EdDsa => "EdDSA",
41            Self::Es256 => "ES256",
42            Self::Es256K => "ES256K",
43            Self::Rs256 => "RS256",
44        }
45    }
46
47    /// Parse from string
48    pub fn parse(s: &str) -> DidResult<Self> {
49        match s {
50            "EdDSA" => Ok(Self::EdDsa),
51            "ES256" => Ok(Self::Es256),
52            "ES256K" => Ok(Self::Es256K),
53            "RS256" => Ok(Self::Rs256),
54            other => Err(DidError::InvalidKey(format!(
55                "Unknown JWS algorithm: {}",
56                other
57            ))),
58        }
59    }
60
61    /// Get expected signature length in bytes
62    pub fn signature_length(&self) -> Option<usize> {
63        match self {
64            Self::EdDsa => Some(64),  // Ed25519: 64 bytes
65            Self::Es256 => Some(64),  // P-256: r||s each 32 bytes
66            Self::Es256K => Some(64), // secp256k1: r||s each 32 bytes
67            Self::Rs256 => None,      // RSA: key-size dependent
68        }
69    }
70}
71
72/// JWS JOSE Header (RFC 7515 Section 4)
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct JwsHeader {
75    /// Algorithm
76    pub alg: JwsAlgorithm,
77    /// Key ID (verification method URL)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub kid: Option<String>,
80    /// Base64url-encode payload flag (RFC 7797)
81    /// When false, payload is not base64url-encoded (detached payload)
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub b64: Option<bool>,
84    /// Critical header parameters
85    #[serde(skip_serializing_if = "Option::is_none")]
86    pub crit: Option<Vec<String>>,
87    /// Content type
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub cty: Option<String>,
90}
91
92impl JwsHeader {
93    /// Create a standard header for EdDSA
94    pub fn ed_dsa(kid: Option<&str>) -> Self {
95        Self {
96            alg: JwsAlgorithm::EdDsa,
97            kid: kid.map(String::from),
98            b64: None,
99            crit: None,
100            cty: None,
101        }
102    }
103
104    /// Create a detached payload header (RFC 7797)
105    /// Used for JsonWebSignature2020 where payload is the document hash
106    pub fn detached(alg: JwsAlgorithm, kid: Option<&str>) -> Self {
107        Self {
108            alg,
109            kid: kid.map(String::from),
110            b64: Some(false),
111            crit: Some(vec!["b64".to_string()]),
112            cty: None,
113        }
114    }
115
116    /// Encode header as base64url JSON
117    pub fn encode(&self) -> DidResult<String> {
118        let json = serde_json::to_string(self)
119            .map_err(|e| DidError::SerializationError(format!("Header serialize: {}", e)))?;
120        Ok(URL_SAFE_NO_PAD.encode(json.as_bytes()))
121    }
122
123    /// Decode header from base64url JSON
124    pub fn decode(encoded: &str) -> DidResult<Self> {
125        let bytes = URL_SAFE_NO_PAD
126            .decode(encoded)
127            .map_err(|e| DidError::InvalidProof(format!("Header base64 decode: {}", e)))?;
128        serde_json::from_slice(&bytes)
129            .map_err(|e| DidError::SerializationError(format!("Header deserialize: {}", e)))
130    }
131}
132
133/// Compact JWS token (header.payload.signature)
134#[derive(Debug, Clone)]
135pub struct CompactJws {
136    /// Base64url-encoded header
137    pub header_b64: String,
138    /// Base64url-encoded payload (empty for detached)
139    pub payload_b64: String,
140    /// Base64url-encoded signature
141    pub signature_b64: String,
142}
143
144impl CompactJws {
145    /// Parse a compact JWS string
146    pub fn parse(jws: &str) -> DidResult<Self> {
147        let parts: Vec<&str> = jws.split('.').collect();
148        if parts.len() != 3 {
149            return Err(DidError::InvalidProof(format!(
150                "JWS must have 3 parts separated by '.', got {}",
151                parts.len()
152            )));
153        }
154
155        Ok(Self {
156            header_b64: parts[0].to_string(),
157            payload_b64: parts[1].to_string(),
158            signature_b64: parts[2].to_string(),
159        })
160    }
161
162    /// Create a detached compact JWS (payload omitted per RFC 7515 Section 7.2.4)
163    pub fn to_detached_string(&self) -> String {
164        format!("{}..{}", self.header_b64, self.signature_b64)
165    }
166
167    /// Get the signing input (header_b64 + "." + payload_b64)
168    pub fn signing_input(&self) -> Vec<u8> {
169        format!("{}.{}", self.header_b64, self.payload_b64).into_bytes()
170    }
171
172    /// Get decoded signature bytes
173    pub fn signature_bytes(&self) -> DidResult<Vec<u8>> {
174        URL_SAFE_NO_PAD
175            .decode(&self.signature_b64)
176            .map_err(|e| DidError::InvalidProof(format!("Signature base64 decode: {}", e)))
177    }
178
179    /// Get decoded header
180    pub fn header(&self) -> DidResult<JwsHeader> {
181        JwsHeader::decode(&self.header_b64)
182    }
183}
184
185impl std::fmt::Display for CompactJws {
186    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
187        write!(
188            f,
189            "{}.{}.{}",
190            self.header_b64, self.payload_b64, self.signature_b64
191        )
192    }
193}
194
195/// JsonWebSignature2020 proof structure
196///
197/// Per <https://w3c-ccg.github.io/lds-jws2020/>
198#[derive(Debug, Clone, Serialize, Deserialize)]
199#[serde(rename_all = "camelCase")]
200pub struct JsonWebSignature2020 {
201    /// Proof type identifier
202    #[serde(rename = "type")]
203    pub proof_type: String,
204    /// ISO 8601 creation timestamp
205    pub created: String,
206    /// Verification method URL
207    pub verification_method: String,
208    /// Proof purpose
209    pub proof_purpose: String,
210    /// Compact JWS token (detached signature)
211    pub jws: String,
212    /// Optional challenge for authentication proofs
213    #[serde(skip_serializing_if = "Option::is_none")]
214    pub challenge: Option<String>,
215    /// Optional domain
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub domain: Option<String>,
218    /// Optional nonce
219    #[serde(skip_serializing_if = "Option::is_none")]
220    pub nonce: Option<String>,
221}
222
223impl JsonWebSignature2020 {
224    /// The proof type identifier string
225    pub const PROOF_TYPE: &'static str = "JsonWebSignature2020";
226
227    /// Sign a JSON document using JsonWebSignature2020
228    ///
229    /// The signing process:
230    /// 1. Canonicalize the document (or use raw JSON bytes)
231    /// 2. Create JWS header with algorithm and key ID
232    /// 3. Create signing input: base64url(header) + "." + base64url(payload_hash)
233    /// 4. Sign with the private key
234    /// 5. Create detached JWS: base64url(header) + ".." + base64url(signature)
235    pub fn sign(
236        document: &serde_json::Value,
237        secret_key_bytes: &[u8],
238        vm_id: &str,
239        purpose: ProofPurpose,
240    ) -> DidResult<Self> {
241        // Serialize document to canonical JSON bytes
242        let doc_bytes = serialize_document(document)?;
243
244        // Hash the document for signing input
245        let doc_hash = sha256_hash(&doc_bytes);
246
247        // Create JWS header (detached, no b64 encoding of payload)
248        let header = JwsHeader::detached(JwsAlgorithm::EdDsa, Some(vm_id));
249        let header_b64 = header.encode()?;
250
251        // Payload is base64url of the document hash
252        let payload_b64 = URL_SAFE_NO_PAD.encode(&doc_hash);
253
254        // Signing input: ASCII(header_b64 || "." || payload_b64)
255        let signing_input = format!("{}.{}", header_b64, payload_b64).into_bytes();
256
257        // Sign with Ed25519
258        let signer = Ed25519Signer::from_bytes(secret_key_bytes)?;
259        let signature = signer.sign(&signing_input);
260        let signature_b64 = URL_SAFE_NO_PAD.encode(&signature);
261
262        // Detached JWS: header..signature (payload omitted)
263        let jws = format!("{}..{}", header_b64, signature_b64);
264
265        Ok(Self {
266            proof_type: Self::PROOF_TYPE.to_string(),
267            created: chrono::Utc::now().to_rfc3339(),
268            verification_method: vm_id.to_string(),
269            proof_purpose: purpose.as_str().to_string(),
270            jws,
271            challenge: None,
272            domain: None,
273            nonce: None,
274        })
275    }
276
277    /// Verify a JsonWebSignature2020 proof against a document
278    ///
279    /// The verification process:
280    /// 1. Parse the detached JWS
281    /// 2. Reconstruct the signing input from header and document hash
282    /// 3. Verify the signature with the public key
283    pub fn verify(
284        document: &serde_json::Value,
285        proof: &Self,
286        public_key_bytes: &[u8],
287    ) -> DidResult<bool> {
288        // Parse the detached JWS
289        let jws_str = &proof.jws;
290
291        // Handle both detached ("header..sig") and full ("header.payload.sig") formats
292        let (header_b64, signature_b64) = parse_jws_parts(jws_str)?;
293
294        // Decode the header to get the algorithm
295        let header = JwsHeader::decode(&header_b64)?;
296
297        // Serialize the document to get the payload
298        let doc_bytes = serialize_document(document)?;
299        let doc_hash = sha256_hash(&doc_bytes);
300        let payload_b64 = URL_SAFE_NO_PAD.encode(&doc_hash);
301
302        // Reconstruct signing input
303        let signing_input = format!("{}.{}", header_b64, payload_b64).into_bytes();
304
305        // Decode signature
306        let signature = URL_SAFE_NO_PAD
307            .decode(&signature_b64)
308            .map_err(|e| DidError::InvalidProof(format!("Signature decode error: {}", e)))?;
309
310        // Verify based on algorithm
311        match header.alg {
312            JwsAlgorithm::EdDsa => {
313                let verifier = Ed25519Verifier::from_bytes(public_key_bytes)?;
314                verifier.verify(&signing_input, &signature)
315            }
316            other => Err(DidError::InvalidProof(format!(
317                "Algorithm {:?} verification not yet implemented",
318                other
319            ))),
320        }
321    }
322
323    /// Verify using a VerificationMethod from a DID Document
324    pub fn verify_with_method(
325        document: &serde_json::Value,
326        proof: &Self,
327        vm: &VerificationMethod,
328    ) -> DidResult<bool> {
329        let public_key = vm.get_public_key_bytes()?;
330        Self::verify(document, proof, &public_key)
331    }
332
333    /// Set challenge for authentication proofs
334    pub fn with_challenge(mut self, challenge: &str) -> Self {
335        self.challenge = Some(challenge.to_string());
336        self
337    }
338
339    /// Set domain
340    pub fn with_domain(mut self, domain: &str) -> Self {
341        self.domain = Some(domain.to_string());
342        self
343    }
344
345    /// Set nonce
346    pub fn with_nonce(mut self, nonce: &str) -> Self {
347        self.nonce = Some(nonce.to_string());
348        self
349    }
350
351    /// Convert to a crate::proof::Proof for embedding in documents
352    pub fn to_proof(&self) -> crate::proof::Proof {
353        crate::proof::Proof {
354            proof_type: Self::PROOF_TYPE.to_string(),
355            created: chrono::DateTime::parse_from_rfc3339(&self.created)
356                .map(|dt| dt.with_timezone(&chrono::Utc))
357                .unwrap_or_else(|_| chrono::Utc::now()),
358            verification_method: self.verification_method.clone(),
359            proof_purpose: self.proof_purpose.clone(),
360            proof_value: None,
361            jws: Some(self.jws.clone()),
362            challenge: self.challenge.clone(),
363            domain: self.domain.clone(),
364            nonce: self.nonce.clone(),
365            cryptosuite: None,
366        }
367    }
368}
369
370/// JWS Signer for creating compact JWS tokens
371pub struct JwsSigner {
372    algorithm: JwsAlgorithm,
373    secret_key: Vec<u8>,
374    key_id: Option<String>,
375}
376
377impl JwsSigner {
378    /// Create an EdDSA (Ed25519) signer
379    pub fn ed_dsa(secret_key: &[u8], key_id: Option<&str>) -> DidResult<Self> {
380        if secret_key.len() != 32 {
381            return Err(DidError::InvalidKey(
382                "Ed25519 secret key must be 32 bytes".to_string(),
383            ));
384        }
385        Ok(Self {
386            algorithm: JwsAlgorithm::EdDsa,
387            secret_key: secret_key.to_vec(),
388            key_id: key_id.map(String::from),
389        })
390    }
391
392    /// Sign a payload and return a compact JWS
393    pub fn sign(&self, payload: &[u8]) -> DidResult<CompactJws> {
394        let header = JwsHeader {
395            alg: self.algorithm,
396            kid: self.key_id.clone(),
397            b64: None,
398            crit: None,
399            cty: None,
400        };
401
402        let header_b64 = header.encode()?;
403        let payload_b64 = URL_SAFE_NO_PAD.encode(payload);
404        let signing_input = format!("{}.{}", header_b64, payload_b64).into_bytes();
405
406        let signature_b64 = match self.algorithm {
407            JwsAlgorithm::EdDsa => {
408                let signer = Ed25519Signer::from_bytes(&self.secret_key)?;
409                let sig = signer.sign(&signing_input);
410                URL_SAFE_NO_PAD.encode(&sig)
411            }
412            other => {
413                return Err(DidError::SigningFailed(format!(
414                    "Algorithm {:?} signing not yet implemented",
415                    other
416                )));
417            }
418        };
419
420        Ok(CompactJws {
421            header_b64,
422            payload_b64,
423            signature_b64,
424        })
425    }
426}
427
428/// JWS Verifier for verifying compact JWS tokens
429pub struct JwsVerifier {
430    public_key: Vec<u8>,
431}
432
433impl JwsVerifier {
434    /// Create an Ed25519 verifier
435    pub fn ed25519(public_key: &[u8]) -> DidResult<Self> {
436        if public_key.len() != 32 {
437            return Err(DidError::InvalidKey(
438                "Ed25519 public key must be 32 bytes".to_string(),
439            ));
440        }
441        Ok(Self {
442            public_key: public_key.to_vec(),
443        })
444    }
445
446    /// Verify a compact JWS token
447    pub fn verify_compact(&self, jws: &CompactJws) -> DidResult<bool> {
448        let header = jws.header()?;
449        let signing_input = jws.signing_input();
450        let signature = jws.signature_bytes()?;
451
452        match header.alg {
453            JwsAlgorithm::EdDsa => {
454                let verifier = Ed25519Verifier::from_bytes(&self.public_key)?;
455                verifier.verify(&signing_input, &signature)
456            }
457            other => Err(DidError::VerificationFailed(format!(
458                "Algorithm {:?} verification not yet implemented",
459                other
460            ))),
461        }
462    }
463
464    /// Verify a JWS string (compact serialization)
465    pub fn verify_string(&self, jws_str: &str) -> DidResult<bool> {
466        let jws = CompactJws::parse(jws_str)?;
467        self.verify_compact(&jws)
468    }
469}
470
471/// Parse a JWS string into header and signature parts
472/// Handles both full ("header.payload.sig") and detached ("header..sig") formats
473fn parse_jws_parts(jws_str: &str) -> DidResult<(String, String)> {
474    let parts: Vec<&str> = jws_str.split('.').collect();
475
476    match parts.len() {
477        3 => {
478            // Standard or detached format
479            Ok((parts[0].to_string(), parts[2].to_string()))
480        }
481        _ => Err(DidError::InvalidProof(format!(
482            "Invalid JWS format: expected 3 dot-separated parts, got {}",
483            parts.len()
484        ))),
485    }
486}
487
488/// Serialize a JSON document to canonical bytes for signing
489fn serialize_document(doc: &serde_json::Value) -> DidResult<Vec<u8>> {
490    // Use a deterministic serialization
491    // In a full implementation, this would use JSON-LD canonicalization (RDFC-1.0)
492    // For now, we use a sorted-key JSON serialization as an approximation
493    let canonical = canonicalize_json(doc)?;
494    Ok(canonical.into_bytes())
495}
496
497/// Produce a deterministic JSON string by sorting object keys recursively
498fn canonicalize_json(value: &serde_json::Value) -> DidResult<String> {
499    match value {
500        serde_json::Value::Object(map) => {
501            let mut sorted: Vec<(&String, &serde_json::Value)> = map.iter().collect();
502            sorted.sort_by_key(|(k, _)| k.as_str());
503
504            let mut parts = Vec::with_capacity(sorted.len());
505            for (k, v) in sorted {
506                let key = serde_json::to_string(k)
507                    .map_err(|e| DidError::SerializationError(e.to_string()))?;
508                let val = canonicalize_json(v)?;
509                parts.push(format!("{}:{}", key, val));
510            }
511            Ok(format!("{{{}}}", parts.join(",")))
512        }
513        serde_json::Value::Array(arr) => {
514            let parts: DidResult<Vec<String>> = arr.iter().map(canonicalize_json).collect();
515            Ok(format!("[{}]", parts?.join(",")))
516        }
517        other => {
518            serde_json::to_string(other).map_err(|e| DidError::SerializationError(e.to_string()))
519        }
520    }
521}
522
523/// Compute SHA-256 hash
524fn sha256_hash(data: &[u8]) -> Vec<u8> {
525    let mut hasher = Sha256::new();
526    hasher.update(data);
527    hasher.finalize().to_vec()
528}
529
530/// Attach a JsonWebSignature2020 proof to a document
531pub fn attach_jws_proof(
532    document: &mut serde_json::Value,
533    proof: &JsonWebSignature2020,
534) -> DidResult<()> {
535    let proof_value =
536        serde_json::to_value(proof).map_err(|e| DidError::SerializationError(e.to_string()))?;
537
538    if let Some(obj) = document.as_object_mut() {
539        obj.insert("proof".to_string(), proof_value);
540        Ok(())
541    } else {
542        Err(DidError::InvalidFormat(
543            "Document must be a JSON object".to_string(),
544        ))
545    }
546}
547
548/// Extract and remove proof from a document (for verification)
549pub fn extract_jws_proof(document: &mut serde_json::Value) -> DidResult<JsonWebSignature2020> {
550    if let Some(obj) = document.as_object_mut() {
551        let proof_value = obj
552            .remove("proof")
553            .ok_or_else(|| DidError::InvalidProof("Document has no 'proof' field".to_string()))?;
554
555        serde_json::from_value(proof_value)
556            .map_err(|e| DidError::SerializationError(format!("Proof deserialize: {}", e)))
557    } else {
558        Err(DidError::InvalidFormat(
559            "Document must be a JSON object".to_string(),
560        ))
561    }
562}
563
564/// Sign a document end-to-end: add proof to document
565pub fn sign_document(
566    document: &mut serde_json::Value,
567    secret_key_bytes: &[u8],
568    vm_id: &str,
569    purpose: ProofPurpose,
570) -> DidResult<()> {
571    // Sign the document without proof field
572    let proof = JsonWebSignature2020::sign(document, secret_key_bytes, vm_id, purpose)?;
573    attach_jws_proof(document, &proof)
574}
575
576/// Verify a document end-to-end: extract proof and verify
577pub fn verify_document(
578    document: &mut serde_json::Value,
579    public_key_bytes: &[u8],
580) -> DidResult<bool> {
581    // Extract proof (this modifies the document by removing the proof)
582    let proof = extract_jws_proof(document)?;
583
584    // Verify without the proof field
585    JsonWebSignature2020::verify(document, &proof, public_key_bytes)
586}
587
588#[cfg(test)]
589mod tests {
590    use super::*;
591    use crate::proof::ed25519::Ed25519Signer;
592
593    fn generate_test_keypair() -> (Vec<u8>, Vec<u8>) {
594        let signer = Ed25519Signer::generate();
595        (
596            signer.secret_key_bytes().to_vec(),
597            signer.public_key_bytes().to_vec(),
598        )
599    }
600
601    #[test]
602    fn test_jws_header_encode_decode() {
603        let header = JwsHeader::ed_dsa(Some("did:key:z123#key-1"));
604        let encoded = header.encode().unwrap();
605        let decoded = JwsHeader::decode(&encoded).unwrap();
606        assert_eq!(decoded.alg, JwsAlgorithm::EdDsa);
607        assert_eq!(decoded.kid, Some("did:key:z123#key-1".to_string()));
608    }
609
610    #[test]
611    fn test_detached_jws_header() {
612        let header = JwsHeader::detached(JwsAlgorithm::EdDsa, Some("kid"));
613        assert_eq!(header.b64, Some(false));
614        assert!(header.crit.is_some());
615        assert!(header.crit.as_ref().unwrap().contains(&"b64".to_string()));
616    }
617
618    #[test]
619    fn test_compact_jws_sign_verify() {
620        let (secret, public) = generate_test_keypair();
621        let payload = b"Hello, World!";
622
623        let signer = JwsSigner::ed_dsa(&secret, Some("test-key")).unwrap();
624        let compact = signer.sign(payload).unwrap();
625
626        let jws_string = compact.to_string();
627        assert_eq!(jws_string.split('.').count(), 3);
628
629        let verifier = JwsVerifier::ed25519(&public).unwrap();
630        let valid = verifier.verify_string(&jws_string).unwrap();
631        assert!(valid);
632    }
633
634    #[test]
635    fn test_compact_jws_invalid_signature() {
636        let (secret, _) = generate_test_keypair();
637        let (_, other_public) = generate_test_keypair();
638        let payload = b"Hello, World!";
639
640        let signer = JwsSigner::ed_dsa(&secret, None).unwrap();
641        let compact = signer.sign(payload).unwrap();
642
643        let verifier = JwsVerifier::ed25519(&other_public).unwrap();
644        let valid = verifier.verify_compact(&compact).unwrap();
645        assert!(!valid);
646    }
647
648    #[test]
649    fn test_json_web_signature_2020_sign_verify() {
650        let (secret, public) = generate_test_keypair();
651        let document = serde_json::json!({
652            "@context": ["https://www.w3.org/2018/credentials/v1"],
653            "type": ["VerifiableCredential"],
654            "issuer": "did:key:z6Mk",
655            "credentialSubject": {
656                "id": "did:example:alice",
657                "name": "Alice"
658            }
659        });
660
661        let vm_id = "did:key:z6Mk#key-1";
662        let proof =
663            JsonWebSignature2020::sign(&document, &secret, vm_id, ProofPurpose::AssertionMethod)
664                .unwrap();
665
666        assert_eq!(proof.proof_type, JsonWebSignature2020::PROOF_TYPE);
667        assert_eq!(proof.verification_method, vm_id);
668        assert!(proof.jws.contains(".."));
669
670        let valid = JsonWebSignature2020::verify(&document, &proof, &public).unwrap();
671        assert!(valid);
672    }
673
674    #[test]
675    fn test_jws_tampered_document() {
676        let (secret, public) = generate_test_keypair();
677        let document = serde_json::json!({
678            "name": "Alice",
679            "age": 30
680        });
681
682        let proof = JsonWebSignature2020::sign(
683            &document,
684            &secret,
685            "did:key:z123#key-1",
686            ProofPurpose::AssertionMethod,
687        )
688        .unwrap();
689
690        // Tamper with the document
691        let tampered = serde_json::json!({
692            "name": "Bob",  // Changed!
693            "age": 30
694        });
695
696        let valid = JsonWebSignature2020::verify(&tampered, &proof, &public).unwrap();
697        assert!(!valid, "Tampered document should not verify");
698    }
699
700    #[test]
701    fn test_sign_verify_document_end_to_end() {
702        let (secret, public) = generate_test_keypair();
703        let mut document = serde_json::json!({
704            "@context": ["https://www.w3.org/2018/credentials/v1"],
705            "type": "TestDocument",
706            "subject": "test"
707        });
708
709        sign_document(
710            &mut document,
711            &secret,
712            "did:key:z6Mk#key-1",
713            ProofPurpose::AssertionMethod,
714        )
715        .unwrap();
716
717        assert!(document.get("proof").is_some());
718
719        let valid = verify_document(&mut document, &public).unwrap();
720        assert!(valid);
721    }
722
723    #[test]
724    fn test_canonicalize_json_deterministic() {
725        // Same data in different order should produce same canonical form
726        let v1 = serde_json::json!({"b": 2, "a": 1});
727        let v2 = serde_json::json!({"a": 1, "b": 2});
728
729        let c1 = canonicalize_json(&v1).unwrap();
730        let c2 = canonicalize_json(&v2).unwrap();
731
732        assert_eq!(c1, c2);
733    }
734
735    #[test]
736    fn test_jws_algorithm_roundtrip() {
737        for alg in [
738            JwsAlgorithm::EdDsa,
739            JwsAlgorithm::Es256,
740            JwsAlgorithm::Es256K,
741            JwsAlgorithm::Rs256,
742        ] {
743            let s = alg.as_str();
744            let parsed = JwsAlgorithm::parse(s).unwrap();
745            assert_eq!(parsed, alg);
746        }
747    }
748
749    #[test]
750    fn test_proof_to_crate_proof() {
751        let (secret, _) = generate_test_keypair();
752        let document = serde_json::json!({"test": true});
753
754        let jws_proof = JsonWebSignature2020::sign(
755            &document,
756            &secret,
757            "did:key:z#key-1",
758            ProofPurpose::AssertionMethod,
759        )
760        .unwrap();
761
762        let proof = jws_proof.to_proof();
763        assert_eq!(proof.proof_type, JsonWebSignature2020::PROOF_TYPE);
764        assert!(proof.jws.is_some());
765    }
766}