1use chrono::Utc;
6use crypto_suites::CryptoSuite;
7use multibase::Base;
8use serde::{Deserialize, Serialize};
9use serde_json_canonicalizer::to_string;
10use sha2::{Digest, Sha256};
11use signer::Signer;
12use thiserror::Error;
13use tracing::debug;
14
15pub mod crypto_suites;
16pub mod signer;
17pub mod verification_proof;
18
19#[derive(Error, Debug)]
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 #[error("RDF Encoding Error: {0}")]
31 RdfEncodingError(String),
32}
33
34#[derive(Clone, Debug, Deserialize, Serialize)]
35#[serde(rename_all = "camelCase")]
36pub struct DataIntegrityProof {
37 #[serde(rename = "type")]
39 pub type_: String,
40
41 pub cryptosuite: CryptoSuite,
42
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub created: Option<String>,
45
46 pub verification_method: String,
47
48 pub proof_purpose: String,
49
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub proof_value: Option<String>,
52
53 #[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
54 pub context: Option<Vec<String>>,
55}
56
57impl DataIntegrityProof {
58 pub async fn sign_jcs_data<S>(
66 data_doc: &S,
67 context: Option<Vec<String>>,
68 signer: &dyn Signer,
69 created: Option<String>,
70 ) -> Result<DataIntegrityProof, DataIntegrityError>
71 where
72 S: Serialize,
73 {
74 let crypto_suite = CryptoSuite::EddsaJcs2022;
76 crypto_suite.validate_key_type(signer.key_type())?;
77 debug!(
78 "CryptoSuite: {}",
79 <CryptoSuite as TryInto<String>>::try_into(crypto_suite).unwrap()
80 );
81
82 let jcs = match to_string(data_doc) {
84 Ok(jcs) => jcs,
85 Err(e) => {
86 return Err(DataIntegrityError::InputDataError(format!(
87 "Failed to serialize data document: {e}",
88 )));
89 }
90 };
91 debug!("Document: {}", jcs);
92
93 let created = if created.is_some() {
94 created
95 } else {
96 Some(Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
97 };
98
99 let mut proof_options = DataIntegrityProof {
101 type_: "DataIntegrityProof".to_string(),
102 cryptosuite: crypto_suite,
103 created,
104 verification_method: signer.verification_method().to_string(),
105 proof_purpose: "assertionMethod".to_string(),
106 proof_value: None,
107 context,
108 };
109
110 let proof_jcs = match to_string(&proof_options) {
111 Ok(jcs) => jcs,
112 Err(e) => {
113 return Err(DataIntegrityError::InputDataError(format!(
114 "Failed to serialize proof options: {e}",
115 )));
116 }
117 };
118 debug!("proof options (JCS): {}", proof_jcs);
119
120 let hash_data = hashing_eddsa_jcs(&jcs, &proof_jcs);
121
122 let signed = signer.sign(hash_data.as_slice()).await?;
124 debug!(
125 "signature data = {}",
126 signed
127 .iter()
128 .map(|b| format!("{b:02x}"))
129 .collect::<Vec<String>>()
130 .join("")
131 );
132
133 proof_options.proof_value = Some(multibase::encode(Base::Base58Btc, &signed).to_string());
135
136 Ok(proof_options)
137 }
138
139 pub async fn sign_rdfc_data(
147 data_doc: &serde_json::Value,
148 context: Option<Vec<String>>,
149 signer: &dyn Signer,
150 created: Option<String>,
151 ) -> Result<DataIntegrityProof, DataIntegrityError> {
152 let crypto_suite = CryptoSuite::EddsaRdfc2022;
153 crypto_suite.validate_key_type(signer.key_type())?;
154 debug!(
155 "CryptoSuite: {}",
156 <CryptoSuite as TryInto<String>>::try_into(crypto_suite).unwrap()
157 );
158
159 let doc_context = data_doc
161 .get("@context")
162 .ok_or_else(|| {
163 DataIntegrityError::InputDataError(
164 "Document must contain @context for RDFC signing".to_string(),
165 )
166 })?
167 .clone();
168
169 let proof_context = if let Some(ctx) = context {
171 ctx
172 } else {
173 match &doc_context {
174 serde_json::Value::Array(arr) => arr
175 .iter()
176 .filter_map(|v| v.as_str().map(|s| s.to_string()))
177 .collect(),
178 serde_json::Value::String(s) => vec![s.clone()],
179 _ => {
180 return Err(DataIntegrityError::InputDataError(
181 "Invalid @context format in document".to_string(),
182 ));
183 }
184 }
185 };
186
187 let created = if created.is_some() {
188 created
189 } else {
190 Some(Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
191 };
192
193 let mut proof_options = DataIntegrityProof {
195 type_: "DataIntegrityProof".to_string(),
196 cryptosuite: crypto_suite,
197 created,
198 verification_method: signer.verification_method().to_string(),
199 proof_purpose: "assertionMethod".to_string(),
200 proof_value: None,
201 context: Some(proof_context),
202 };
203
204 let proof_value = serde_json::to_value(&proof_options).map_err(|e| {
206 DataIntegrityError::InputDataError(format!("Failed to serialize proof options: {e}"))
207 })?;
208
209 let hash_data = hashing_eddsa_rdfc(data_doc, &proof_value)?;
210
211 let signed = signer.sign(hash_data.as_slice()).await?;
213 debug!(
214 "signature data = {}",
215 signed
216 .iter()
217 .map(|b| format!("{b:02x}"))
218 .collect::<Vec<String>>()
219 .join("")
220 );
221
222 proof_options.proof_value = Some(multibase::encode(Base::Base58Btc, &signed).to_string());
224
225 Ok(proof_options)
226 }
227}
228
229fn hashing_eddsa_jcs(transformed_document: &str, canonical_proof_config: &str) -> Vec<u8> {
231 [
232 Sha256::digest(canonical_proof_config),
233 Sha256::digest(transformed_document),
234 ]
235 .concat()
236}
237
238fn hashing_eddsa_rdfc(
243 document: &serde_json::Value,
244 proof_config: &serde_json::Value,
245) -> Result<Vec<u8>, DataIntegrityError> {
246 let doc_hash = affinidi_rdf_encoding::expand_canonicalize_and_hash(document).map_err(|e| {
247 DataIntegrityError::RdfEncodingError(format!("Failed to hash document: {e}"))
248 })?;
249
250 let proof_hash =
251 affinidi_rdf_encoding::expand_canonicalize_and_hash(proof_config).map_err(|e| {
252 DataIntegrityError::RdfEncodingError(format!("Failed to hash proof config: {e}"))
253 })?;
254
255 Ok([proof_hash.as_slice(), doc_hash.as_slice()].concat())
256}
257
258#[cfg(test)]
259mod tests {
260 use affinidi_secrets_resolver::secrets::Secret;
261 use serde_json::json;
262
263 use crate::{DataIntegrityProof, hashing_eddsa_jcs};
264
265 #[test]
266 fn hashing_working() {
267 let hash = hashing_eddsa_jcs("test1", "test2");
268 let mut output = String::new();
269 for x in hash {
270 output.push_str(&format!("{x:02x}"));
271 }
272
273 assert_eq!(
274 output.as_str(),
275 "60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c7521b4f0e9851971998e732078544c96b36c3d01cedf7caa332359d6f1d83567014",
276 );
277 }
278
279 #[tokio::test]
280 async fn test_sign_jcs_data_bad_key() {
281 let generic_doc = json!({"test": "test_data"});
282
283 let pub_key = "zruqgFba156mDWfMUjJUSAKUvgCgF5NfgSYwSuEZuXpixts8tw3ot5BasjeyM65f8dzk5k6zgXf7pkbaaBnPrjCUmcJ";
284 let pri_key = "z42tmXtqqQBLmEEwn8tfi1bA2ghBx9cBo6wo8a44kVJEiqyA";
285 let secret = Secret::from_multibase(pri_key, Some(&format!("did:key:{pub_key}#{pub_key}")))
286 .expect("Couldn't create test key data");
287
288 assert!(DataIntegrityProof::sign_jcs_data(&generic_doc, None, &secret, None).await.is_err());
289 }
290
291 #[tokio::test]
292 async fn test_sign_jcs_data_good() {
293 let generic_doc = json!({"test": "test_data"});
294
295 let pub_key = "z6MktDNePDZTvVcF5t6u362SsonU7HkuVFSMVCjSspQLDaBm";
296 let pri_key = "z3u2UQyiY96d7VQaua8yiaSyQxq5Z5W5Qkpz7o2H2pc9BkEa";
297 let secret = Secret::from_multibase(pri_key, Some(&format!("did:key:{pub_key}#{pub_key}")))
298 .expect("Couldn't create test key data");
299
300 let context = vec![
301 "context1".to_string(),
302 "context2".to_string(),
303 "context3".to_string(),
304 ];
305 assert!(
306 DataIntegrityProof::sign_jcs_data(&generic_doc, Some(context), &secret, None).await.is_ok(),
307 "Signing failed"
308 );
309 }
310}