affinidi_data_integrity/
lib.rs1use 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#[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 #[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 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 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 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 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 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 proof_options.proof_value = Some(multibase::encode(Base::Base58Btc, &signed).to_string());
131
132 Ok(proof_options)
133 }
134}
135
136fn 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 }