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:
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)