Skip to main content

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