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 ssi::security::MultibaseBuf;
13use thiserror::Error;
14use tracing::debug;
15
16pub mod crypto_suites;
17pub mod verification_proof;
18
19#[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 #[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 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 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 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 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 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 proof_options.proof_value =
132 Some(MultibaseBuf::encode(Base::Base58Btc, &signed).to_string());
133
134 Ok(proof_options)
135 }
136}
137
138fn 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 }