use getrandom::getrandom;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const PROVENANCE_CLAIM_VERSION: u8 = 1;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProvenanceClaim {
#[serde(
serialize_with = "serialize_claim_id",
deserialize_with = "deserialize_claim_id"
)]
pub claim_id: [u8; 16],
pub content_code: String,
pub created_at: u64,
pub file_size: u64,
pub format: String,
pub height: u32,
pub instance_digest: String,
pub issuer_id: String,
pub notice_digest: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_claim_id: Option<String>,
pub rights_policy: u8,
pub schema_version: u8,
pub software: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub statement_uri: Option<String>,
pub width: u32,
}
fn serialize_claim_id<S: serde::Serializer>(
id: &[u8; 16],
serializer: S,
) -> Result<S::Ok, S::Error> {
let s = hex::encode(id);
serializer.serialize_str(&s)
}
fn deserialize_claim_id<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<[u8; 16], D::Error> {
let s = String::deserialize(deserializer)?;
let bytes = hex::decode(&s).map_err(serde::de::Error::custom)?;
if bytes.len() != 16 {
return Err(serde::de::Error::custom("claim_id must be 16 bytes"));
}
let mut id = [0u8; 16];
id.copy_from_slice(&bytes);
Ok(id)
}
impl ProvenanceClaim {
#[must_use]
pub fn new(rights_policy: u8) -> Self {
Self {
schema_version: PROVENANCE_CLAIM_VERSION,
claim_id: Self::random_claim_id(),
rights_policy,
notice_digest: String::new(),
content_code: String::new(),
instance_digest: String::new(),
format: String::new(),
width: 0,
height: 0,
file_size: 0,
created_at: 0,
issuer_id: String::new(),
software: String::new(),
parent_claim_id: None,
statement_uri: None,
}
}
#[must_use]
pub fn with_notice_digest(mut self, notice_text: &[u8]) -> Self {
let mut hasher = Sha256::new();
hasher.update(notice_text);
let hash: [u8; 32] = hasher.finalize().into();
self.notice_digest = format!("sha256:{}", hex::encode(hash));
self
}
#[must_use]
pub fn with_notice_digest_raw(mut self, digest: String) -> Self {
self.notice_digest = digest;
self
}
#[must_use]
pub fn with_content_code(mut self, code: String) -> Self {
self.content_code = code;
self
}
#[must_use]
pub fn with_instance_digest(mut self, file_bytes: &[u8]) -> Self {
let mut hasher = Sha256::new();
hasher.update(file_bytes);
let hash: [u8; 32] = hasher.finalize().into();
self.instance_digest = format!("sha256:{}", hex::encode(hash));
self
}
#[must_use]
pub fn with_instance_digest_raw(mut self, digest: String) -> Self {
self.instance_digest = digest;
self
}
#[must_use]
pub fn with_source_facts(
mut self,
format: &str,
width: u32,
height: u32,
file_size: u64,
) -> Self {
self.format = format.to_string();
self.width = width;
self.height = height;
self.file_size = file_size;
self
}
#[must_use]
pub fn with_creation_time(mut self, time: u64) -> Self {
self.created_at = time;
self
}
#[must_use]
pub fn with_issuer_id(mut self, issuer_id: String) -> Self {
self.issuer_id = issuer_id;
self
}
#[must_use]
pub fn with_software(mut self, software: &str) -> Self {
self.software = software.to_string();
self
}
#[must_use]
pub fn with_parent_claim(mut self, parent_id: String) -> Self {
self.parent_claim_id = Some(parent_id);
self
}
#[must_use]
pub fn with_statement_uri(mut self, uri: &str) -> Self {
self.statement_uri = Some(uri.to_string());
self
}
#[must_use]
pub fn canonical_bytes(&self) -> Vec<u8> {
let mut map = serde_json::Map::new();
map.insert(
"content_code".into(),
serde_json::Value::String(self.content_code.clone()),
);
map.insert(
"created_at".into(),
serde_json::Value::Number(self.created_at.into()),
);
map.insert(
"file_size".into(),
serde_json::Value::Number(self.file_size.into()),
);
map.insert(
"format".into(),
serde_json::Value::String(self.format.clone()),
);
map.insert(
"height".into(),
serde_json::Value::Number(self.height.into()),
);
map.insert(
"instance_digest".into(),
serde_json::Value::String(self.instance_digest.clone()),
);
map.insert(
"issuer_id".into(),
serde_json::Value::String(self.issuer_id.clone()),
);
map.insert(
"notice_digest".into(),
serde_json::Value::String(self.notice_digest.clone()),
);
if let Some(ref parent) = self.parent_claim_id {
map.insert(
"parent_claim_id".into(),
serde_json::Value::String(parent.clone()),
);
}
map.insert(
"rights_policy".into(),
serde_json::Value::Number(self.rights_policy.into()),
);
map.insert(
"schema_version".into(),
serde_json::Value::Number(self.schema_version.into()),
);
map.insert(
"software".into(),
serde_json::Value::String(self.software.clone()),
);
if let Some(ref uri) = self.statement_uri {
map.insert(
"statement_uri".into(),
serde_json::Value::String(uri.clone()),
);
}
map.insert("width".into(), serde_json::Value::Number(self.width.into()));
let canonical = serde_json::Value::Object(map);
serde_json::to_string(&canonical)
.expect("provenance claim canonical serialization failed")
.into_bytes()
}
#[must_use]
pub fn digest(&self) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(self.canonical_bytes());
hasher.finalize().into()
}
#[must_use]
pub fn random_claim_id() -> [u8; 16] {
let mut id = [0u8; 16];
getrandom(&mut id).expect("failed to generate random claim ID");
id
}
}