use affinidi_secrets_resolver::secrets::Secret;
use chrono::Utc;
use crypto_suites::CryptoSuite;
use multibase::Base;
use serde::{Deserialize, Serialize};
use serde_json_canonicalizer::to_string;
use sha2::{Digest, Sha256};
use thiserror::Error;
use tracing::debug;
pub mod crypto_suites;
pub mod verification_proof;
#[derive(Error, Debug)]
pub enum DataIntegrityError {
#[error("Input Data Error: {0}")]
InputDataError(String),
#[error("Crypto Error: {0}")]
CryptoError(String),
#[error("Secrets Error: {0}")]
SecretsError(String),
#[error("Verification Error: {0}")]
VerificationError(String),
#[error("RDF Encoding Error: {0}")]
RdfEncodingError(String),
}
#[derive(Clone, Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DataIntegrityProof {
#[serde(rename = "type")]
pub type_: String,
pub cryptosuite: CryptoSuite,
#[serde(skip_serializing_if = "Option::is_none")]
pub created: Option<String>,
pub verification_method: String,
pub proof_purpose: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub proof_value: Option<String>,
#[serde(rename = "@context", skip_serializing_if = "Option::is_none")]
pub context: Option<Vec<String>>,
}
impl DataIntegrityProof {
pub fn sign_jcs_data<S>(
data_doc: &S,
context: Option<Vec<String>>,
secret: &Secret,
created: Option<String>,
) -> Result<DataIntegrityProof, DataIntegrityError>
where
S: Serialize,
{
let crypto_suite = CryptoSuite::EddsaJcs2022;
crypto_suite.validate_key_type(secret.get_key_type())?;
debug!(
"CryptoSuite: {}",
<CryptoSuite as TryInto<String>>::try_into(crypto_suite).unwrap()
);
let jcs = match to_string(data_doc) {
Ok(jcs) => jcs,
Err(e) => {
return Err(DataIntegrityError::InputDataError(format!(
"Failed to serialize data document: {e}",
)));
}
};
debug!("Document: {}", jcs);
let created = if created.is_some() {
created
} else {
Some(Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
};
let mut proof_options = DataIntegrityProof {
type_: "DataIntegrityProof".to_string(),
cryptosuite: crypto_suite,
created,
verification_method: secret.id.clone(),
proof_purpose: "assertionMethod".to_string(),
proof_value: None,
context,
};
let proof_jcs = match to_string(&proof_options) {
Ok(jcs) => jcs,
Err(e) => {
return Err(DataIntegrityError::InputDataError(format!(
"Failed to serialize proof options: {e}",
)));
}
};
debug!("proof options (JCS): {}", proof_jcs);
let hash_data = hashing_eddsa_jcs(&jcs, &proof_jcs);
let signed = crypto_suite.sign(secret, hash_data.as_slice())?;
debug!(
"signature data = {}",
signed
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<String>>()
.join("")
);
proof_options.proof_value = Some(multibase::encode(Base::Base58Btc, &signed).to_string());
Ok(proof_options)
}
pub fn sign_rdfc_data(
data_doc: &serde_json::Value,
context: Option<Vec<String>>,
secret: &Secret,
created: Option<String>,
) -> Result<DataIntegrityProof, DataIntegrityError> {
let crypto_suite = CryptoSuite::EddsaRdfc2022;
crypto_suite.validate_key_type(secret.get_key_type())?;
debug!(
"CryptoSuite: {}",
<CryptoSuite as TryInto<String>>::try_into(crypto_suite).unwrap()
);
let doc_context = data_doc
.get("@context")
.ok_or_else(|| {
DataIntegrityError::InputDataError(
"Document must contain @context for RDFC signing".to_string(),
)
})?
.clone();
let proof_context = if let Some(ctx) = context {
ctx
} else {
match &doc_context {
serde_json::Value::Array(arr) => arr
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect(),
serde_json::Value::String(s) => vec![s.clone()],
_ => {
return Err(DataIntegrityError::InputDataError(
"Invalid @context format in document".to_string(),
));
}
}
};
let created = if created.is_some() {
created
} else {
Some(Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true))
};
let mut proof_options = DataIntegrityProof {
type_: "DataIntegrityProof".to_string(),
cryptosuite: crypto_suite,
created,
verification_method: secret.id.clone(),
proof_purpose: "assertionMethod".to_string(),
proof_value: None,
context: Some(proof_context),
};
let proof_value = serde_json::to_value(&proof_options).map_err(|e| {
DataIntegrityError::InputDataError(format!("Failed to serialize proof options: {e}"))
})?;
let hash_data = hashing_eddsa_rdfc(data_doc, &proof_value)?;
let signed = crypto_suite.sign(secret, hash_data.as_slice())?;
debug!(
"signature data = {}",
signed
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<String>>()
.join("")
);
proof_options.proof_value = Some(multibase::encode(Base::Base58Btc, &signed).to_string());
Ok(proof_options)
}
}
fn hashing_eddsa_jcs(transformed_document: &str, canonical_proof_config: &str) -> Vec<u8> {
[
Sha256::digest(canonical_proof_config),
Sha256::digest(transformed_document),
]
.concat()
}
fn hashing_eddsa_rdfc(
document: &serde_json::Value,
proof_config: &serde_json::Value,
) -> Result<Vec<u8>, DataIntegrityError> {
let doc_hash = affinidi_rdf_encoding::expand_canonicalize_and_hash(document).map_err(|e| {
DataIntegrityError::RdfEncodingError(format!("Failed to hash document: {e}"))
})?;
let proof_hash =
affinidi_rdf_encoding::expand_canonicalize_and_hash(proof_config).map_err(|e| {
DataIntegrityError::RdfEncodingError(format!("Failed to hash proof config: {e}"))
})?;
Ok([proof_hash.as_slice(), doc_hash.as_slice()].concat())
}
#[cfg(test)]
mod tests {
use affinidi_secrets_resolver::secrets::Secret;
use serde_json::json;
use crate::{DataIntegrityProof, hashing_eddsa_jcs};
#[test]
fn hashing_working() {
let hash = hashing_eddsa_jcs("test1", "test2");
let mut output = String::new();
for x in hash {
output.push_str(&format!("{x:02x}"));
}
assert_eq!(
output.as_str(),
"60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c7521b4f0e9851971998e732078544c96b36c3d01cedf7caa332359d6f1d83567014",
);
}
#[test]
fn test_sign_jcs_data_bad_key() {
let generic_doc = json!({"test": "test_data"});
let pub_key = "zruqgFba156mDWfMUjJUSAKUvgCgF5NfgSYwSuEZuXpixts8tw3ot5BasjeyM65f8dzk5k6zgXf7pkbaaBnPrjCUmcJ";
let pri_key = "z42tmXtqqQBLmEEwn8tfi1bA2ghBx9cBo6wo8a44kVJEiqyA";
let secret = Secret::from_multibase(pri_key, Some(&format!("did:key:{pub_key}#{pub_key}")))
.expect("Couldn't create test key data");
assert!(DataIntegrityProof::sign_jcs_data(&generic_doc, None, &secret, None).is_err());
}
#[test]
fn test_sign_jcs_data_good() {
let generic_doc = json!({"test": "test_data"});
let pub_key = "z6MktDNePDZTvVcF5t6u362SsonU7HkuVFSMVCjSspQLDaBm";
let pri_key = "z3u2UQyiY96d7VQaua8yiaSyQxq5Z5W5Qkpz7o2H2pc9BkEa";
let secret = Secret::from_multibase(pri_key, Some(&format!("did:key:{pub_key}#{pub_key}")))
.expect("Couldn't create test key data");
let context = vec![
"context1".to_string(),
"context2".to_string(),
"context3".to_string(),
];
assert!(
DataIntegrityProof::sign_jcs_data(&generic_doc, Some(context), &secret, None).is_ok(),
"Signing failed"
);
}
}