use crate::error::Error;
use crate::extract::locate_block;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Exclusion {
pub start: usize,
pub length: usize,
}
pub const DATA_HASH_LABEL: &str = "c2pa.hash.data";
pub fn manifest_exclusion(text: &str) -> Result<Exclusion, Error> {
reject_bare_cr(text.as_bytes())?;
let block = locate_block(text)?;
let len = text.len();
let bs = block.line_start;
let be = block.line_end;
let exclusion = if bs == 0 && be == len {
Exclusion {
start: 0,
length: len,
}
} else if bs == 0 {
Exclusion {
start: 0,
length: be,
}
} else if be == len {
let bytes = text.as_bytes();
let mut start = bs - 1; if start > 0 && bytes[start - 1] == b'\r' {
start -= 1;
}
Exclusion {
start,
length: len - start,
}
} else {
Exclusion {
start: bs,
length: be - bs,
}
};
Ok(exclusion)
}
pub fn hashed_bytes(text: &str) -> Result<Vec<u8>, Error> {
let exclusion = manifest_exclusion(text)?;
apply_exclusions(text.as_bytes(), &[exclusion])
}
pub(crate) fn apply_exclusions(bytes: &[u8], exclusions: &[Exclusion]) -> Result<Vec<u8>, Error> {
let mut cursor = 0usize;
let mut out = Vec::with_capacity(bytes.len());
for ex in exclusions {
let end = ex
.start
.checked_add(ex.length)
.ok_or(Error::MalformedExclusion)?;
if ex.start < cursor {
return Err(Error::MalformedExclusion);
}
if end > bytes.len() {
return Err(Error::HashMismatch);
}
out.extend_from_slice(&bytes[cursor..ex.start]);
cursor = end;
}
out.extend_from_slice(&bytes[cursor..]);
Ok(out)
}
fn reject_bare_cr(bytes: &[u8]) -> Result<(), Error> {
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'\r' {
if bytes.get(i + 1) != Some(&b'\n') {
return Err(Error::BareCarriageReturn);
}
i += 2;
} else {
i += 1;
}
}
Ok(())
}
#[cfg(feature = "hard-binding")]
mod hashing {
use super::{apply_exclusions, manifest_exclusion, reject_bare_cr, Exclusion, DATA_HASH_LABEL};
use crate::codec;
use crate::error::Error;
use sha2::{Digest, Sha256, Sha384, Sha512};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Algorithm {
Sha256,
Sha384,
Sha512,
}
impl Algorithm {
pub fn id(self) -> &'static str {
match self {
Algorithm::Sha256 => "sha256",
Algorithm::Sha384 => "sha384",
Algorithm::Sha512 => "sha512",
}
}
pub fn from_id(id: &str) -> Result<Self, Error> {
match id {
"sha256" => Ok(Algorithm::Sha256),
"sha384" => Ok(Algorithm::Sha384),
"sha512" => Ok(Algorithm::Sha512),
other => Err(Error::UnsupportedAlgorithm(other.to_string())),
}
}
fn hash(self, data: &[u8]) -> Vec<u8> {
match self {
Algorithm::Sha256 => Sha256::digest(data).to_vec(),
Algorithm::Sha384 => Sha384::digest(data).to_vec(),
Algorithm::Sha512 => Sha512::digest(data).to_vec(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DataHash {
pub exclusions: Vec<Exclusion>,
pub alg: String,
pub hash: Vec<u8>,
pub name: Option<String>,
}
impl DataHash {
pub fn label(&self) -> &'static str {
DATA_HASH_LABEL
}
pub fn to_json(&self) -> String {
let ranges: Vec<String> = self
.exclusions
.iter()
.map(|e| format!("{{\"start\":{},\"length\":{}}}", e.start, e.length))
.collect();
let mut json = format!(
"{{\"exclusions\":[{}],\"alg\":\"{}\",\"hash\":\"{}\"",
ranges.join(","),
self.alg,
codec::encode(&self.hash)
);
if let Some(name) = &self.name {
json.push_str(&format!(",\"name\":\"{}\"", name));
}
json.push('}');
json
}
}
pub fn compute_data_hash(text: &str, alg: Algorithm) -> Result<DataHash, Error> {
let exclusion = manifest_exclusion(text)?;
let covered = apply_exclusions(text.as_bytes(), &[exclusion])?;
Ok(DataHash {
exclusions: vec![exclusion],
alg: alg.id().to_string(),
hash: alg.hash(&covered),
name: None,
})
}
pub fn verify_data_hash(text: &str, data_hash: &DataHash) -> Result<(), Error> {
reject_bare_cr(text.as_bytes())?;
let alg = Algorithm::from_id(&data_hash.alg)?;
if data_hash.exclusions.is_empty() {
return Err(Error::MalformedExclusion);
}
let covered = apply_exclusions(text.as_bytes(), &data_hash.exclusions)?;
if alg.hash(&covered) == data_hash.hash {
Ok(())
} else {
Err(Error::HashMismatch)
}
}
}
#[cfg(feature = "hard-binding")]
pub use hashing::{compute_data_hash, verify_data_hash, Algorithm, DataHash};
#[cfg(test)]
mod tests {
use super::*;
use crate::embed::{embed_front_matter, embed_manifest, embed_manifest_at_end, ManifestRef};
const URL: &str = "https://example.com/m.c2pa";
#[test]
fn exclusion_at_beginning() {
let embedded = embed_manifest("print('hi')\n", ManifestRef::Url(URL), "#", None);
let ex = manifest_exclusion(&embedded).unwrap();
assert_eq!(ex.start, 0);
assert_eq!(hashed_bytes(&embedded).unwrap(), b"print('hi')\n");
}
#[test]
fn exclusion_at_end_excludes_preceding_newline() {
let embedded = embed_manifest_at_end("print('hi')\n", ManifestRef::Url(URL), "#", None);
let ex = manifest_exclusion(&embedded).unwrap();
assert_eq!(ex.start + ex.length, embedded.len());
assert_eq!(hashed_bytes(&embedded).unwrap(), b"print('hi')");
}
#[test]
fn exclusion_elsewhere_after_header() {
let text =
"#!/bin/sh\n# -----BEGIN C2PA MANIFEST----- u -----END C2PA MANIFEST-----\necho hi\n";
let ex = manifest_exclusion(text).unwrap();
assert_eq!(ex.start, "#!/bin/sh\n".len());
assert_eq!(hashed_bytes(text).unwrap(), b"#!/bin/sh\necho hi\n");
}
#[test]
fn exclusion_front_matter_keeps_fences() {
let embedded = embed_front_matter("title: doc\n", ManifestRef::Url(URL), "---");
let covered = String::from_utf8(hashed_bytes(&embedded).unwrap()).unwrap();
assert!(covered.starts_with("---\n"));
assert!(covered.contains("title: doc"));
assert!(!covered.contains("BEGIN C2PA MANIFEST"));
}
#[test]
fn exclusion_whole_file_is_block() {
let text = "# -----BEGIN C2PA MANIFEST----- u -----END C2PA MANIFEST-----\n";
let ex = manifest_exclusion(text).unwrap();
assert_eq!(ex.start, 0);
assert_eq!(ex.length, text.len());
assert_eq!(hashed_bytes(text).unwrap(), b"");
}
#[test]
fn crlf_is_supported() {
let text = "# -----BEGIN C2PA MANIFEST----- u -----END C2PA MANIFEST-----\r\nprint()\r\n";
assert_eq!(hashed_bytes(text).unwrap(), b"print()\r\n");
}
#[test]
fn bare_cr_is_rejected() {
let text = "# -----BEGIN C2PA MANIFEST----- u -----END C2PA MANIFEST-----\rprint()";
assert!(matches!(
manifest_exclusion(text),
Err(Error::BareCarriageReturn)
));
}
#[test]
fn apply_exclusions_rejects_out_of_order() {
let bytes = b"0123456789";
let ranges = [
Exclusion {
start: 5,
length: 2,
},
Exclusion {
start: 1,
length: 2,
},
];
assert!(matches!(
apply_exclusions(bytes, &ranges),
Err(Error::MalformedExclusion)
));
}
#[test]
fn apply_exclusions_rejects_beyond_end() {
let bytes = b"0123456789";
let ranges = [Exclusion {
start: 8,
length: 5,
}];
assert!(matches!(
apply_exclusions(bytes, &ranges),
Err(Error::HashMismatch)
));
}
}