did_utils/proof/
eddsa_jcs_2022.rs

1use multibase::Base;
2
3use crate::crypto::{sha256_hash, CoreSign, Ed25519KeyPair, Error};
4
5use super::{model::Proof, traits::CryptoProof};
6
7pub const CRYPRO_SUITE_EDDSA_JCS_2022: &str = "eddsa-jcs-2022";
8pub const PROOF_TYPE_DATA_INTEGRITY_PROOF: &str = "DataIntegrityProof";
9
10pub struct EdDsaJcs2022 {
11    /// The proof object
12    ///
13    /// In a proof creation process, it does not contain the proof value, but
14    ///   carries info like challenge, nonce, etc.
15    ///
16    /// In a proof verification process, it contains the proof as found in the
17    ///   secured document, including the proof value
18    pub proof: Proof,
19
20    /// The keypair used to sreate the proof: in which case the signing key must be present.
21    ///
22    /// The keypair used to verify the proof: in which case only the public key must be present.
23    ///
24    /// This module does not perform resolution of the verification method. Module assumes calles
25    /// extracted the public key prior to calling this module.
26    pub key_pair: Ed25519KeyPair,
27
28    /// The proof value codec. This is important for the encoding of the proof.
29    ///
30    /// For the decoding, codec is automaticaly infered from the string.
31    pub proof_value_codec: Option<Base>,
32}
33
34impl CryptoProof for EdDsaJcs2022 {
35    fn proof(&self, payload: serde_json::Value) -> Result<Proof, Error> {
36        match self.proof_value_codec {
37            None => Err(Error::InvalidCall("proof_value_codec must be set for proof creation".to_string())),
38            Some(_) => {
39                let normalized_proof = Proof {
40                    proof_type: PROOF_TYPE_DATA_INTEGRITY_PROOF.to_string(),
41                    cryptosuite: Some(CRYPRO_SUITE_EDDSA_JCS_2022.to_string()),
42                    created: match self.proof.created {
43                        Some(created) => Some(created),
44                        None => Some(chrono::Utc::now()),
45                    },
46                    proof_value: None,
47                    ..self.proof.clone()
48                };
49
50                // Canonicalization
51                let canon_proof = json_canon::to_string(&normalized_proof).map_err(|_| Error::InvalidProof)?;
52                let canon_doc = json_canon::to_string(&payload).map_err(|_| Error::InvalidProof)?;
53
54                // Compute hash to sign
55                let hash = [sha256_hash(canon_proof.as_bytes()), sha256_hash(canon_doc.as_bytes())].concat();
56
57                self.key_pair.sign(&hash[..]).map(|signature| Proof {
58                    proof_value: Some(multibase::encode(self.proof_value_codec.unwrap(), signature)),
59                    ..normalized_proof
60                })
61            }
62        }
63    }
64
65    fn verify(&self, payload: serde_json::Value) -> Result<(), Error> {
66        match self.proof.proof_value.clone() {
67            None => Err(Error::InvalidProof),
68            Some(proof_value) => {
69                // Clone the proof
70                // - droping the proof value
71                // - normalyzing proof type fields
72                // This is the document to be signed
73                let normalized_proof = Proof {
74                    proof_value: None,
75                    proof_type: PROOF_TYPE_DATA_INTEGRITY_PROOF.to_string(),
76                    cryptosuite: Some(CRYPRO_SUITE_EDDSA_JCS_2022.to_string()),
77                    ..self.proof.clone()
78                };
79
80                // Strip the proof from the payload if any.
81                let naked_payload = match payload.get("proof") {
82                    None => payload,
83                    Some(_) => {
84                        let mut naked_payload = payload.clone();
85                        naked_payload.as_object_mut().unwrap().remove("proof");
86                        naked_payload
87                    }
88                };
89
90                // Canonicalization
91                let canon_proof = json_canon::to_string(&normalized_proof).map_err(|_| Error::InvalidProof)?;
92                let canon_doc = json_canon::to_string(&naked_payload).map_err(|_| Error::InvalidProof)?;
93
94                // Compute hash to verify
95                let hash = [sha256_hash(canon_proof.as_bytes()), sha256_hash(canon_doc.as_bytes())].concat();
96
97                multibase::decode(proof_value)
98                    .map_err(|_| Error::InvalidProof)
99                    .and_then(|signature| self.key_pair.verify(&hash, &(signature.1)))
100            }
101        }
102    }
103}
104
105// the test module
106#[cfg(test)]
107mod tests {
108    use serde_json::Value;
109
110    use crate::{crypto::Generate, proof::model::UnsecuredDocument};
111
112    // create an EdDsaJcs2022 object and use it to produce a proof.
113    // The proof is then verified.
114    #[test]
115    fn test_create_verify_proof() {
116        use chrono::TimeZone;
117        use serde_json::json;
118
119        use crate::crypto::Ed25519KeyPair;
120
121        use super::*;
122
123        let my_string = String::from("Sample seed bytes of thirtytwo!b");
124        let seed: &[u8] = my_string.as_bytes();
125        let key_pair = Ed25519KeyPair::new_with_seed(seed).unwrap();
126        let public_key = &key_pair.public_key.clone();
127
128        let proof = Proof {
129            id: None,
130            proof_type: "DataIntegrityProof".to_string(),
131            cryptosuite: Some("jcs-eddsa-2022".to_string()),
132            proof_purpose: "assertionMethod".to_string(),
133            verification_method: "https://di.example/issuer#z6MkjLrk3gKS2nnkeWcmcxiZPGskmesDpuwRBorgHxUXfxnG".to_string(),
134            created: Some(chrono::Utc.with_ymd_and_hms(2023, 3, 5, 19, 23, 24).unwrap()),
135            expires: None,
136            domain: Some(crate::proof::model::Domain::SingleString("vc-demo.adorsys.com".to_string())),
137            challenge: Some("523452345234asfdasdfasdfa".to_string()),
138            proof_value: None,
139            previous_proof: None,
140            nonce: Some("1234567890".to_string()),
141        };
142
143        let payload = json!({
144            "id": "did:example:123456789abcdefghi",
145            "name": "Alice",
146            "age": 101,
147            "image": "data:image/png;base64,iVBORw0KGgo...kJggg==",
148        });
149
150        let ed_dsa_jcs_2022_prover = EdDsaJcs2022 {
151            proof,
152            key_pair,
153            proof_value_codec: Some(Base::Base58Btc),
154        };
155
156        let secured_proof = ed_dsa_jcs_2022_prover.proof(payload.clone()).unwrap();
157
158        let secure_doc = UnsecuredDocument {
159            content: payload,
160            proof: crate::proof::model::Proofs::SingleProof(Box::new(secured_proof.clone())),
161        };
162
163        let expected_canonicalized_proof = r#"{"challenge":"523452345234asfdasdfasdfa","created":"2023-03-05T19:23:24Z","cryptosuite":"eddsa-jcs-2022","domain":"vc-demo.adorsys.com","nonce":"1234567890","proofPurpose":"assertionMethod","proofValue":"z2DbDNkE47SquDQ7wM6p3RjNdFB1FG7Num2w9kprZjUB2gNZvz7bYgcT5XCe3TdjfxxWfKkup1ZdrRhfEMLsk2kmr","type":"DataIntegrityProof","verificationMethod":"https://di.example/issuer#z6MkjLrk3gKS2nnkeWcmcxiZPGskmesDpuwRBorgHxUXfxnG"}"#;
164        let canonicalized_proof = json_canon::to_string(&secured_proof).unwrap();
165        assert_eq!(expected_canonicalized_proof, canonicalized_proof);
166
167        // let canonicalized_secured_doc = json_canon::to_string(&secure_doc).unwrap();
168        // Serialize the struct into a serde_json::Value
169        let secure_doc_json_value: Value = serde_json::to_value(&secure_doc).expect("Failed to serialize");
170
171        let ed_dsa_jcs_2022_verifier = EdDsaJcs2022 {
172            proof: secured_proof,
173            key_pair: Ed25519KeyPair::from_public_key(public_key.as_bytes()).unwrap(),
174            proof_value_codec: None,
175        };
176
177        ed_dsa_jcs_2022_verifier.verify(secure_doc_json_value).unwrap();
178    }
179}