affinidi_data_integrity/
lib.rs

1/*!
2*   W3C Data Integrity Implementation
3*/
4
5use affinidi_secrets_resolver::secrets::Secret;
6use chrono::Utc;
7use crypto_suites::CryptoSuite;
8use multibase::Base;
9use serde::{Deserialize, Serialize};
10use serde_json_canonicalizer::to_string;
11use sha2::{Digest, Sha256};
12use ssi::security::MultibaseBuf;
13use thiserror::Error;
14use tracing::debug;
15
16pub mod crypto_suites;
17pub mod verification_proof;
18
19/// Affinidi Data Integrity Library Errors
20#[derive(Error, Debug, PartialEq)]
21pub enum DataIntegrityError {
22    #[error("Input Data Error: {0}")]
23    InputDataError(String),
24    #[error("Crypto Error: {0}")]
25    CryptoError(String),
26    #[error("Secrets Error: {0}")]
27    SecretsError(String),
28    #[error("Verification Error: {0}")]
29    VerificationError(String),
30}
31
32#[derive(Clone, Debug, Deserialize, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct DataIntegrityProof {
35    /// Must be 'DataIntegrityProof'
36    #[serde(rename = "type")]
37    pub type_: String,
38
39    pub cryptosuite: CryptoSuite,
40
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub created: Option<String>,
43
44    pub verification_method: String,
45
46    pub proof_purpose: String,
47
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub proof_value: Option<String>,
50
51    #[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
52    pub context: Option<Vec<String>>,
53}
54
55impl DataIntegrityProof {
56    /// Creates a signature for the given data using the specified key.
57    /// data_doc: Serializable Struct
58    /// context: Optional context for the proof
59    /// secret: Secret containing the private key to sign with
60    /// created: Optional timestamp for the proof creation ("2023-02-24T23:36:38Z")
61    ///
62    /// Returns a Result containing a proof if successfull
63    pub fn sign_jcs_data<S>(
64        data_doc: &S,
65        context: Option<Vec<String>>,
66        secret: &Secret,
67        created: Option<String>,
68    ) -> Result<DataIntegrityProof, DataIntegrityError>
69    where
70        S: Serialize,
71    {
72        // Initialise as required
73        let crypto_suite: CryptoSuite = secret.get_key_type().try_into()?;
74        debug!(
75            "CryptoSuite: {}",
76            <CryptoSuite as TryInto<String>>::try_into(crypto_suite.clone()).unwrap()
77        );
78
79        // Step 1: Serialize the data document to a canonical JSON string
80        let jcs = match to_string(data_doc) {
81            Ok(jcs) => jcs,
82            Err(e) => {
83                return Err(DataIntegrityError::InputDataError(format!(
84                    "Failed to serialize data document: {e}",
85                )));
86            }
87        };
88        debug!("Document: {}", jcs);
89
90        let created = if created.is_some() {
91            created
92        } else {
93            Some(Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
94        };
95
96        // Create a Proof Options struct
97        let mut proof_options = DataIntegrityProof {
98            type_: "DataIntegrityProof".to_string(),
99            cryptosuite: crypto_suite.clone(),
100            created,
101            verification_method: secret.id.clone(),
102            proof_purpose: "assertionMethod".to_string(),
103            proof_value: None,
104            context,
105        };
106
107        let proof_jcs = match to_string(&proof_options) {
108            Ok(jcs) => jcs,
109            Err(e) => {
110                return Err(DataIntegrityError::InputDataError(format!(
111                    "Failed to serialize proof options: {e}",
112                )));
113            }
114        };
115        debug!("proof options (JCS): {}", proof_jcs);
116
117        let hash_data = hashing_eddsa_jcs(&jcs, &proof_jcs);
118
119        // Step 6: Sign the final hash
120        let signed = crypto_suite.sign(secret, hash_data.as_slice())?;
121        debug!(
122            "signature data = {}",
123            signed
124                .iter()
125                .map(|b| format!("{b:02x}"))
126                .collect::<Vec<String>>()
127                .join("")
128        );
129
130        // Step 7: Encode using base58btc
131        proof_options.proof_value =
132            Some(MultibaseBuf::encode(Base::Base58Btc, &signed).to_string());
133
134        Ok(proof_options)
135    }
136}
137
138/// Hashing Algorithm for EDDSA JCS
139fn hashing_eddsa_jcs(transformed_document: &str, canonical_proof_config: &str) -> Vec<u8> {
140    [
141        Sha256::digest(canonical_proof_config),
142        Sha256::digest(transformed_document),
143    ]
144    .concat()
145}
146
147#[cfg(test)]
148mod tests {
149    use affinidi_secrets_resolver::secrets::Secret;
150    use serde_json::json;
151
152    use crate::{DataIntegrityProof, hashing_eddsa_jcs};
153
154    #[test]
155    fn hashing_working() {
156        let hash = hashing_eddsa_jcs("test1", "test2");
157        let mut output = String::new();
158        for x in hash {
159            output.push_str(&format!("{x:02x}"));
160        }
161
162        assert_eq!(
163            output.as_str(),
164            "60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c7521b4f0e9851971998e732078544c96b36c3d01cedf7caa332359d6f1d83567014",
165        );
166    }
167
168    #[test]
169    fn test_sign_jcs_data_bad_key() {
170        let generic_doc = json!({"test": "test_data"});
171
172        let pub_key = "zruqgFba156mDWfMUjJUSAKUvgCgF5NfgSYwSuEZuXpixts8tw3ot5BasjeyM65f8dzk5k6zgXf7pkbaaBnPrjCUmcJ";
173        let pri_key = "z42tmXtqqQBLmEEwn8tfi1bA2ghBx9cBo6wo8a44kVJEiqyA";
174        let secret =
175            Secret::from_multibase(&format!("did:key:{pub_key}#{pub_key}"), pub_key, pri_key)
176                .expect("Couldn't create test key data");
177
178        assert!(DataIntegrityProof::sign_jcs_data(&generic_doc, None, &secret, None).is_err());
179    }
180
181    #[test]
182    fn test_sign_jcs_data_good() {
183        let generic_doc = json!({"test": "test_data"});
184
185        let pub_key = "z6MktDNePDZTvVcF5t6u362SsonU7HkuVFSMVCjSspQLDaBm";
186        let pri_key = "z3u2UQyiY96d7VQaua8yiaSyQxq5Z5W5Qkpz7o2H2pc9BkEa";
187        let secret =
188            Secret::from_multibase(&format!("did:key:{pub_key}#{pub_key}"), pub_key, pri_key)
189                .expect("Couldn't create test key data");
190
191        let context = vec![
192            "context1".to_string(),
193            "context2".to_string(),
194            "context3".to_string(),
195        ];
196        assert!(
197            DataIntegrityProof::sign_jcs_data(&generic_doc, Some(context), &secret, None).is_ok(),
198            "Signing failed"
199        );
200    }
201
202    /*
203    #[test]
204    fn test_sign_jcs_proof_only_good() {
205        let generic_doc =
206            json!({"test": "test_data", "@context": ["context1", "context2", "context3"]});
207
208        let context: Vec<String> = generic_doc
209            .get("@context")
210            .unwrap()
211            .as_array()
212            .unwrap()
213            .iter()
214            .map(|c| c.as_str().unwrap().to_string())
215            .collect();
216
217        let pub_key = "z6MktDNePDZTvVcF5t6u362SsonU7HkuVFSMVCjSspQLDaBm";
218        let pri_key = "z3u2UQyiY96d7VQaua8yiaSyQxq5Z5W5Qkpz7o2H2pc9BkEa";
219        let secret =
220            Secret::from_multibase(&format!("did:key:{pub_key}#{pub_key}"), pub_key, pri_key)
221                .expect("Couldn't create test key data");
222
223        assert!(
224            DataIntegrityProof::sign_jcs_proof_only(&generic_doc, Some(context), &secret).is_ok(),
225            "Signing failed"
226        );
227    }*/
228}