c2pa-rs

A fork of https://github.com/contentauth/c2pa-rs/
git clone git://archive.git.mtrnord.blog/mtrnords-photography-manager/c2pa-rs.git
Log | Files | Refs | README

commit 4113ec8a343c8f2d1103e111d48ae869a70af657
parent fa7d19c1a25c49a86286ecdddf9cf987104e7f63
Author: Gavin  Peacock <gpeacock@adobe.com>
Date:   Wed,  9 Nov 2022 15:03:13 -0800

(MINOR) Add sign feature for signing manifests without file I/O (#125)

* Add save_jumbf_to_memory

* Add sign feature, unit test signs stream

* Added embed_stream and sign feature support

* Add an embed_from_memory method

* [MINOR] ManifestStore:from_bytes/_async takes slice instead of Vec

* Add &mut[u8] to CAIRead and CAIReadWrite

* Add return value comment on embed_from_memory

* [MINOR] ManifestStore::from_bytes(_async) takes slice instead of Vec

* add stream thumbnail generation support

* Add Exif Assertion support

* write exif to jumbf as json instead of cbor

Diffstat:
Msdk/Cargo.toml | 5+++--
Msdk/src/assertions/data_hash.rs | 20+++++++++++++++++---
Msdk/src/assertions/exif.rs | 1+
Msdk/src/asset_handlers/jpeg_io.rs | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------------
Msdk/src/asset_io.rs | 18+++++++++++++++++-
Msdk/src/claim.rs | 2+-
Msdk/src/create_signer.rs | 3++-
Msdk/src/error.rs | 5+++--
Msdk/src/hashed_uri.rs | 2+-
Msdk/src/jumbf_io.rs | 54+++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msdk/src/lib.rs | 12++++++------
Msdk/src/manifest.rs | 99++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msdk/src/manifest_store.rs | 18++++++++----------
Msdk/src/openssl/ec_signer.rs | 17++---------------
Msdk/src/openssl/ec_validator.rs | 1+
Msdk/src/openssl/ed_signer.rs | 19+------------------
Msdk/src/openssl/ed_validator.rs | 1+
Msdk/src/openssl/rsa_signer.rs | 18+-----------------
Msdk/src/openssl/temp_signer.rs | 5+++++
Msdk/src/salt.rs | 4++--
Msdk/src/signer.rs | 12+++++++++---
Msdk/src/store.rs | 196++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
Msdk/src/time_stamp.rs | 16++++++++--------
Msdk/src/utils/hash_utils.rs | 17+++++++++++++----
Msdk/src/utils/mod.rs | 2+-
Msdk/src/utils/test.rs | 21++++++++++++++-------
Msdk/src/utils/thumbnail.rs | 50+++++++++++++++++++++++++++++++++++++++++++++-----
Msdk/src/validator.rs | 8++++----
28 files changed, 563 insertions(+), 170 deletions(-)

diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml @@ -22,12 +22,13 @@ rustdoc-args = ["--cfg", "docsrs"] default = ["add_thumbnails"] add_thumbnails = ["image"] async_signer = ["async-trait", "file_io"] -bmff = [] # Work in progress support for BMFF-based containers -file_io = ["openssl"] +bmff = [] +file_io = ["sign"] serialize_thumbnails = [] xmp_write = ["xmp_toolkit"] no_interleaved_io = ["file_io"] fetch_remote_manifests = ["file_io"] +sign = ["openssl"] # The diagnostics feature is unsupported and might be removed. # It enables some low-overhead timing features used in our development cycle. diff --git a/sdk/src/assertions/data_hash.rs b/sdk/src/assertions/data_hash.rs @@ -19,9 +19,10 @@ use serde_bytes::ByteBuf; use crate::{ assertion::{Assertion, AssertionBase, AssertionCbor}, assertions::labels, + asset_io::CAIReadWrite, cbor_types::UriT, error::{Error, Result}, - utils::hash_utils::{hash_asset_by_alg, verify_asset_by_alg, verify_by_alg, Exclusion}, + utils::hash_utils::{hash_stream_by_alg, verify_asset_by_alg, verify_by_alg, Exclusion}, }; const ASSERTION_CREATION_VERSION: usize = 1; @@ -104,6 +105,12 @@ impl DataHash { Ok(()) } + /// generate the hash value for the Asset stream using the range from the DataHash + pub fn gen_hash_from_stream(&mut self, stream: &mut dyn CAIReadWrite) -> Result<()> { + self.hash = self.hash_from_stream(stream)?; + Ok(()) + } + // add padding to match size pub fn pad_to_size(&mut self, desired_size: usize) -> Result<()> { let mut curr_size = self.to_assertion()?.data().len(); @@ -143,6 +150,13 @@ impl DataHash { /// generate the asset hash from a file asset using the constructed /// start and length values fn hash_from_asset(&mut self, asset_path: &Path) -> Result<Vec<u8>> { + let mut file = std::fs::File::open(asset_path)?; + self.hash_from_stream(&mut file) + } + + /// generate the asset hash from a stream using the constructed + /// start and length values + pub fn hash_from_stream(&mut self, stream: &mut dyn CAIReadWrite) -> Result<Vec<u8>> { if self.is_remote_hash() { return Err(Error::BadParam( "asset hash is remote, not yet supported".to_owned(), @@ -156,8 +170,8 @@ impl DataHash { // sort the exclusions let hash = match self.exclusions { - Some(ref e) => hash_asset_by_alg(&alg, asset_path, Some(e.clone()))?, - None => hash_asset_by_alg(&alg, asset_path, None)?, + Some(ref e) => hash_stream_by_alg(&alg, stream, Some(e.clone()))?, + None => hash_stream_by_alg(&alg, stream, None)?, }; if hash.is_empty() { diff --git a/sdk/src/assertions/exif.rs b/sdk/src/assertions/exif.rs @@ -24,6 +24,7 @@ use crate::{assertions::labels, Assertion, AssertionBase, AssertionJson, Error, /// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_exif_information> /// /// This does not yet define or validate individual fields, but will ensure the correct assertion structure +/// #[derive(Serialize, Deserialize, Debug)] pub struct Exif { #[serde(rename = "@context", skip_serializing_if = "Option::is_none")] diff --git a/sdk/src/asset_handlers/jpeg_io.rs b/sdk/src/asset_handlers/jpeg_io.rs @@ -12,8 +12,8 @@ // each license. use std::{ - fs::{read, File}, - io::Cursor, + fs::File, + io::{Cursor, SeekFrom}, path::*, }; @@ -24,8 +24,11 @@ use img_parts::{ }; use crate::{ - asset_io::{AssetIO, CAILoader, CAIRead, HashBlockObjectType, HashObjectPositions}, - error::{wrap_io_err, Error, Result}, + asset_io::{ + AssetIO, CAILoader, CAIRead, CAIReadWrite, CAIWriter, HashBlockObjectType, + HashObjectPositions, + }, + error::{Error, Result}, }; const XMP_SIGNATURE: &[u8] = b"http://ns.adobe.com/xap/1.0/"; @@ -63,8 +66,12 @@ fn xmp_from_bytes(asset_bytes: &[u8]) -> Option<String> { } } -fn add_required_segs(asset_path: &std::path::Path) -> Result<()> { - let buf = read(asset_path)?; +fn add_required_segs_to_stream(stream: &mut dyn CAIReadWrite) -> Result<()> { + let mut buf: Vec<u8> = Vec::new(); + stream.seek(SeekFrom::Start(0))?; + stream.read_to_end(&mut buf).map_err(Error::IoError)?; + stream.seek(SeekFrom::Start(0))?; + let dimg_opt = DynImage::from_bytes(buf.into()) .map_err(|_err| Error::InvalidAsset("Could not parse input JPEG".to_owned()))?; @@ -76,7 +83,7 @@ fn add_required_segs(asset_path: &std::path::Path) -> Result<()> { let mut no_bytes: Vec<u8> = vec![0; 50]; // enough bytes to be valid no_bytes.splice(16..20, C2PA_MARKER); // cai UUID signature let aio = JpegIO {}; - aio.save_cai_store(asset_path, &no_bytes)?; + aio.write_cai(stream, &no_bytes)?; } } else { return Err(Error::UnsupportedType); @@ -135,6 +142,7 @@ fn delete_cai_segments(jpeg: &mut img_parts::jpeg::Jpeg) -> Result<()> { } Ok(()) } + pub struct JpegIO {} impl CAILoader for JpegIO { @@ -145,6 +153,7 @@ impl CAILoader for JpegIO { // load the bytes let mut buf: Vec<u8> = Vec::new(); + asset_reader.seek(SeekFrom::Start(0))?; asset_reader.read_to_end(&mut buf).map_err(Error::IoError)?; let dimg_opt = DynImage::from_bytes(buf.into()) @@ -224,17 +233,14 @@ impl CAILoader for JpegIO { } } -impl AssetIO for JpegIO { - fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> { - let mut f = File::open(asset_path)?; - - self.read_cai(&mut f) - } - - fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> { - let input = read(asset_path).map_err(wrap_io_err)?; - - let mut jpeg = Jpeg::from_bytes(input.into()).map_err(|_err| Error::EmbeddingError)?; +impl CAIWriter for JpegIO { + fn write_cai(&self, stream: &mut dyn CAIReadWrite, store_bytes: &[u8]) -> Result<()> { + //fn write_cai<W: Write>(buf: Vec<u8>, writer: W, store_bytes: &[u8]) -> Result<()> { + let mut buf = Vec::new(); + // read the whole asset + stream.seek(SeekFrom::Start(0))?; + stream.read_to_end(&mut buf).map_err(Error::IoError)?; + let mut jpeg = Jpeg::from_bytes(buf.into()).map_err(|_err| Error::EmbeddingError)?; // remove existing CAI segments delete_cai_segments(&mut jpeg)?; @@ -283,34 +289,31 @@ impl AssetIO for JpegIO { jpeg.segments_mut().insert(seg, app11_segment); // we put this in the beginning... } - let output = std::fs::OpenOptions::new() - .read(true) - .write(true) - .truncate(true) - .open(asset_path) - .map_err(Error::IoError)?; - + stream.seek(SeekFrom::Start(0))?; jpeg.encoder() - .write_to(output) + .write_to(stream) .map_err(|_err| Error::InvalidAsset("JPEG write error".to_owned()))?; - Ok(()) } - fn get_object_locations( + fn get_object_locations_from_stream( &self, - asset_path: &std::path::Path, + stream: &mut dyn CAIReadWrite, ) -> Result<Vec<HashObjectPositions>> { - // make sure the file has the required segments so we can generate all the required offsets - add_required_segs(asset_path)?; - let mut cai_en: Vec<u8> = Vec::new(); let mut cai_seg_cnt: u32 = 0; let mut positions: Vec<HashObjectPositions> = Vec::new(); let mut curr_offset = 2; // start after JPEG marker - let buf = read(asset_path)?; + // make sure the file has the required segments so we can generate all the required offsets + add_required_segs_to_stream(stream)?; + + let mut buf: Vec<u8> = Vec::new(); + stream.seek(SeekFrom::Start(0))?; + stream.read_to_end(&mut buf).map_err(Error::IoError)?; + stream.seek(SeekFrom::Start(0))?; + let dimg = DynImage::from_bytes(buf.into()) .map_err(|e| Error::OtherError(Box::new(e)))? .ok_or(Error::UnsupportedType)?; @@ -374,7 +377,7 @@ impl AssetIO for JpegIO { length: seg.len_with_entropy(), htype: HashBlockObjectType::Xmp, }; - // todo: pick the app1 that is the xmp (not cruical as it gets hashed either way) + // todo: pick the app1 that is the xmp (not crucial as it gets hashed either way) positions.push(v); } _ => { @@ -396,9 +399,43 @@ impl AssetIO for JpegIO { Ok(positions) } +} + +impl AssetIO for JpegIO { + fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>> { + let mut f = File::open(asset_path)?; + + self.read_cai(&mut f) + } + + fn save_cai_store(&self, asset_path: &std::path::Path, store_bytes: &[u8]) -> Result<()> { + let mut stream = std::fs::OpenOptions::new() + .read(true) + .write(true) + //.truncate(true) + .open(asset_path) + .map_err(Error::IoError)?; + + self.write_cai(&mut stream, store_bytes)?; + + Ok(()) + } + + fn get_object_locations( + &self, + asset_path: &std::path::Path, + ) -> Result<Vec<HashObjectPositions>> { + let mut file = std::fs::OpenOptions::new() + .read(true) + .write(true) + .open(asset_path) + .map_err(Error::IoError)?; + + self.get_object_locations_from_stream(&mut file) + } fn remove_cai_store(&self, asset_path: &std::path::Path) -> Result<()> { - let input = read(asset_path).map_err(wrap_io_err)?; + let input = std::fs::read(asset_path).map_err(Error::IoError)?; let mut jpeg = Jpeg::from_bytes(input.into()).map_err(|_err| Error::EmbeddingError)?; diff --git a/sdk/src/asset_io.rs b/sdk/src/asset_io.rs @@ -13,7 +13,7 @@ use std::{ fmt, - io::{Read, Seek}, + io::{Read, Seek, Write}, path::Path, }; @@ -42,8 +42,15 @@ pub trait CAIRead: Read + Seek {} impl CAIRead for std::fs::File {} impl CAIRead for std::io::Cursor<&[u8]> {} +impl CAIRead for std::io::Cursor<&mut [u8]> {} impl CAIRead for std::io::Cursor<Vec<u8>> {} +pub trait CAIReadWrite: CAIRead + Write {} + +impl CAIReadWrite for std::fs::File {} +impl CAIReadWrite for std::io::Cursor<&mut [u8]> {} +impl CAIReadWrite for std::io::Cursor<Vec<u8>> {} + // Interface for in memory CAI reading pub trait CAILoader { // Return entire CAI block as Vec<u8> @@ -53,6 +60,15 @@ pub trait CAILoader { fn read_xmp(&self, asset_reader: &mut dyn CAIRead) -> Option<String>; } +pub trait CAIWriter { + fn write_cai(&self, stream: &mut dyn CAIReadWrite, store_bytes: &[u8]) -> Result<()>; + + fn get_object_locations_from_stream( + &self, + stream: &mut dyn CAIReadWrite, + ) -> Result<Vec<HashObjectPositions>>; +} + pub trait AssetIO { // Return entire CAI block as Vec<u8> fn read_cai_store(&self, asset_path: &Path) -> Result<Vec<u8>>; diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs @@ -647,7 +647,7 @@ impl Claim { } // crate private function to allow for patching a data hash with final contents - #[cfg(feature = "file_io")] + #[cfg(feature = "sign")] pub(crate) fn update_data_hash(&mut self, mut data_hash: DataHash) -> Result<()> { let mut replacement_assertion = data_hash.to_assertion()?; diff --git a/sdk/src/create_signer.rs b/sdk/src/create_signer.rs @@ -15,7 +15,7 @@ //! The `create_signer` module provides a way to obtain a [`Signer`] //! instance for each signing format supported by this crate. - +#[cfg(feature = "file_io")] use std::path::Path; use crate::{ @@ -65,6 +65,7 @@ pub fn from_keys( /// * `pkey_path` - Path to the private key file /// * `alg` - Format for signing /// * `tsa_url` - Optional URL for a timestamp authority +#[cfg(feature = "file_io")] pub fn from_files<P: AsRef<Path>>( signcert_path: P, pkey_path: P, diff --git a/sdk/src/error.rs b/sdk/src/error.rs @@ -244,7 +244,7 @@ pub enum Error { CborError(#[from] serde_cbor::Error), #[error(transparent)] - #[cfg(feature = "file_io")] + #[cfg(feature = "openssl")] OpenSslError(#[from] openssl::error::ErrorStack), #[error(transparent)] @@ -257,11 +257,12 @@ pub enum Error { /// A specialized `Result` type for C2PA toolkit operations. pub type Result<T> = std::result::Result<T, Error>; +#[cfg(feature = "file_io")] pub(crate) fn wrap_io_err(err: std::io::Error) -> Error { Error::IoError(err) } -#[cfg(feature = "file_io")] +#[cfg(feature = "sign")] pub(crate) fn wrap_openssl_err(err: openssl::error::ErrorStack) -> Error { Error::OpenSslError(err) } diff --git a/sdk/src/hashed_uri.rs b/sdk/src/hashed_uri.rs @@ -50,7 +50,7 @@ impl HashedUri { self.hash.clone() } - #[cfg(feature = "file_io")] + #[cfg(feature = "sign")] pub(crate) fn update_hash(&mut self, hash: Vec<u8>) { self.hash = hash; } diff --git a/sdk/src/jumbf_io.rs b/sdk/src/jumbf_io.rs @@ -21,7 +21,7 @@ use crate::{ asset_handlers::{ bmff_io::BmffIO, c2pa_io::C2paIO, jpeg_io::JpegIO, png_io::PngIO, tiff_io::TiffIO, }, - asset_io::{AssetIO, CAILoader, HashObjectPositions}, + asset_io::{AssetIO, CAILoader, CAIReadWrite, CAIWriter, HashObjectPositions}, error::{Error, Result}, }; @@ -86,6 +86,30 @@ pub fn load_jumbf_from_memory(asset_type: &str, data: &[u8]) -> Result<Vec<u8>> Ok(cai_block) } +/// writes the jumbf data in store_bytes +/// reads an asset of asset_type from reader, adds jumbf data and then writes to writer +pub fn save_jumbf_to_stream( + asset_type: &str, + stream: &mut dyn CAIReadWrite, + store_bytes: &[u8], +) -> Result<()> { + match get_caiwriter_handler(asset_type) { + Some(asset_handler) => asset_handler.write_cai(stream, store_bytes), + None => Err(Error::UnsupportedType), + } +} + +/// writes the jumbf data in store_bytes into an asset in data and the updatedcar data +pub fn save_jumbf_to_memory( + asset_type: &str, + data: Vec<u8>, + store_bytes: &[u8], +) -> Result<Vec<u8>> { + let mut stream = Cursor::new(data); + save_jumbf_to_stream(asset_type, &mut stream, store_bytes)?; + Ok(stream.into_inner()) +} + pub fn get_assetio_handler(ext: &str) -> Option<Box<dyn AssetIO>> { let ext = ext.to_lowercase(); match ext.as_ref() { @@ -117,6 +141,24 @@ pub fn get_cailoader_handler(asset_type: &str) -> Option<Box<dyn CAILoader>> { } } +pub fn get_caiwriter_handler(asset_type: &str) -> Option<Box<dyn CAIWriter>> { + let asset_type = asset_type.to_lowercase(); + match asset_type.as_ref() { + // "c2pa" | "application/c2pa" | "application/x-c2pa-manifest-store" => { + // Some(Box::new(C2paIO {})) + // } + "jpg" | "jpeg" | "image/jpeg" => Some(Box::new(JpegIO {})), + // "png" | "image/png" => Some(Box::new(PngIO {})), + // "avif" | "heif" | "heic" | "mp4" | "m4a" | "application/mp4" | "audio/mp4" + // | "image/avif" | "image/heic" | "image/heif" | "video/mp4" + // if cfg!(feature = "bmff") && !cfg!(target_arch = "wasm32") => + // { + // Some(Box::new(BmffIO::new(&asset_type))) + // } + _ => None, + } +} + pub fn get_file_extension(path: &Path) -> Option<String> { let ext_osstr = path.extension()?; @@ -225,6 +267,16 @@ pub fn object_locations(in_path: &Path) -> Result<Vec<HashObjectPositions>> { } } +pub fn object_locations_from_stream( + format: &str, + stream: &mut dyn CAIReadWrite, +) -> Result<Vec<HashObjectPositions>> { + match get_caiwriter_handler(format) { + Some(handler) => handler.get_object_locations_from_stream(stream), + _ => Err(Error::UnsupportedType), + } +} + /// removes the C2PA JUMBF from an asset /// Note: Use with caution since this deletes C2PA data /// It is useful when creating remote manifests from embedded manifests diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs @@ -86,7 +86,7 @@ pub mod assertions; mod cose_validator; -#[cfg(feature = "file_io")] +#[cfg(feature = "sign")] pub mod create_signer; mod error; @@ -110,14 +110,14 @@ mod signing_alg; #[cfg(feature = "file_io")] pub use ingredient::{DefaultOptions, IngredientOptions}; pub use signing_alg::{SigningAlg, UnknownAlgorithmError}; -#[cfg(feature = "file_io")] +#[cfg(feature = "sign")] pub(crate) mod ocsp_utils; -#[cfg(feature = "file_io")] +#[cfg(feature = "sign")] mod openssl; -#[cfg(feature = "file_io")] +#[cfg(feature = "sign")] mod signer; -#[cfg(feature = "file_io")] +#[cfg(feature = "sign")] pub use signer::Signer; #[cfg(feature = "async_signer")] pub use signer::{AsyncSigner, RemoteSigner}; @@ -129,7 +129,7 @@ pub(crate) mod asset_handlers; pub(crate) mod asset_io; pub(crate) mod claim; -#[cfg(feature = "file_io")] +#[cfg(feature = "sign")] pub mod cose_sign; #[cfg(all(feature = "xmp_write", feature = "file_io"))] diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs @@ -20,8 +20,6 @@ use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::Value; use uuid::Uuid; -#[cfg(feature = "file_io")] -use crate::Signer; use crate::{ assertion::{AssertionBase, AssertionData}, assertions::{labels, Actions, CreativeWork, Exif, Thumbnail, User, UserCbor}, @@ -32,6 +30,8 @@ use crate::{ store::Store, Ingredient, ManifestAssertion, ManifestAssertionKind, }; +#[cfg(feature = "sign")] +use crate::{asset_io::CAIReadWrite, Signer}; #[cfg(all(feature = "async_signer", feature = "file_io"))] use crate::{AsyncSigner, RemoteSigner}; @@ -742,6 +742,53 @@ impl Manifest { store.save_to_asset(source_path.as_ref(), signer, dest_path.as_ref()) } + /// Embed a signed manifest into a stream using a supplied signer. + /// returns the bytes of the manifest that was embedded + #[cfg(feature = "sign")] + pub fn embed_from_memory( + &mut self, + format: &str, + asset: &[u8], + signer: &dyn Signer, + ) -> Result<Vec<u8>> { + // first make a copy of the asset that will contain our modified result + // todo:: see if we can pass a trait with to_vec support like we to for Strings + let asset = asset.to_vec(); + let mut stream = std::io::Cursor::new(asset); + self.embed_stream(format, &mut stream, signer)?; + Ok(stream.into_inner()) + } + + /// Embed a signed manifest into a stream using a supplied signer. + /// returns the bytes of the manifest that was embedded + #[cfg(feature = "sign")] + pub fn embed_stream( + &mut self, + format: &str, + stream: &mut dyn CAIReadWrite, + signer: &dyn Signer, + ) -> Result<Vec<u8>> { + self.set_format(format); + // todo:: read instance_id from xmp from stream + self.set_instance_id(format!("xmp:iid:{}", Uuid::new_v4())); + + // generate thumbnail if we don't already have one + if self.thumbnail().is_none() { + #[cfg(feature = "add_thumbnails")] + if let Ok((format, image)) = + crate::utils::thumbnail::make_thumbnail_from_stream(format, stream) + { + self.set_thumbnail(format, image); + } + } + + // convert the manifest to a store + let mut store = self.to_store()?; + + // sign and write our store to to the output image file + store.save_to_stream(format, stream, signer) + } + /// Embed a signed manifest into the target file using a supplied [`AsyncSigner`]. #[cfg(feature = "file_io")] #[cfg(feature = "async_signer")] @@ -804,6 +851,8 @@ pub(crate) mod tests { #[cfg(feature = "file_io")] use tempfile::tempdir; + #[cfg(feature = "sign")] + use crate::utils::test::temp_signer; use crate::{ assertions::{c2pa_action, Action, Actions}, utils::test::TEST_VC, @@ -813,9 +862,7 @@ pub(crate) mod tests { use crate::{ status_tracker::{DetailedStatusTracker, StatusTracker}, store::Store, - utils::test::{ - fixture_path, temp_dir_path, temp_fixture_path, temp_signer, TEST_SMALL_JPEG, - }, + utils::test::{fixture_path, temp_dir_path, temp_fixture_path, TEST_SMALL_JPEG}, validation_status, Ingredient, }; @@ -1158,7 +1205,7 @@ pub(crate) mod tests { //let manifest_store = crate::ManifestStore::from_file(&sidecar).expect("from_file"); let manifest_store = - crate::ManifestStore::from_bytes("c2pa", c2pa_data, true).expect("from_bytes"); + crate::ManifestStore::from_bytes("c2pa", &c2pa_data, true).expect("from_bytes"); assert_eq!(manifest_store.active_label(), Some("MyLabel")); assert_eq!( manifest_store.get_active().unwrap().title().unwrap(), @@ -1166,6 +1213,46 @@ pub(crate) mod tests { ); } + #[test] + #[cfg(feature = "sign")] + fn test_embed_stream() { + use crate::assertions::User; + let image = include_bytes!("../tests/fixtures/earth_apollo17.jpg"); + // convert buffer to cursor with Read/Write/Seek capability + let mut stream = std::io::Cursor::new(image.to_vec()); + // let mut image = image.to_vec(); + // let mut stream = std::io::Cursor::new(image.as_mut_slice()); + + let mut manifest = Manifest::new("my_app".to_owned()); + manifest.set_title("EmbedStream"); + manifest + .add_assertion(&User::new( + "org.contentauth.mylabel", + r#"{"my_tag":"Anything I want"}"#, + )) + .unwrap(); + + let signer = temp_signer(); + // Embed a manifest using the signer. + manifest + .embed_stream("jpeg", &mut stream, &signer) + .expect("embed_stream"); + + // get the updated image + let image = stream.into_inner(); + + let manifest_store = + crate::ManifestStore::from_bytes("jpeg", &image, true).expect("from_bytes"); + assert_eq!( + manifest_store.get_active().unwrap().title().unwrap(), + "EmbedStream" + ); + #[cfg(feature = "add_thumbnails")] + assert!(manifest_store.get_active().unwrap().thumbnail().is_some()); + + println!("{}", manifest_store); + } + #[cfg(feature = "file_io")] #[actix::test] /// Verify that an ingredient with error is reported on the ingredient and not on the manifest_store diff --git a/sdk/src/manifest_store.rs b/sdk/src/manifest_store.rs @@ -116,10 +116,10 @@ impl ManifestStore { } /// generate a Store from a format string and bytes - pub fn from_bytes(format: &str, image_bytes: Vec<u8>, verify: bool) -> Result<ManifestStore> { + pub fn from_bytes(format: &str, image_bytes: &[u8], verify: bool) -> Result<ManifestStore> { let mut validation_log = DetailedStatusTracker::new(); - Store::load_from_memory(format, &image_bytes, verify, &mut validation_log) + Store::load_from_memory(format, image_bytes, verify, &mut validation_log) .map(|store| Self::from_store(&store, &mut validation_log)) } @@ -146,12 +146,12 @@ impl ManifestStore { /// Loads a ManifestStore from a file pub async fn from_bytes_async( format: &str, - image_bytes: Vec<u8>, + image_bytes: &[u8], verify: bool, ) -> Result<ManifestStore> { let mut validation_log = DetailedStatusTracker::new(); - Store::load_from_memory_async(format, &image_bytes, verify, &mut validation_log) + Store::load_from_memory_async(format, image_bytes, verify, &mut validation_log) .await .map(|store| Self::from_store(&store, &mut validation_log)) } @@ -283,8 +283,7 @@ mod tests { fn manifest_report_image() { let image_bytes = include_bytes!("../tests/fixtures/CA.jpg"); - let manifest_store = - ManifestStore::from_bytes("image/jpeg", image_bytes.to_vec(), true).unwrap(); + let manifest_store = ManifestStore::from_bytes("image/jpeg", image_bytes, true).unwrap(); assert!(!manifest_store.manifests.is_empty()); assert!(manifest_store.active_label().is_some()); @@ -302,10 +301,9 @@ mod tests { async fn manifest_report_image_async() { let image_bytes = include_bytes!("../tests/fixtures/CA.jpg"); - let manifest_store = - ManifestStore::from_bytes_async("image/jpeg", image_bytes.to_vec(), true) - .await - .unwrap(); + let manifest_store = ManifestStore::from_bytes_async("image/jpeg", image_bytes, true) + .await + .unwrap(); assert!(!manifest_store.manifests.is_empty()); assert!(manifest_store.active_label().is_some()); diff --git a/sdk/src/openssl/ec_signer.rs b/sdk/src/openssl/ec_signer.rs @@ -11,8 +11,6 @@ // specific language governing permissions and limitations under // each license. -use std::{fs, path::Path}; - use openssl::{ ec::EcKey, hash::MessageDigest, @@ -26,7 +24,7 @@ use x509_parser::der_parser::{ use super::check_chain_order; use crate::{ - error::{wrap_io_err, wrap_openssl_err, Error, Result}, + error::{wrap_openssl_err, Error, Result}, signer::ConfigurableSigner, Signer, SigningAlg, }; @@ -45,18 +43,6 @@ pub struct EcSigner { } impl ConfigurableSigner for EcSigner { - fn from_files<P: AsRef<Path>>( - signcert_path: P, - pkey_path: P, - alg: SigningAlg, - tsa_url: Option<String>, - ) -> Result<Self> { - let signcert = fs::read(signcert_path).map_err(wrap_io_err)?; - let pkey = fs::read(pkey_path).map_err(wrap_io_err)?; - - Self::from_signcert_and_pkey(&signcert, &pkey, alg, tsa_url) - } - fn from_signcert_and_pkey( signcert: &[u8], pkey: &[u8], @@ -206,6 +192,7 @@ fn der_to_p1363(data: &[u8], alg: SigningAlg) -> Result<Vec<u8>> { } #[cfg(test)] +#[cfg(feature = "file_io")] mod tests { #![allow(clippy::unwrap_used)] diff --git a/sdk/src/openssl/ec_validator.rs b/sdk/src/openssl/ec_validator.rs @@ -72,6 +72,7 @@ fn wrap_openssl_err(err: openssl::error::ErrorStack) -> Error { } #[cfg(test)] +#[cfg(feature = "file_io")] mod tests { #![allow(clippy::unwrap_used)] use super::*; diff --git a/sdk/src/openssl/ed_signer.rs b/sdk/src/openssl/ed_signer.rs @@ -11,8 +11,6 @@ // specific language governing permissions and limitations under // each license. -use std::{fs, path::Path}; - use openssl::{ pkey::{PKey, Private}, x509::X509, @@ -35,18 +33,6 @@ pub struct EdSigner { } impl ConfigurableSigner for EdSigner { - fn from_files<P: AsRef<Path>>( - signcert_path: P, - pkey_path: P, - alg: SigningAlg, - tsa_url: Option<String>, - ) -> Result<Self> { - let signcert = fs::read(signcert_path).map_err(wrap_io_err)?; - let pkey = fs::read(pkey_path).map_err(wrap_io_err)?; - - Self::from_signcert_and_pkey(&signcert, &pkey, alg, tsa_url) - } - fn from_signcert_and_pkey( signcert: &[u8], pkey: &[u8], @@ -113,15 +99,12 @@ impl Signer for EdSigner { } } -fn wrap_io_err(err: std::io::Error) -> Error { - Error::IoError(err) -} - fn wrap_openssl_err(err: openssl::error::ErrorStack) -> Error { Error::OpenSslError(err) } #[cfg(test)] +#[cfg(feature = "file_io")] mod tests { #![allow(clippy::unwrap_used)] use super::*; diff --git a/sdk/src/openssl/ed_validator.rs b/sdk/src/openssl/ed_validator.rs @@ -39,6 +39,7 @@ impl CoseValidator for EdValidator { } #[cfg(test)] +#[cfg(feature = "file_io")] mod tests { #![allow(clippy::unwrap_used)] diff --git a/sdk/src/openssl/rsa_signer.rs b/sdk/src/openssl/rsa_signer.rs @@ -11,7 +11,7 @@ // specific language governing permissions and limitations under // each license. -use std::{cell::Cell, fs, path::Path}; +use std::cell::Cell; //use extfmt::Hexlify; use openssl::{ @@ -66,18 +66,6 @@ impl RsaSigner { } impl ConfigurableSigner for RsaSigner { - fn from_files<P: AsRef<Path>>( - signcert_path: P, - pkey_path: P, - alg: SigningAlg, - tsa_url: Option<String>, - ) -> Result<Self> { - let signcert = fs::read(signcert_path).map_err(wrap_io_err)?; - let pkey = fs::read(pkey_path).map_err(wrap_io_err)?; - - Self::from_signcert_and_pkey(&signcert, &pkey, alg, tsa_url) - } - fn from_signcert_and_pkey( signcert: &[u8], pkey: &[u8], @@ -197,10 +185,6 @@ impl Signer for RsaSigner { } } -fn wrap_io_err(err: std::io::Error) -> Error { - Error::IoError(err) -} - fn wrap_openssl_err(err: openssl::error::ErrorStack) -> Error { Error::OpenSslError(err) } diff --git a/sdk/src/openssl/temp_signer.rs b/sdk/src/openssl/temp_signer.rs @@ -31,8 +31,10 @@ #![allow(clippy::panic)] #![allow(clippy::unwrap_used)] +#[cfg(feature = "file_io")] use std::path::{Path, PathBuf}; +#[cfg(feature = "file_io")] use crate::{ openssl::{EcSigner, EdSigner, RsaSigner}, signer::ConfigurableSigner, @@ -57,6 +59,7 @@ use crate::{ /// # Panics /// /// Can panic if unable to invoke OpenSSL executable properly. +#[cfg(feature = "file_io")] pub fn get_ec_signer<P: AsRef<Path>>( path: P, alg: SigningAlg, @@ -101,6 +104,7 @@ pub fn get_ec_signer<P: AsRef<Path>>( /// # Panics /// /// Can panic if unable to invoke OpenSSL executable properly. +#[cfg(feature = "file_io")] pub fn get_ed_signer<P: AsRef<Path>>( path: P, alg: SigningAlg, @@ -142,6 +146,7 @@ pub fn get_ed_signer<P: AsRef<Path>>( /// # Panics /// /// Can panic if unable to invoke OpenSSL executable properly. +#[cfg(feature = "file_io")] pub fn get_rsa_signer<P: AsRef<Path>>( path: P, alg: SigningAlg, diff --git a/sdk/src/salt.rs b/sdk/src/salt.rs @@ -55,13 +55,13 @@ impl Default for DefaultSalt { impl SaltGenerator for DefaultSalt { fn generate_salt(&self) -> Option<Vec<u8>> { - #[cfg(feature = "file_io")] // auto generation not supported on wasm + #[cfg(feature = "sign")] // auto generation not supported on wasm { let mut salt = vec![0; self.salt_len]; openssl::rand::rand_bytes(&mut salt).ok()?; Some(salt) } - #[cfg(not(feature = "file_io"))] + #[cfg(not(feature = "sign"))] { None } diff --git a/sdk/src/signer.rs b/sdk/src/signer.rs @@ -10,9 +10,9 @@ // implied. See the LICENSE-MIT and LICENSE-APACHE files for the // specific language governing permissions and limitations under // each license. - +#[cfg(feature = "file_io")] +use crate::Error; use crate::{Result, SigningAlg}; - /// The `Signer` trait generates a cryptographic signature over a byte array. /// /// This trait exists to allow the signature mechanism to be extended. @@ -48,12 +48,18 @@ pub trait Signer { /// Trait to allow loading of signing credential from external sources pub(crate) trait ConfigurableSigner: Signer + Sized { /// Create signer form credential files + #[cfg(feature = "file_io")] fn from_files<P: AsRef<std::path::Path>>( signcert_path: P, pkey_path: P, alg: SigningAlg, tsa_url: Option<String>, - ) -> Result<Self>; + ) -> Result<Self> { + let signcert = std::fs::read(signcert_path).map_err(Error::IoError)?; + let pkey = std::fs::read(pkey_path).map_err(Error::IoError)?; + + Self::from_signcert_and_pkey(&signcert, &pkey, alg, tsa_url) + } /// Create signer from credentials data fn from_signcert_and_pkey( diff --git a/sdk/src/store.rs b/sdk/src/store.rs @@ -11,29 +11,26 @@ // specific language governing permissions and limitations under // each license. +#[cfg(feature = "sign")] +use std::io::SeekFrom; use std::{collections::HashMap, io::Cursor}; #[cfg(feature = "file_io")] use std::{fs, path::Path}; -#[cfg(feature = "file_io")] use log::error; #[cfg(all(feature = "xmp_write", feature = "file_io"))] use crate::embedded_xmp; #[cfg(feature = "async_signer")] use crate::AsyncSigner; -#[cfg(feature = "file_io")] +#[cfg(feature = "sign")] use crate::{ assertion::AssertionData, - assertions::{BmffHash, DataHash, DataMap, ExclusionsMap, SubsetMap}, - asset_io::{HashBlockObjectType, HashObjectPositions}, - claim::RemoteManifest, + assertions::DataHash, + asset_io::{CAIReadWrite, HashBlockObjectType, HashObjectPositions}, cose_sign::cose_sign, cose_validator::verify_cose, - jumbf_io::{ - get_file_extension, get_supported_file_extension, is_bmff_format, load_jumbf_from_file, - object_locations, remove_jumbf_from_file, save_jumbf_to_file, - }, + jumbf_io::{object_locations_from_stream, save_jumbf_to_stream}, utils::{ hash_utils::{hash256, Exclusion}, patch::patch_bytes, @@ -51,6 +48,15 @@ use crate::{ status_tracker::{log_item, OneShotStatusTracker, StatusTracker}, validation_status, ManifestStoreReport, }; +#[cfg(feature = "file_io")] +use crate::{ + assertions::{BmffHash, DataMap, ExclusionsMap, SubsetMap}, + claim::RemoteManifest, + jumbf_io::{ + get_file_extension, get_supported_file_extension, is_bmff_format, load_jumbf_from_file, + object_locations, remove_jumbf_from_file, save_jumbf_to_file, + }, +}; const MANIFEST_STORE_EXT: &str = "c2pa"; // file extension for external manifests @@ -318,7 +324,7 @@ impl Store { // Returns placeholder that will be searched for and replaced // with actual signature data. - #[cfg(feature = "file_io")] + #[cfg(feature = "sign")] fn sign_claim_placeholder(&self, claim: &Claim, min_reserve_size: usize) -> Vec<u8> { let placeholder_str = format!("signature placeholder:{}", claim.label()); let mut placeholder = hash256(placeholder_str.as_bytes()).as_bytes().to_vec(); @@ -330,7 +336,7 @@ impl Store { } /// Sign the claim and return signature. - #[cfg(feature = "file_io")] + #[cfg(feature = "sign")] pub fn sign_claim( &self, claim: &Claim, @@ -429,7 +435,7 @@ impl Store { self.claims_map.insert(label, index); } - #[cfg(feature = "file_io")] + #[cfg(feature = "sign")] fn add_assertion_to_jumbf_store( store: &mut CAIAssertionStore, claim_assertion: &ClaimAssertion, @@ -591,7 +597,7 @@ impl Store { self.to_jumbf_internal(signer.reserve_size()) } - #[cfg(feature = "file_io")] + #[cfg(feature = "sign")] fn to_jumbf_internal(&self, min_reserve_size: usize) -> Result<Vec<u8>> { // Create the CAI block. let mut cai_block = Cai::new(); @@ -1222,13 +1228,26 @@ impl Store { block_locations: &mut Vec<HashObjectPositions>, calc_hashes: bool, ) -> Result<Vec<DataHash>> { + let mut file = std::fs::File::open(asset_path)?; + Self::generate_data_hashes_for_stream(&mut file, alg, block_locations, calc_hashes) + } + + // generate a list of AssetHashes based on the location of objects in the stream + #[cfg(feature = "sign")] + fn generate_data_hashes_for_stream( + stream: &mut dyn CAIReadWrite, + alg: &str, + block_locations: &mut Vec<HashObjectPositions>, + calc_hashes: bool, + ) -> Result<Vec<DataHash>> { if block_locations.is_empty() { let out: Vec<DataHash> = vec![]; return Ok(out); } - let metadata = asset_path.metadata().map_err(crate::error::wrap_io_err)?; - let file_len: u64 = metadata.len(); + let stream_len = stream.seek(SeekFrom::End(0))?; + stream.seek(SeekFrom::Start(0))?; + let mut hashes: Vec<DataHash> = Vec::new(); // sort blocks by offset @@ -1253,7 +1272,7 @@ impl Store { } } - if block_end as u64 > file_len { + if block_end as u64 > stream_len { return Err(Error::BadParam( "data hash exclusions out of range".to_string(), )); @@ -1266,7 +1285,7 @@ impl Store { dh.add_exclusion(Exclusion::new(block_start, block_end - block_start)); } if calc_hashes { - dh.gen_hash(asset_path)?; + dh.gen_hash_from_stream(stream)?; } else { match alg { "sha256" => dh.set_hash([0u8; 32].to_vec()), @@ -1408,6 +1427,35 @@ impl Store { Ok(()) } + /// Embed the claims store as jumbf into a stream. Updates XMP with provenance record. + /// When called, the stream should contain an asset matching format. + /// on return, the stream will contain the new manifest signed with signer + /// This directly modifies the asset in stream, backup stream first if you need to preserve it. + #[cfg(feature = "sign")] + pub fn save_to_stream( + &mut self, + format: &str, + stream: &mut dyn CAIReadWrite, + signer: &dyn Signer, + ) -> Result<Vec<u8>> { + let jumbf_bytes = self.start_save_stream(format, stream, signer.reserve_size())?; + + let pc = self.provenance_claim().ok_or(Error::ClaimEncoding)?; + let sig = self.sign_claim(pc, signer, signer.reserve_size())?; + let sig_placeholder = self.sign_claim_placeholder(pc, signer.reserve_size()); + + match self.finish_save_stream(jumbf_bytes, format, stream, sig, &sig_placeholder) { + Ok((s, m)) => { + // save sig so store is up to date + let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; + pc_mut.set_signature_val(s); + + Ok(m) + } + Err(e) => Err(e), + } + } + /// Embed the claims store as jumbf into an asset. Updates XMP with provenance record. #[cfg(feature = "file_io")] pub fn save_to_asset( @@ -1576,6 +1624,91 @@ impl Store { } } + #[cfg(feature = "sign")] + fn start_save_stream( + &mut self, + format: &str, + stream: &mut dyn CAIReadWrite, + reserve_size: usize, + ) -> Result<Vec<u8>> { + let mut data; + // 1) Add DC provenance XMP + + let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; + // todo:: stream support for XMP write + + // 2) Get hash ranges if needed, do not generate for update manifests + let mut hash_ranges = object_locations_from_stream(format, stream)?; + let hashes: Vec<DataHash> = if pc.update_manifest() { + Vec::new() + } else { + Store::generate_data_hashes_for_stream(stream, pc.alg(), &mut hash_ranges, false)? + }; + + // add the placeholder data hashes to provenance claim so that the required space is reserved + for mut hash in hashes { + // add padding to account for possible cbor expansion of final DataHash + let padding: Vec<u8> = vec![0x0; 10]; + hash.add_padding(padding); + + pc.add_assertion(&hash)?; + } + + // 3) Generate in memory CAI jumbf block + // and write preliminary jumbf store to file + // source and dest the same so save_jumbf_to_file will use the same file since we have already cloned + data = self.to_jumbf_internal(reserve_size)?; + let jumbf_size = data.len(); + save_jumbf_to_stream(format, stream, &data)?; + + // 4) determine final object locations and patch the asset hashes with correct offset + // replace the source with correct asset hashes so that the claim hash will be correct + let pc = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?; + + // get the final hash ranges, but not for update manifests + let mut new_hash_ranges = object_locations_from_stream(format, stream)?; + let updated_hashes = if pc.update_manifest() { + Vec::new() + } else { + Store::generate_data_hashes_for_stream(stream, pc.alg(), &mut new_hash_ranges, true)? + }; + + // patch existing claim hash with updated data + for hash in updated_hashes { + pc.update_data_hash(hash)?; + } + + // regenerate the jumbf because the cbor changed + data = self.to_jumbf_internal(reserve_size)?; + if jumbf_size != data.len() { + return Err(Error::JumbfCreationError); + } + + Ok(data) // return JUMBF data + } + + #[cfg(feature = "sign")] + fn finish_save_stream( + &self, + mut jumbf_bytes: Vec<u8>, + format: &str, + stream: &mut dyn CAIReadWrite, + sig: Vec<u8>, + sig_placeholder: &[u8], + ) -> Result<(Vec<u8>, Vec<u8>)> { + if sig_placeholder.len() != sig.len() { + return Err(Error::CoseSigboxTooSmall); + } + + patch_bytes(&mut jumbf_bytes, sig_placeholder, &sig) + .map_err(|_| Error::JumbfCreationError)?; + + // re-save to file + save_jumbf_to_stream(format, stream, &jumbf_bytes)?; + + Ok((sig, jumbf_bytes)) + } + #[cfg(feature = "file_io")] fn start_save( &mut self, @@ -3127,6 +3260,35 @@ pub mod tests { } } + #[actix::test] + #[cfg(feature = "sign")] + async fn test_jumbf_generation_stream() { + let file_buffer = include_bytes!("../tests/fixtures/earth_apollo17.jpg").to_vec(); + // convert buffer to cursor with Read/Write/Seek capability + let mut buf_io = Cursor::new(file_buffer); + + // Create claims store. + let mut store = Store::new(); + + // Create a new claim. + let claim1 = create_test_claim().unwrap(); + + let signer = temp_signer(); + + store.commit_claim(claim1).unwrap(); + + store.save_to_stream("jpeg", &mut buf_io, &signer).unwrap(); + + // convert our cursor back into a buffer + let result = buf_io.into_inner(); + + // make sure we can read from new file + let mut report = DetailedStatusTracker::new(); + let _new_store = Store::load_from_memory("jpeg", &result, true, &mut report).unwrap(); + + // std::fs::write("target/test.jpg", result).unwrap(); + } + #[test] #[cfg(feature = "file_io")] fn test_tiff_jumbf_generation() { diff --git a/sdk/src/time_stamp.rs b/sdk/src/time_stamp.rs @@ -127,7 +127,7 @@ pub fn get_ta_url() -> Option<String> { /// internal only function to work around bug in serialization of TimeStampResponse /// so we just return the data directly -#[cfg(feature = "file_io")] +#[cfg(feature = "sign")] fn time_stamp_request_http( url: &str, request: &crate::asn1::rfc3161::TimeStampReq, @@ -194,7 +194,8 @@ fn time_stamp_request_http( /// /// This is a wrapper around [time_stamp_request_http] that constructs the low-level /// ASN.1 request object with reasonable defaults. -#[cfg(feature = "file_io")] + +#[cfg(feature = "sign")] fn time_stamp_message_http( url: &str, message: &[u8], @@ -238,7 +239,7 @@ impl std::ops::Deref for TimeStampResponse { impl TimeStampResponse { /// Whether the time stamp request was successful. - #[cfg(feature = "file_io")] + #[cfg(feature = "sign")] pub fn is_success(&self) -> bool { matches!( self.0.status.status, @@ -289,7 +290,7 @@ impl TimeStampResponse { /// Generate TimeStamp based on rfc3161 using "data" as MessageImprint and return raw TimeStampRsp bytes #[allow(unused_variables)] pub fn timestamp_data(url: &str, data: &[u8]) -> Result<Vec<u8>> { - #[cfg(feature = "file_io")] + #[cfg(feature = "sign")] { let ts = time_stamp_message_http(url, data, x509_certificate::DigestAlgorithm::Sha256)?; @@ -298,12 +299,11 @@ pub fn timestamp_data(url: &str, data: &[u8]) -> Result<Vec<u8>> { Ok(ts) } - #[cfg(not(feature = "file_io"))] + #[cfg(not(feature = "sign"))] { Err(Error::WasmNoCrypto) } } - pub fn gt_to_datetime( gt: x509_certificate::asn1time::GeneralizedTime, ) -> chrono::DateTime<chrono::Utc> { @@ -392,7 +392,7 @@ impl TstContainer { } } - #[cfg(feature = "file_io")] + #[cfg(feature = "sign")] pub fn add_token(&mut self, token: TstToken) { self.tst_tokens.push(token); } @@ -405,7 +405,7 @@ impl Default for TstContainer { } /// Wrap rfc3161 TimeStampRsp in COSE sigTst object -#[cfg(feature = "file_io")] +#[cfg(feature = "sign")] pub fn make_cose_timestamp(ts_data: &[u8]) -> TstContainer { let token = TstToken { val: ts_data.to_vec(), diff --git a/sdk/src/utils/hash_utils.rs b/sdk/src/utils/hash_utils.rs @@ -27,7 +27,7 @@ use serde::{Deserialize, Serialize}; // direct sha functions use sha2::{Digest, Sha256, Sha384, Sha512}; -use crate::{Error, Result}; +use crate::{asset_io::CAIReadWrite, Error, Result}; const MAX_HASH_BUF: usize = 256 * 1024 * 1024; // cap memory usage to 256MB @@ -172,12 +172,22 @@ pub fn hash_by_alg(alg: &str, data: &[u8], exclusions: Option<Vec<Exclusion>>) - } } -// Return hash bytes for assset using desired hashing algorithm. +// Return hash bytes for asset using desired hashing algorithm. pub fn hash_asset_by_alg( alg: &str, asset_path: &Path, exclusions: Option<Vec<Exclusion>>, ) -> Result<Vec<u8>> { + let mut file = File::open(asset_path)?; + hash_stream_by_alg(alg, &mut file, exclusions) +} + +// Return hash bytes for stream using desired hashing algorithm. +pub fn hash_stream_by_alg( + alg: &str, + data: &mut dyn CAIReadWrite, + exclusions: Option<Vec<Exclusion>>, +) -> Result<Vec<u8>> { use Hasher::*; let mut hasher_enum = match alg { "sha256" => SHA256(Sha256::new()), @@ -192,7 +202,6 @@ pub fn hash_asset_by_alg( } }; - let mut data = File::open(asset_path)?; let data_len = data.seek(SeekFrom::End(0))?; data.seek(SeekFrom::Start(0))?; @@ -303,7 +312,7 @@ pub fn hash_asset_by_alg( Ok(Hasher::finalize(hasher_enum)) } -// verify the hash using the specified alogrithm +// verify the hash using the specified algorithm pub fn verify_by_alg( alg: &str, hash: &[u8], diff --git a/sdk/src/utils/mod.rs b/sdk/src/utils/mod.rs @@ -16,7 +16,7 @@ pub(crate) mod cbor_types; pub(crate) mod hash_utils; #[allow(dead_code)] // for wasm build pub(crate) mod patch; -#[cfg(all(feature = "file_io", feature = "add_thumbnails"))] +#[cfg(all(feature = "add_thumbnails", any(feature = "file_io", feature = "sign")))] pub(crate) mod thumbnail; pub(crate) mod time_it; #[allow(dead_code)] // for wasm builds diff --git a/sdk/src/utils/test.rs b/sdk/src/utils/test.rs @@ -25,12 +25,9 @@ use crate::{ Result, }; #[cfg(feature = "file_io")] -use crate::{ - create_signer, - openssl::RsaSigner, - signer::{ConfigurableSigner, Signer}, - SigningAlg, -}; +use crate::{create_signer, Signer}; +#[cfg(feature = "sign")] +use crate::{openssl::RsaSigner, signer::ConfigurableSigner, SigningAlg}; pub const TEST_SMALL_JPEG: &str = "earth_apollo17.jpg"; @@ -196,7 +193,7 @@ pub fn temp_fixture_path(temp_dir: &TempDir, file_name: &str) -> PathBuf { /// Can panic if the certs cannot be read. (This function should only /// be used as part of testing infrastructure.) #[cfg(feature = "file_io")] -pub fn temp_signer() -> RsaSigner { +pub fn temp_signer_file() -> RsaSigner { #![allow(clippy::expect_used)] let mut sign_cert_path = fixture_path("certs"); sign_cert_path.push("ps256"); @@ -210,6 +207,16 @@ pub fn temp_signer() -> RsaSigner { .expect("get_temp_signer") } +#[cfg(feature = "sign")] +pub fn temp_signer() -> RsaSigner { + #![allow(clippy::expect_used)] + let sign_cert = include_bytes!("../../tests/fixtures/certs/ps256.pub").to_vec(); + let pem_key = include_bytes!("../../tests/fixtures/certs/ps256.pem").to_vec(); + + RsaSigner::from_signcert_and_pkey(&sign_cert, &pem_key, SigningAlg::Ps256, None) + .expect("get_temp_signer") +} + /// Create a [`Signer`] instance for a specific algorithm that can be used for testing purposes. /// /// # Returns diff --git a/sdk/src/utils/thumbnail.rs b/sdk/src/utils/thumbnail.rs @@ -11,19 +11,21 @@ // specific language governing permissions and limitations under // each license. -use image::ImageFormat; +use image::{io::Reader, ImageFormat}; use crate::Result; +#[cfg(feature = "sign")] +use crate::{asset_io::CAIReadWrite, Error}; + +// max edge size allowed in pixels for thumbnail creation +const THUMBNAIL_LONGEST_EDGE: u32 = 1024; +const THUMBNAIL_JPEG_QUALITY: u8 = 80; /// utility to generate a thumbnail from a file at path /// returns Result (format, image_bits) if successful, otherwise Error pub fn make_thumbnail(path: &std::path::Path) -> Result<(String, Vec<u8>)> { let format = ImageFormat::from_path(path)?; - // max edge size allowed in pixels for thumbnail creation - const THUMBNAIL_LONGEST_EDGE: u32 = 1024; - const THUMBNAIL_JPEG_QUALITY: u8 = 80; // JPEG quality 1-100 - let mut img = image::open(path)?; let longest_edge = THUMBNAIL_LONGEST_EDGE; @@ -47,3 +49,41 @@ pub fn make_thumbnail(path: &std::path::Path) -> Result<(String, Vec<u8>)> { let format = content_type.to_owned(); Ok((format, cursor.into_inner())) } + +/// utility to generate a thumbnail from a file at path +/// returns Result (format, image_bits) if successful, otherwise Error +#[cfg(feature = "sign")] +pub fn make_thumbnail_from_stream( + format: &str, + stream: &mut dyn CAIReadWrite, +) -> Result<(String, Vec<u8>)> { + let format = ImageFormat::from_extension(format) + .or_else(|| ImageFormat::from_mime_type(format)) + .ok_or(Error::UnsupportedType)?; + + let reader = Reader::with_format(std::io::BufReader::new(stream), format); + let mut img = reader.decode()?; + + let longest_edge = THUMBNAIL_LONGEST_EDGE; + + // generate a thumbnail image scaled down and in jpeg format + if img.width() > longest_edge || img.height() > longest_edge { + img = img.thumbnail(longest_edge, longest_edge); + } + + // for png files, use png thumbnails for transparency + // for other supported types try a jpeg thumbnail + let (output_format, content_type) = match format { + ImageFormat::Png => (image::ImageOutputFormat::Png, "image/png"), + _ => ( + image::ImageOutputFormat::Jpeg(THUMBNAIL_JPEG_QUALITY), + "image/jpeg", + ), + }; + let thumbnail_bits = Vec::new(); + let mut cursor = std::io::Cursor::new(thumbnail_bits); + img.write_to(&mut cursor, output_format)?; + + let format = content_type.to_owned(); + Ok((format, cursor.into_inner())) +} diff --git a/sdk/src/validator.rs b/sdk/src/validator.rs @@ -13,7 +13,7 @@ use chrono::{DateTime, Utc}; -#[cfg(feature = "file_io")] +#[cfg(feature = "sign")] use crate::openssl::{EcValidator, EdValidator, RsaValidator}; use crate::{Result, SigningAlg}; @@ -51,8 +51,8 @@ impl CoseValidator for DummyValidator { // • RS512 RSASSA-PKCS1-v1_5 using SHA-512 // • ED25519 Edwards Curve ED25519 -/// return validator for supported C2PA algorthms -#[cfg(feature = "file_io")] +/// return validator for supported C2PA algorithms +#[cfg(feature = "openssl")] pub(crate) fn get_validator(alg: SigningAlg) -> Box<dyn CoseValidator> { match alg { SigningAlg::Es256 | SigningAlg::Es384 | SigningAlg::Es512 => { @@ -68,7 +68,7 @@ pub(crate) fn get_validator(alg: SigningAlg) -> Box<dyn CoseValidator> { } } -#[cfg(not(feature = "file_io"))] +#[cfg(not(feature = "sign"))] #[allow(dead_code)] pub(crate) fn get_validator(_alg: SigningAlg) -> Box<dyn CoseValidator> { Box::new(DummyValidator)