Skip to content

Commit ec8095b

Browse files
authored
Gpeacock/embed_remote_settings (contentauth#460)
* Change settings Manifest to Builder. * Ensure sign to stream respects options. * add missing writer for c2pa_io. * Add streaming bmff and streaming remote_url support. * PR feedback fixes.
1 parent e28dc87 commit ec8095b

File tree

9 files changed

+383
-215
lines changed

9 files changed

+383
-215
lines changed

V2_API_NOTES.md renamed to 2024_API_NOTES.md

+15-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## V2 API Notes
1+
## 2024 API Notes
22

33
### Goals
44
Provide a consistent flexible well tested API focusing on core functionality.
@@ -12,7 +12,7 @@ Provide a consistent flexible well tested API focusing on core functionality.
1212
- Enable builds for cameras and other embedded environments.
1313
- Provide a consistent model for setting runtime options.
1414
- Write unit tests, integration tests and documentation for all the v2 APIs.
15-
- Keep v1 to v2 porting as simple as possible.
15+
- Keep porting as simple as possible.
1616

1717

1818
### Resource References
@@ -50,6 +50,19 @@ The source asset is the asset that we will hash and sign. It can the output from
5050

5151
If there is no parent ingredient defined, and the source has a manifest store, the sdk will generate a parent ingredient from the parent.
5252

53+
### Remote URLs and embedding
54+
The default operation of c2pa signing is to embed a c2pa manifest store into an asset.
55+
We also return the c2pa manifest store so that it can be written to a sidecar or uploaded to a remote service.
56+
- The API supports embedding a remote url reference into the asset.
57+
- The remote URL is stored in different ways depending on the asset, but is often stored in XMP data.
58+
- The remote URL must be added to the asset before signing so that it can be hashed along with the asset.
59+
- Not all file formats support embedding remote URLs or embedding manifests stores.
60+
- If you embed a manifest or a remote URL, a new asset will be created with the new data embedded.
61+
- If you don't embed, then the original asset is unmodified and there is no need to write one out.
62+
- The remote url can be set with builder.remote_url.
63+
- If embedding is not needed, set the builder.no_embed flag to true.
64+
65+
5366
## Testing
5467
We need a more comprehensive set of tests for the rust codebase.
5568

sdk/src/assertions/bmff_hash.rs

+16-20
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ use std::{
1717
fmt, fs,
1818
io::{BufReader, Cursor, SeekFrom},
1919
ops::Deref,
20-
path::{Path, PathBuf},
20+
path::Path,
2121
};
2222

2323
use mp4::*;
@@ -37,8 +37,8 @@ use crate::{
3737
cbor_types::UriT,
3838
utils::{
3939
hash_utils::{
40-
concat_and_hash, hash_asset_by_alg, hash_stream_by_alg, vec_compare,
41-
verify_stream_by_alg, HashRange, Hasher,
40+
concat_and_hash, hash_stream_by_alg, vec_compare, verify_stream_by_alg, HashRange,
41+
Hasher,
4242
},
4343
merkle::C2PAMerkleTree,
4444
},
@@ -253,9 +253,6 @@ pub struct BmffHash {
253253
#[serde(skip_serializing)]
254254
url: Option<UriT>, // deprecated in V2 and not to be used
255255

256-
#[serde(skip)]
257-
pub path: PathBuf,
258-
259256
#[serde(skip)]
260257
bmff_version: usize,
261258
}
@@ -271,7 +268,6 @@ impl BmffHash {
271268
merkle: None,
272269
name: Some(name.to_string()),
273270
url,
274-
path: PathBuf::new(),
275271
bmff_version: ASSERTION_CREATION_VERSION,
276272
}
277273
}
@@ -326,22 +322,24 @@ impl BmffHash {
326322
}
327323

328324
/// Generate the hash value for the asset using the range from the BmffHash.
329-
pub fn gen_hash(&mut self, asset_path: &Path) -> crate::error::Result<()> {
330-
self.hash = Some(ByteBuf::from(self.hash_from_asset(asset_path)?));
331-
self.path = PathBuf::from(asset_path);
325+
pub fn gen_hash_from_stream(&mut self, stream: &mut dyn CAIRead) -> crate::error::Result<()> {
326+
self.hash = Some(ByteBuf::from(self.hash_from_stream(stream)?));
327+
//self.path = PathBuf::from(asset_path);
332328
Ok(())
333329
}
334330

335-
/// Generate the hash again.
336-
pub fn regen_hash(&mut self) -> crate::error::Result<()> {
337-
let p = self.path.clone();
338-
self.hash = Some(ByteBuf::from(self.hash_from_asset(p.as_path())?));
331+
/// Generate the hash value for the asset using the range from the BmffHash.
332+
#[cfg(feature = "file_io")]
333+
pub fn gen_hash(&mut self, asset_path: &Path) -> crate::error::Result<()> {
334+
let mut file = std::fs::File::open(asset_path)?;
335+
self.hash = Some(ByteBuf::from(self.hash_from_stream(&mut file)?));
336+
//self.path = PathBuf::from(asset_path);
339337
Ok(())
340338
}
341339

342-
/// Generate the asset hash from a file asset using the constructed
340+
/// Generate the asset hash from an asset stream using the constructed
343341
/// start and length values.
344-
fn hash_from_asset(&mut self, asset_path: &Path) -> crate::error::Result<Vec<u8>> {
342+
fn hash_from_stream(&mut self, stream: &mut dyn CAIRead) -> crate::error::Result<Vec<u8>> {
345343
if self.is_remote_hash() {
346344
return Err(Error::BadParam(
347345
"asset hash is remote, not yet supported".to_owned(),
@@ -356,11 +354,9 @@ impl BmffHash {
356354
let bmff_exclusions = &self.exclusions;
357355

358356
// convert BMFF exclusion map to flat exclusion list
359-
let mut data = fs::File::open(asset_path)?;
360-
let exclusions =
361-
bmff_to_jumbf_exclusions(&mut data, bmff_exclusions, self.bmff_version > 1)?;
357+
let exclusions = bmff_to_jumbf_exclusions(stream, bmff_exclusions, self.bmff_version > 1)?;
362358

363-
let hash = hash_asset_by_alg(&alg, asset_path, Some(exclusions))?;
359+
let hash = hash_stream_by_alg(&alg, stream, Some(exclusions), true)?;
364360

365361
if hash.is_empty() {
366362
Err(Error::BadParam("could not generate data hash".to_string()))

sdk/src/asset_handlers/c2pa_io.rs

+4
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ impl AssetIO for C2paIO {
120120
self
121121
}
122122

123+
fn get_writer(&self, asset_type: &str) -> Option<Box<dyn CAIWriter>> {
124+
Some(Box::new(C2paIO::new(asset_type)))
125+
}
126+
123127
fn supported_types(&self) -> &[&str] {
124128
&SUPPORTED_TYPES
125129
}

sdk/src/builder.rs

+77-33
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ pub struct Builder {
193193
#[serde(flatten)]
194194
pub definition: ManifestDefinition,
195195

196+
/// Optional remote URL for the manifest
197+
pub remote_url: Option<String>,
198+
199+
// If true, the manifest store will not be embedded in the asset on sign
200+
pub no_embed: bool,
201+
196202
/// container for binary assets (like thumbnails)
197203
#[serde(skip)]
198204
resources: ResourceStore,
@@ -481,14 +487,15 @@ impl Builder {
481487
claim.add_claim_generator_info(claim_info);
482488
}
483489

484-
// if let Some(remote_op) = &self.remote_manifest {
485-
// match remote_op {
486-
// RemoteManifest::NoRemote => (),
487-
// RemoteManifest::SideCar => claim.set_external_manifest(),
488-
// RemoteManifest::Remote(r) => claim.set_remote_manifest(r)?,
489-
// RemoteManifest::EmbedWithRemote(r) => claim.set_embed_remote_manifest(r)?,
490-
// };
491-
// }
490+
if let Some(remote_url) = &self.remote_url {
491+
if self.no_embed {
492+
claim.set_remote_manifest(remote_url)?;
493+
} else {
494+
claim.set_embed_remote_manifest(remote_url)?;
495+
}
496+
} else if self.no_embed {
497+
claim.set_external_manifest()
498+
}
492499

493500
if let Some(title) = definition.title.as_ref() {
494501
claim.set_title(Some(title.to_owned()));
@@ -657,7 +664,9 @@ impl Builder {
657664
where
658665
R: Read + Seek + ?Sized,
659666
{
660-
if self.definition.thumbnail.is_none() {
667+
// check settings to see if we should auto generate a thumbnail
668+
let auto_thumbnail = crate::settings::get_settings_value::<bool>("builder.auto_thumbnail")?;
669+
if self.definition.thumbnail.is_none() && auto_thumbnail {
661670
stream.rewind()?;
662671
if let Ok((format, image)) =
663672
crate::utils::thumbnail::make_thumbnail_from_stream(format, stream)
@@ -778,7 +787,7 @@ mod tests {
778787
use wasm_bindgen_test::*;
779788

780789
use super::*;
781-
use crate::{manifest_store::ManifestStore, utils::test::temp_signer};
790+
use crate::{utils::test::temp_signer, Reader};
782791
#[cfg(target_arch = "wasm32")]
783792
wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
784793

@@ -826,6 +835,8 @@ mod tests {
826835
.to_string()
827836
}
828837

838+
#[cfg(not(target_arch = "wasm32"))]
839+
const TEST_IMAGE_CLEAN: &[u8] = include_bytes!("../tests/fixtures/IMG_0003.jpg");
829840
const TEST_IMAGE: &[u8] = include_bytes!("../tests/fixtures/CA.jpg");
830841

831842
#[test]
@@ -951,21 +962,20 @@ mod tests {
951962
zipped.rewind().unwrap();
952963
let mut _builder = Builder::from_archive(&mut zipped).unwrap();
953964

954-
// sign the ManifestStoreBuilder and write it to the output stream
965+
// sign and write to the output stream
955966
let signer = temp_signer();
956967
builder
957968
.sign(format, &mut source, &mut dest, signer.as_ref())
958969
.unwrap();
959970

960971
// read and validate the signed manifest store
961972
dest.rewind().unwrap();
962-
let manifest_store =
963-
ManifestStore::from_stream(format, &mut dest, true).expect("from_bytes");
973+
let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes");
964974

965975
println!("{}", manifest_store);
966976
assert!(manifest_store.validation_status().is_none());
967-
assert!(manifest_store.get_active().is_some());
968-
let manifest = manifest_store.get_active().unwrap();
977+
assert!(manifest_store.active_manifest().is_some());
978+
let manifest = manifest_store.active_manifest().unwrap();
969979
assert_eq!(manifest.title().unwrap(), "Test_Manifest");
970980
let test_assertion: TestAssertion = manifest.find_assertion("org.life.meaning").unwrap();
971981
assert_eq!(test_assertion.answer, 42);
@@ -984,17 +994,17 @@ mod tests {
984994
.add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE))
985995
.unwrap();
986996

987-
// sign the ManifestStoreBuilder and write it to the output stream
997+
// sign and write to the output stream
988998
let signer = temp_signer();
989999
builder.sign_file(source, &dest, signer.as_ref()).unwrap();
9901000

9911001
// read and validate the signed manifest store
992-
let manifest_store = ManifestStore::from_file(&dest).expect("from_bytes");
1002+
let manifest_store = Reader::from_file(&dest).expect("from_bytes");
9931003

9941004
println!("{}", manifest_store);
9951005
assert!(manifest_store.validation_status().is_none());
9961006
assert_eq!(
997-
manifest_store.get_active().unwrap().title().unwrap(),
1007+
manifest_store.active_manifest().unwrap().title().unwrap(),
9981008
"Test_Manifest"
9991009
);
10001010
}
@@ -1008,15 +1018,14 @@ mod tests {
10081018
"sample1.webp",
10091019
"TUSCANY.TIF",
10101020
"sample1.svg",
1011-
//"APC_0808.dng",
10121021
"sample1.wav",
10131022
"test.avi",
1014-
//"sample1.mp3",
1015-
//"sample1.avif",
1016-
//"sample1.heic",
1017-
//"sample1.heif",
1018-
//"video1.mp4",
1019-
//"cloud_manifest.c2pa",
1023+
"sample1.mp3",
1024+
"sample1.avif",
1025+
"sample1.heic",
1026+
"sample1.heif",
1027+
"video1.mp4",
1028+
"cloud_manifest.c2pa",
10201029
];
10211030
for file_name in TESTFILES {
10221031
let extension = file_name.split('.').last().unwrap();
@@ -1036,21 +1045,23 @@ mod tests {
10361045
.add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE))
10371046
.unwrap();
10381047

1039-
// sign the ManifestStoreBuilder and write it to the output stream
1048+
// sign and write to the output stream
10401049
let signer = temp_signer();
10411050
builder
10421051
.sign(format, &mut source, &mut dest, signer.as_ref())
10431052
.unwrap();
10441053

10451054
// read and validate the signed manifest store
10461055
dest.rewind().unwrap();
1047-
let manifest_store =
1048-
ManifestStore::from_stream(format, &mut dest, true).expect("from_bytes");
1056+
let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes");
10491057

10501058
println!("{}", manifest_store);
1051-
assert!(manifest_store.validation_status().is_none());
1059+
if format != "c2pa" {
1060+
// c2pa files will not validate since they have no associated asset
1061+
assert!(manifest_store.validation_status().is_none());
1062+
}
10521063
assert_eq!(
1053-
manifest_store.get_active().unwrap().title().unwrap(),
1064+
manifest_store.active_manifest().unwrap().title().unwrap(),
10541065
"Test_Manifest"
10551066
);
10561067

@@ -1091,15 +1102,48 @@ mod tests {
10911102

10921103
// read and validate the signed manifest store
10931104
dest.rewind().unwrap();
1094-
let manifest_store =
1095-
ManifestStore::from_stream(format, &mut dest, true).expect("from_bytes");
1105+
let manifest_store = Reader::from_stream(format, &mut dest).expect("from_bytes");
10961106

10971107
println!("{}", manifest_store);
10981108
#[cfg(not(target_arch = "wasm32"))] // skip this until we get wasm async signing working
10991109
assert!(manifest_store.validation_status().is_none());
11001110
assert_eq!(
1101-
manifest_store.get_active().unwrap().title().unwrap(),
1111+
manifest_store.active_manifest().unwrap().title().unwrap(),
11021112
"Test_Manifest"
11031113
);
11041114
}
1115+
1116+
#[test]
1117+
#[cfg(not(target_arch = "wasm32"))]
1118+
fn test_builder_remote_url() {
1119+
let mut source = Cursor::new(TEST_IMAGE_CLEAN);
1120+
let mut dest = Cursor::new(Vec::new());
1121+
1122+
let mut builder = Builder::from_json(&manifest_json()).unwrap();
1123+
builder.remote_url = Some("http://my_remote_url".to_string());
1124+
builder.no_embed = true;
1125+
1126+
builder
1127+
.add_resource("thumbnail1.jpg", Cursor::new(TEST_IMAGE))
1128+
.unwrap();
1129+
1130+
// sign the ManifestStoreBuilder and write it to the output stream
1131+
let signer = temp_signer();
1132+
let manifest_data = builder
1133+
.sign("image/jpeg", &mut source, &mut dest, signer.as_ref())
1134+
.unwrap();
1135+
1136+
// check to make sure we have a remote url and no manifest data
1137+
dest.set_position(0);
1138+
let _err = c2pa::Reader::from_stream("image/jpeg", &mut dest).expect_err("from_bytes");
1139+
1140+
// now validate the manifest against the written asset
1141+
dest.set_position(0);
1142+
let reader =
1143+
c2pa::Reader::from_manifest_data_and_stream(&manifest_data, "image/jpeg", &mut dest)
1144+
.expect("from_bytes");
1145+
1146+
println!("{}", reader.json());
1147+
assert!(reader.validation_status().is_none());
1148+
}
11051149
}

sdk/src/claim.rs

-2
Original file line numberDiff line numberDiff line change
@@ -938,15 +938,13 @@ impl Claim {
938938
}
939939

940940
// Crate private function to allow for patching a BMFF hash with final contents.
941-
#[cfg(feature = "file_io")]
942941
pub(crate) fn update_bmff_hash(&mut self, bmff_hash: BmffHash) -> Result<()> {
943942
self.replace_assertion(bmff_hash.to_assertion()?)
944943
}
945944

946945
// Patch an existing assertion with new contents.
947946
//
948947
// `replace_with` should match in name and size of an existing assertion.
949-
#[cfg(feature = "file_io")]
950948
pub(crate) fn replace_assertion(&mut self, replace_with: Assertion) -> Result<()> {
951949
self.update_assertion(
952950
replace_with,

0 commit comments

Comments
 (0)