commit aa0b276b32d17a2aa50acfadbdc13d7a67f53a90
parent af59b429bdc729687f4439b34104a3d104d37a5f
Author: mauricefisher64 <92736594+mauricefisher64@users.noreply.github.com>
Date: Thu, 11 Aug 2022 21:45:25 -0400
Support validating remote and external manifest stores (#108)
* Treat 'meta' box as standard container
Support for more BMFF container types
Handle bad FourCC names
Add bad signer unit test
* clippy fixes
* External manifest generation
* More detailed unit test
Bug fixes
Slight speed up
* test smaller chunk size for hashing
* Only generate output upon success.
Auto cleanup temp content
* cleanup
* WASM build fixes
* code review fixes
* Fix typo
* Changes based on feedback
* Support Info for constructor
Put remote manifest behind xmp_write feature
Split external manifest into two functions
* One more suggestion
* formatting
* formatting
* Fix for build failure when feature xmp_write is missing
* Support for loading/verifying remote and external manifests
Add support for embedding manifest and external reference into same file
Remove obsolete XMP usages
Code cleanup
* Expose external manifest options
* Put remote fetching behind feature flag
* Fix cut and paste bug
* Fix typo causing feature disablement
* remove unneeded code
* fix formatting
* another formatting issue
* more formatting
* Try again to make formatting happy
* Fix bad error message
* One more attempt to fix formatting complaints
* Fixes for new clippy
* fmt made happy
* more new clippy
* Try to fix Ubuntu failure
* Try another Ubuntu fix
* Ubuntu fix
Diffstat:
25 files changed, 311 insertions(+), 243 deletions(-)
diff --git a/README.md b/README.md
@@ -66,6 +66,7 @@ The Rust SDK crate provides:
* `serialize_thumbnails` includes binary thumbnail data in the [Serde](https://serde.rs/) serialization output.
* `xmp_write` enables updating XMP on embed with the `dcterms:provenance` field. (Requires [xmp_toolkit](https://crates.io/crates/xmp_toolkit).)
* `no_interleaved_io` forces fully-synchronous I/O; otherwise, the SDK uses threaded I/O for some operations to improve performance.
+* `fetch_remote_manifests` enables the verification step to retrieve externally referenced manifest stores. External manifests are only fetched if there is no embedded manifest store and no locally adjacent .c2pa manifest store file of the same name.
## License
diff --git a/sdk/Cargo.toml b/sdk/Cargo.toml
@@ -25,6 +25,7 @@ file_io = ["openssl"]
serialize_thumbnails = []
xmp_write = ["xmp_toolkit"]
no_interleaved_io = ["file_io"]
+fetch_remote_manifests = ["file_io"]
# 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/asn1/rfc3161.rs b/sdk/src/asn1/rfc3161.rs
@@ -192,7 +192,7 @@ impl PkiStatusInfo {
pub fn encode_ref(&self) -> impl Values + '_ {
encode::sequence((
- (&self.status).encode(),
+ self.status.encode(),
self.status_string
.as_ref()
.map(|status_string| status_string.encode_ref()),
diff --git a/sdk/src/assertion.rs b/sdk/src/assertion.rs
@@ -174,7 +174,7 @@ pub trait AssertionJson: Serialize + DeserializeOwned + AssertionBase {
/// the Assertion type (see spec).
/// for Json assertions the data is a Json string and Vec<u8> for
/// binary data and json data to be cbor encoded.
-#[derive(Deserialize, Serialize, PartialEq, Clone)]
+#[derive(Deserialize, Serialize, PartialEq, Eq, Clone)]
pub enum AssertionData {
Json(String), // json encoded data
Binary(Vec<u8>), // binary data
@@ -212,7 +212,7 @@ impl fmt::Debug for AssertionData {
// contain its AssertionData. For the User Assertion type we
// allow a String to set the label. The AssertionData contains
// the data payload for the assertion and the version number for its schema (if supported).
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Assertion {
label: String,
version: Option<usize>,
diff --git a/sdk/src/assertions/actions.rs b/sdk/src/assertions/actions.rs
@@ -66,7 +66,7 @@ pub mod c2pa_action {
/// the action.
///
/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>.
-#[derive(Deserialize, Serialize, Debug, PartialEq)]
+#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
pub struct Action {
/// The label associated with this action. See ([`c2pa_action`]).
action: String,
@@ -229,7 +229,7 @@ impl Action {
/// other information such as what software performed the action.
///
/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_actions>.
-#[derive(Deserialize, Serialize, Debug, PartialEq)]
+#[derive(Deserialize, Serialize, Debug, PartialEq, Eq)]
pub struct Actions {
/// A list of [`Action`]s.
pub actions: Vec<Action>,
diff --git a/sdk/src/assertions/bmff_hash.rs b/sdk/src/assertions/bmff_hash.rs
@@ -32,7 +32,7 @@ use crate::{
const ASSERTION_CREATION_VERSION: usize = 1;
-#[derive(Serialize, Deserialize, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct ExclusionsMap {
pub xpath: String,
pub length: Option<u32>,
@@ -57,7 +57,7 @@ impl ExclusionsMap {
}
}
-#[derive(Serialize, Deserialize, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct MerkleMap {
#[serde(rename = "uniqueId")]
pub unique_id: u32,
@@ -76,21 +76,21 @@ pub struct MerkleMap {
pub hashes: Vec<ByteBuf>,
}
-#[derive(Serialize, Deserialize, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct DataMap {
pub offset: u32,
#[serde(with = "serde_bytes")]
pub value: Vec<u8>,
}
-#[derive(Serialize, Deserialize, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct SubsetMap {
pub offset: u32,
pub length: u32,
}
/// Helper class to create BmffHash assertion. (These are auto-generated by the SDK.)
-#[derive(Serialize, Deserialize, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct BmffHash {
exclusions: Vec<ExclusionsMap>,
@@ -234,14 +234,8 @@ impl BmffHash {
}
}
- pub fn verify_hash(&self, asset_path: &Path, alg: Option<String>) -> Result<()> {
- let curr_alg = match &self.alg {
- Some(a) => a.clone(),
- None => match alg {
- Some(a) => a,
- None => "sha256".to_string(),
- },
- };
+ pub fn verify_hash(&self, asset_path: &Path, alg: Option<&str>) -> Result<()> {
+ let curr_alg = alg.unwrap_or("sha256");
let bmff_exclusions = &self.exclusions;
@@ -249,7 +243,7 @@ impl BmffHash {
let mut data = fs::File::open(asset_path)?;
let exclusions = bmff_to_jumbf_exclusions(&mut data, bmff_exclusions)?;
- if verify_asset_by_alg(&curr_alg, &self.hash, asset_path, Some(exclusions)) {
+ if verify_asset_by_alg(curr_alg, &self.hash, asset_path, Some(exclusions)) {
Ok(())
} else {
Err(Error::HashMismatch("Hashes do not match".to_owned()))
diff --git a/sdk/src/assertions/data_hash.rs b/sdk/src/assertions/data_hash.rs
@@ -27,7 +27,7 @@ use crate::{
const ASSERTION_CREATION_VERSION: usize = 1;
/// Helper class to create DataHash assertion
-#[derive(Serialize, Deserialize, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct DataHash {
#[serde(skip_serializing_if = "Option::is_none")]
pub exclusions: Option<Vec<Exclusion>>,
@@ -192,22 +192,16 @@ impl DataHash {
/// Used to verify a DataHash against an asset.
#[allow(dead_code)] // used in tests
- pub fn verify_hash(&self, asset_path: &Path, alg: Option<String>) -> Result<()> {
+ pub fn verify_hash(&self, asset_path: &Path, alg: Option<&str>) -> Result<()> {
if self.is_remote_hash() {
return Err(Error::BadParam("asset hash is remote".to_owned()));
}
- let curr_alg = match &self.alg {
- Some(a) => a.clone(),
- None => match alg {
- Some(a) => a,
- None => "sha256".to_string(),
- },
- };
+ let curr_alg = alg.unwrap_or("sha256");
let exclusions = self.exclusions.as_ref().cloned();
- if verify_asset_by_alg(&curr_alg, &self.hash, asset_path, exclusions) {
+ if verify_asset_by_alg(curr_alg, &self.hash, asset_path, exclusions) {
Ok(())
} else {
Err(Error::HashMismatch("Hashes do not match".to_owned()))
diff --git a/sdk/src/assertions/ingredient.rs b/sdk/src/assertions/ingredient.rs
@@ -24,7 +24,7 @@ use crate::{
const ASSERTION_CREATION_VERSION: usize = 1;
// Used to differentiate a parent from a component
-#[derive(Serialize, Deserialize, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)]
pub enum Relationship {
#[serde(rename = "parentOf")]
ParentOf,
diff --git a/sdk/src/assertions/metadata.rs b/sdk/src/assertions/metadata.rs
@@ -27,7 +27,7 @@ use crate::{
const ASSERTION_CREATION_VERSION: usize = 1;
/// The Metadata structure can be used as part of other assertions or on its own to reference others
-#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct Metadata {
#[serde(rename = "reviewRatings", skip_serializing_if = "Option::is_none")]
reviews: Option<Vec<ReviewRating>>,
@@ -155,7 +155,7 @@ pub mod c2pa_source {
}
/// A description of the source for assertion data
-#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)]
+#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq, Eq)]
pub struct DataSource {
/// A value from among the enumerated list indicating the source of the assertion.
#[serde(rename = "type")]
@@ -193,7 +193,7 @@ impl DataSource {
}
/// Identifies a person responsible for an action.
-#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq)]
+#[derive(Deserialize, Serialize, Clone, Debug, Default, PartialEq, Eq)]
pub struct Actor {
/// An identifier for a human actor, used when the "type" is `humanEntry.identified`.
#[serde(skip_serializing_if = "Option::is_none")]
@@ -213,7 +213,7 @@ impl Actor {
}
}
-#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum ReviewCode {
#[serde(rename(serialize = "actions.unknownActionsPerformed"))]
ActionsUnknown,
@@ -241,7 +241,7 @@ pub enum ReviewCode {
/// A rating on an [`Assertion`].
///
/// See <https://c2pa.org/specifications/specifications/1.0/specs/C2PA_Specification.html#_claim_review>.
-#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
+#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct ReviewRating {
pub explanation: String,
#[serde(skip_serializing_if = "Option::is_none")]
diff --git a/sdk/src/assertions/user_cbor.rs b/sdk/src/assertions/user_cbor.rs
@@ -19,7 +19,7 @@ use crate::{
};
/// Helper class to create Cbor User assertion
-#[derive(Serialize, Deserialize, Default, Debug, PartialEq)]
+#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
pub struct UserCbor {
label: String,
cbor_data: Vec<u8>,
diff --git a/sdk/src/asset_handlers/bmff_io.rs b/sdk/src/asset_handlers/bmff_io.rs
@@ -69,7 +69,7 @@ impl CAIRead for NamedTempFile {}
macro_rules! boxtype {
($( $name:ident => $value:expr ),*) => {
- #[derive(Clone, Copy, Debug, PartialEq)]
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BoxType {
$( $name, )*
UnknownBox(u32),
@@ -188,7 +188,7 @@ impl BoxHeaderLite {
// Get box type string.
let mut t = [0u8; 4];
t.clone_from_slice(&buf[4..8]);
- let fourcc = String::from_utf8_lossy(&buf[4..8].to_vec()).to_string();
+ let fourcc = String::from_utf8_lossy(&buf[4..8]).to_string();
let typ = u32::from_be_bytes(t);
// Get largesize if size is 1
diff --git a/sdk/src/asset_io.rs b/sdk/src/asset_io.rs
@@ -19,7 +19,7 @@ use std::{
use crate::error::Result;
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub enum HashBlockObjectType {
Cai,
Xmp,
diff --git a/sdk/src/claim.rs b/sdk/src/claim.rs
@@ -53,7 +53,7 @@ pub enum ClaimAssetData<'a> {
ByteData(&'a [u8]),
}
-#[derive(PartialEq, Clone)]
+#[derive(PartialEq, Eq, Clone)]
// helper struct to allow arbitrary order for assertions stored in jumbf. The instance is
// stored separate from the Assertion to allow for late binding to the label. Also,
// we can load assertions in any order and know the position without re-parsing label. We also
@@ -234,11 +234,12 @@ pub enum AssertionStoreJsonFormat {
}
/// Remote manifest options. Use 'set_remote_manifest' to generate external manifests.
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RemoteManifest {
- NoRemote, // No external manifest (default)
+ NoRemote, // No external manifest (default)
SideCar, // Manifest will be saved as a side car file, output asset is untouched.
Remote(String), // Manifest will be saved as a side car file, output asset will contain remote reference
+ EmbedWithRemote(String), // Manifest will be embedded with a remote reference, sidecar will be generated
}
impl Default for RemoteManifest {
@@ -434,13 +435,23 @@ impl Claim {
&mut self,
remote_url: S,
) -> Result<()> {
- url::Url::parse(remote_url.as_ref())
+ let url = url::Url::parse(remote_url.as_ref())
.map_err(|_e| Error::BadParam("remote url is badly formed".to_string()))?;
- self.remote_manifest = RemoteManifest::Remote(remote_url.into());
+ self.remote_manifest = RemoteManifest::Remote(url.to_string());
Ok(())
}
+ pub fn set_embed_remote_manifest<S: Into<String> + AsRef<str>>(
+ &mut self,
+ remote_url: S,
+ ) -> Result<()> {
+ let url = url::Url::parse(remote_url.as_ref())
+ .map_err(|_e| Error::BadParam("remote url is badly formed".to_string()))?;
+ self.remote_manifest = RemoteManifest::EmbedWithRemote(url.to_string());
+
+ Ok(())
+ }
pub fn set_external_manifest(&mut self) {
self.remote_manifest = RemoteManifest::SideCar;
}
@@ -1047,7 +1058,7 @@ impl Claim {
// only verify local hashes here
let hash_result = match asset_data {
ClaimAssetData::PathData(asset_path) => {
- dh.verify_hash(asset_path, Some(claim.alg().to_string()))
+ dh.verify_hash(asset_path, Some(claim.alg()))
}
ClaimAssetData::ByteData(asset_bytes) => {
dh.verify_in_memory_hash(asset_bytes, Some(claim.alg().to_string()))
@@ -1090,7 +1101,7 @@ impl Claim {
let hash_result = match asset_data {
ClaimAssetData::PathData(asset_path) => {
- dh.verify_hash(asset_path, Some(claim.alg().to_string()))
+ dh.verify_hash(asset_path, Some(claim.alg()))
}
ClaimAssetData::ByteData(asset_bytes) => {
dh.verify_in_memory_hash(asset_bytes, Some(claim.alg().to_string()))
diff --git a/sdk/src/error.rs b/sdk/src/error.rs
@@ -54,6 +54,9 @@ pub enum Error {
#[error("claim already signed, no further changes allowed")]
ClaimAlreadySigned,
+ #[error("attempt to add new claim without signing last claim")]
+ ClaimUnsigned,
+
#[error("missing signature box link")]
ClaimMissingSignatureBox,
@@ -166,6 +169,9 @@ pub enum Error {
#[error("required JUMBF box not found")]
JumbfBoxNotFound,
+ #[error("could not fetch the remote manifest")]
+ RemoteManifestFetch(String),
+
#[error("stopped because of logged error")]
LogStop,
diff --git a/sdk/src/hashed_uri.rs b/sdk/src/hashed_uri.rs
@@ -17,7 +17,7 @@ use serde::{Deserialize, Serialize};
/// Hashed Uri stucture as defined by C2PA spec
/// It is annotated to produce the correctly tagged cbor serialization
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct HashedUri {
url: String, // URI stored as tagged cbor
#[serde(skip_serializing_if = "Option::is_none")]
diff --git a/sdk/src/jumbf/boxes.rs b/sdk/src/jumbf/boxes.rs
@@ -1747,7 +1747,7 @@ pub fn unread_bytes<S: Seek>(seeker: &mut S, size: u64) -> JumbfParseResult<()>
/// macro for dealing with the type of a BMFF/JUMBF box
macro_rules! boxtype {
($( $name:ident => $value:expr ),*) => {
- #[derive(Debug, Clone, Copy, PartialEq)]
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoxType {
$( $name, )*
UnknownBox(u32),
diff --git a/sdk/src/jumbf/labels.rs b/sdk/src/jumbf/labels.rs
@@ -106,7 +106,7 @@ pub(crate) fn to_relative_uri(uri: &str) -> String {
let parts: Vec<&str> = raw_uri.split('/').collect();
if parts.len() > 4 && parts[1] == MANIFEST_STORE {
- return format!("{}={}", JUMBF_PREFIX, parts[3..].join("/"));
+ format!("{}={}", JUMBF_PREFIX, parts[3..].join("/"))
} else {
// Doesn't look like an absolute URI, so we'll return it as-is.
uri.to_string()
diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs
@@ -143,7 +143,9 @@ pub(crate) mod store;
pub(crate) mod time_stamp;
pub(crate) mod utils;
pub mod validation_status;
-pub(crate) use utils::{cbor_types, hash_utils, xmp_inmemory_utils};
+#[cfg(feature = "file_io")]
+pub(crate) use utils::xmp_inmemory_utils;
+pub(crate) use utils::{cbor_types, hash_utils};
pub(crate) mod validator;
#[cfg(target_arch = "wasm32")]
pub mod wasm;
diff --git a/sdk/src/manifest.rs b/sdk/src/manifest.rs
@@ -25,7 +25,7 @@ use crate::Signer;
use crate::{
assertion::{AssertionBase, AssertionData},
assertions::{labels, Actions, CreativeWork, Thumbnail, User, UserCbor},
- claim::Claim,
+ claim::{Claim, RemoteManifest},
error::{Error, Result},
jumbf,
salt::DefaultSalt,
@@ -86,6 +86,9 @@ pub struct Manifest {
/// Signature data (only used for reporting)
#[serde(skip_serializing_if = "Option::is_none")]
signature_info: Option<SignatureInfo>,
+
+ #[serde(skip_deserializing, skip_serializing)]
+ remote_manifest: Option<RemoteManifest>,
}
impl Manifest {
@@ -105,6 +108,7 @@ impl Manifest {
redactions: None,
credentials: None,
signature_info: None,
+ remote_manifest: None,
}
}
@@ -187,6 +191,23 @@ impl Manifest {
self.thumbnail = Some((format.into(), thumbnail));
self
}
+ pub fn set_sidecar_manifest(&mut self) -> &mut Self {
+ self.remote_manifest = Some(RemoteManifest::SideCar);
+ self
+ }
+
+ pub fn set_remote_manifest<S: Into<String>>(&mut self, remote_url: S) -> &mut Self {
+ self.remote_manifest = Some(RemoteManifest::Remote(remote_url.into()));
+ self
+ }
+
+ pub fn set_embedded_manifest_with_remote_ref<S: Into<String>>(
+ &mut self,
+ remote_url: S,
+ ) -> &mut Self {
+ self.remote_manifest = Some(RemoteManifest::EmbedWithRemote(remote_url.into()));
+ self
+ }
pub fn signature_info(&self) -> Option<&SignatureInfo> {
self.signature_info.as_ref()
@@ -506,6 +527,15 @@ impl Manifest {
);
let mut claim = Claim::new(&generator, self.vendor.as_deref());
+ if let Some(remote_op) = &self.remote_manifest {
+ match remote_op {
+ RemoteManifest::NoRemote => (),
+ RemoteManifest::SideCar => claim.set_external_manifest(),
+ RemoteManifest::Remote(r) => claim.set_remote_manifest(r)?,
+ RemoteManifest::EmbedWithRemote(r) => claim.set_embed_remote_manifest(r)?,
+ };
+ }
+
if let Some(title) = self.title() {
claim.set_title(Some(title.to_owned()));
}
@@ -1029,11 +1059,7 @@ pub(crate) mod tests {
);
// this would happen on some remote server
- let cose_sign1_box =
- crate::cose_sign::cose_sign_async(&signer, claim_bytes, self.reserve_size())
- .await;
-
- cose_sign1_box
+ crate::cose_sign::cose_sign_async(&signer, claim_bytes, self.reserve_size()).await
}
fn reserve_size(&self) -> usize {
10000
diff --git a/sdk/src/manifest_assertion.rs b/sdk/src/manifest_assertion.rs
@@ -7,7 +7,7 @@ use crate::{
};
/// Assertions in C2PA can be stored in several formats
-#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)]
+#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)]
pub enum ManifestAssertionKind {
Cbor,
Json,
diff --git a/sdk/src/signing_alg.rs b/sdk/src/signing_alg.rs
@@ -22,7 +22,7 @@ use std::{fmt, str::FromStr};
/// > All digital signatures that are stored in a C2PA Manifest shall
/// > be generated using one of the digital signature algorithms and
/// > key types listed as described in this section.
-#[derive(Copy, Clone, Debug, PartialEq)]
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SigningAlg {
/// ECDSA with SHA-256
Es256,
@@ -81,7 +81,7 @@ impl fmt::Display for SigningAlg {
}
}
-#[derive(Debug, PartialEq)]
+#[derive(Debug, PartialEq, Eq)]
/// This error is thrown when converting from a string to [`SigningAlg`]
/// if the algorithm string is unrecognized.
///
diff --git a/sdk/src/store.rs b/sdk/src/store.rs
@@ -31,8 +31,8 @@ use crate::{
cose_sign::cose_sign,
cose_validator::verify_cose,
jumbf_io::{
- get_supported_file_extension, is_bmff_format, load_jumbf_from_file, object_locations,
- save_jumbf_to_file,
+ get_file_extension, get_supported_file_extension, is_bmff_format, load_jumbf_from_file,
+ object_locations, save_jumbf_to_file,
},
utils::{
hash_utils::{hash256, Exclusion},
@@ -49,9 +49,7 @@ use crate::{
jumbf::{self, boxes::*},
jumbf_io::{get_cailoader_handler, load_jumbf_from_memory},
status_tracker::{log_item, OneShotStatusTracker, StatusTracker},
- validation_status,
- xmp_inmemory_utils::extract_provenance,
- ManifestStoreReport,
+ validation_status, ManifestStoreReport,
};
const MANIFEST_STORE_EXT: &str = "c2pa"; // file extension for external manifests
@@ -116,7 +114,7 @@ impl Store {
}
/// Get the provenance if available.
- /// If loaded from an existing asset it will be provenance from that XMP
+ /// If loaded from an existing asset it will be provenance from the last claim.
/// If a new claim is committed that will be the provenance claim
pub fn provenance_path(&self) -> Option<String> {
if self.provenance_path.is_none() {
@@ -142,6 +140,12 @@ impl Store {
/// Add a new Claim to this Store. The function
/// will return the label of the claim.
pub fn commit_claim(&mut self, mut claim: Claim) -> Result<String> {
+ // make sure there is no pending unsigned claim
+ if let Some(pc) = self.provenance_claim() {
+ if pc.signature_val().is_empty() {
+ return Err(Error::ClaimUnsigned);
+ }
+ }
// verify the claim is valid
claim.build()?;
@@ -699,6 +703,10 @@ impl Store {
}
pub fn from_jumbf(buffer: &[u8], validation_log: &mut impl StatusTracker) -> Result<Store> {
+ if buffer.is_empty() {
+ return Err(Error::JumbfNotFound);
+ }
+
let mut store = Store::new();
// setup a cursor for reading the buffer...
@@ -968,45 +976,6 @@ impl Store {
}
}
- // verify the provenance of the claim
- fn provenance_checks<'a>(
- store: &'a Store,
- xmp_opt: Option<String>,
- validation_log: &mut impl StatusTracker,
- ) -> Result<&'a Claim> {
- #[cfg(feature = "diagnostics")]
- let _t = crate::utils::time_it::TimeIt::new("verify_store");
-
- // look for the active manifest in xmp if available
- let provenance_claim = match xmp_opt {
- Some(xmp_str) => match extract_provenance(&xmp_str) {
- Some(c) => c,
- None => store.provenance_path().unwrap_or_else(|| "".to_string()), // if not explicitly set use active manifest
- },
- None => store.provenance_path().unwrap_or_else(|| "".to_string()), // if not explicitly set use active manifest
- };
-
- // get claim that matches the provenance label
- let claim_label = Store::manifest_label_from_path(&provenance_claim);
- let claim = match store.get_claim(&claim_label) {
- Some(c) => c,
- None => {
- let log_item = log_item!(
- &claim_label,
- "could not find active manifest",
- "verify_store"
- )
- .error(Error::ProvenanceMissing)
- .validation_status(validation_status::CLAIM_MISSING);
- validation_log.log(log_item, Some(Error::ProvenanceMissing))?;
-
- return Err(Error::ProvenanceMissing);
- }
- };
-
- Ok(claim)
- }
-
// wake the ingredients and validate
fn ingredient_checks<'a>(
store: &Store,
@@ -1190,11 +1159,21 @@ impl Store {
/// validation_log: If present all found errors are logged and returned, other wise first error causes exit and is returned
pub async fn verify_store_async(
store: &Store,
- xmp_opt: Option<String>,
asset_bytes: &[u8],
validation_log: &mut impl StatusTracker,
) -> Result<()> {
- let claim = Store::provenance_checks(store, xmp_opt, validation_log)?;
+ let claim = match store.provenance_claim() {
+ Some(c) => c,
+ None => {
+ let log_item =
+ log_item!("Unknown", "could not find active manifest", "verify_store")
+ .error(Error::ProvenanceMissing)
+ .validation_status(validation_status::CLAIM_MISSING);
+ validation_log.log(log_item, Some(Error::ProvenanceMissing))?;
+
+ return Err(Error::ProvenanceMissing);
+ }
+ };
// verify the provenance claim
Claim::verify_claim_async(claim, asset_bytes, true, validation_log).await?;
@@ -1211,11 +1190,21 @@ impl Store {
/// validation_log: If present all found errors are logged and returned, other wise first error causes exit and is returned
pub fn verify_store<'a>(
store: &Store,
- xmp_opt: Option<String>,
asset_data: &ClaimAssetData<'a>,
validation_log: &mut impl StatusTracker,
) -> Result<()> {
- let claim = Store::provenance_checks(store, xmp_opt, validation_log)?;
+ let claim = match store.provenance_claim() {
+ Some(c) => c,
+ None => {
+ let log_item =
+ log_item!("Unknown", "could not find active manifest", "verify_store")
+ .error(Error::ProvenanceMissing)
+ .validation_status(validation_status::CLAIM_MISSING);
+ validation_log.log(log_item, Some(Error::ProvenanceMissing))?;
+
+ return Err(Error::ProvenanceMissing);
+ }
+ };
// verify the provenance claim
Claim::verify_claim(claim, asset_data, true, validation_log)?;
@@ -1404,7 +1393,9 @@ impl Store {
fn copy_c2pa_to_output(source: &Path, dest: &Path, remote_type: RemoteManifest) -> Result<()> {
match remote_type {
crate::claim::RemoteManifest::NoRemote => Store::move_or_copy(source, dest)?,
- crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => {
+ crate::claim::RemoteManifest::SideCar
+ | crate::claim::RemoteManifest::Remote(_)
+ | crate::claim::RemoteManifest::EmbedWithRemote(_) => {
// make correct path names
let source_asset = source;
let source_cai = source_asset.with_extension(MANIFEST_STORE_EXT);
@@ -1442,7 +1433,8 @@ impl Store {
// get correct output path for remote manifest
let output_path = match pc.remote_manifest() {
- crate::claim::RemoteManifest::NoRemote => temp_file.to_path_buf(),
+ crate::claim::RemoteManifest::NoRemote
+ | crate::claim::RemoteManifest::EmbedWithRemote(_) => temp_file.to_path_buf(),
crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => {
temp_file.with_extension(MANIFEST_STORE_EXT)
}
@@ -1454,6 +1446,14 @@ impl Store {
let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
pc_mut.set_signature_val(s);
+ // do we need to make a C2PA file in addtion to standard embedded output
+ if let crate::claim::RemoteManifest::EmbedWithRemote(_url) =
+ pc_mut.remote_manifest()
+ {
+ let c2pa = output_path.with_extension(MANIFEST_STORE_EXT);
+ std::fs::write(c2pa, &m)?;
+ }
+
// copy the correct files upon completion
Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?;
@@ -1490,7 +1490,8 @@ impl Store {
// get correct output path for remote manifest
let output_path = match pc.remote_manifest() {
- crate::claim::RemoteManifest::NoRemote => temp_file.to_path_buf(),
+ crate::claim::RemoteManifest::NoRemote
+ | crate::claim::RemoteManifest::EmbedWithRemote(_) => temp_file.to_path_buf(),
crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => {
temp_file.with_extension(MANIFEST_STORE_EXT)
}
@@ -1502,6 +1503,14 @@ impl Store {
let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
pc_mut.set_signature_val(s);
+ // do we need to make a C2PA file in addtion to standard embedded output
+ if let crate::claim::RemoteManifest::EmbedWithRemote(_url) =
+ pc_mut.remote_manifest()
+ {
+ let c2pa = output_path.with_extension(MANIFEST_STORE_EXT);
+ std::fs::write(c2pa, &m)?;
+ }
+
// copy the correct files upon completion
Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?;
@@ -1537,7 +1546,8 @@ impl Store {
// get correct output path for remote manifest
let output_path = match pc.remote_manifest() {
- crate::claim::RemoteManifest::NoRemote => temp_file.to_path_buf(),
+ crate::claim::RemoteManifest::NoRemote
+ | crate::claim::RemoteManifest::EmbedWithRemote(_) => temp_file.to_path_buf(),
crate::claim::RemoteManifest::SideCar | crate::claim::RemoteManifest::Remote(_) => {
temp_file.with_extension(MANIFEST_STORE_EXT)
}
@@ -1549,6 +1559,14 @@ impl Store {
let pc_mut = self.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
pc_mut.set_signature_val(s);
+ // do we need to make a C2PA file in addtion to standard embedded output
+ if let crate::claim::RemoteManifest::EmbedWithRemote(_url) =
+ pc_mut.remote_manifest()
+ {
+ let c2pa = output_path.with_extension(MANIFEST_STORE_EXT);
+ std::fs::write(c2pa, &m)?;
+ }
+
// copy the correct files upon completion
Store::copy_c2pa_to_output(&temp_file, dest_path, pc_mut.remote_manifest())?;
@@ -1602,6 +1620,14 @@ impl Store {
embedded_xmp::add_manifest_uri_to_file(dest_path, &_url)?;
d
}
+ crate::claim::RemoteManifest::EmbedWithRemote(_url) => {
+ // even though this block is protected by the outer cfg!(feature = "xmp_write")
+ // the class embedded_xmp is not defined so we have to explicitly exclude it from the build
+ #[cfg(feature = "xmp_write")]
+ embedded_xmp::add_manifest_uri_to_file(dest_path, &_url)?;
+
+ dest_path.to_path_buf()
+ }
}
} else {
// only side car and embedded supported without feature "xmp_write"
@@ -1610,8 +1636,9 @@ impl Store {
crate::claim::RemoteManifest::SideCar => {
dest_path.with_extension(MANIFEST_STORE_EXT)
}
- crate::claim::RemoteManifest::Remote(_) => {
- return Err(Error::BadParam("requires 'xmp_writte' feature".to_string()))
+ crate::claim::RemoteManifest::Remote(_)
+ | crate::claim::RemoteManifest::EmbedWithRemote(_) => {
+ return Err(Error::BadParam("requires 'xmp_write' feature".to_string()))
}
}
};
@@ -1734,69 +1761,71 @@ impl Store {
asset_path: &'a Path,
validation_log: &mut impl StatusTracker,
) -> Result<()> {
- let ext = get_supported_file_extension(asset_path).ok_or(Error::UnsupportedType)?;
-
- let cai_loader = get_cailoader_handler(&ext).ok_or(Error::UnsupportedType)?;
-
- let mut asset_reader = fs::File::open(asset_path)?;
-
- // read xmp if available
- let xmp_opt = cai_loader.read_xmp(&mut asset_reader);
-
- let xmp_copy = xmp_opt.clone();
-
- Store::verify_store(
- self,
- xmp_opt,
- &ClaimAssetData::PathData(asset_path),
- validation_log,
- )?;
-
- // set the provenance if there is xmp otherwise it will default to active manifest
- if let Some(xmp) = xmp_copy {
- if let Some(xmp_provenance) = extract_provenance(&xmp) {
- let claim_label = Store::manifest_label_from_path(&xmp_provenance);
- self.set_provenance_path(&claim_label);
- }
- }
-
- Ok(())
+ Store::verify_store(self, &ClaimAssetData::PathData(asset_path), validation_log)
}
// verify from a buffer without file i/o
pub fn verify_from_buffer(
&mut self,
buf: &[u8],
- asset_type: &str,
+ _asset_type: &str,
validation_log: &mut impl StatusTracker,
) -> Result<()> {
- let mut buf_reader = Cursor::new(buf);
+ Store::verify_store(self, &ClaimAssetData::ByteData(buf), validation_log)
+ }
- let cai_loader = get_cailoader_handler(asset_type).ok_or(Error::UnsupportedType)?;
+ // fetch remote manifest if possible
+ #[cfg(not(target_arch = "wasm32"))]
+ #[cfg(feature = "file_io")]
+ fn fetch_remote_manifest(url: &str) -> Result<Vec<u8>> {
+ use std::io::Read;
+
+ use conv::ValueFrom;
+ use ureq::Error as uError;
+
+ //const MANIFEST_CONTENT_TYPE: &str = "application/x-c2pa-manifest-store"; // todo verify once these are served
+ const DEFAULT_MANIFEST_RESPONSE_SIZE: usize = 10 * 1024 * 1024; // 10 MB
- // read xmp if available
- let xmp_opt = cai_loader.read_xmp(&mut buf_reader);
+ match ureq::get(url).call() {
+ Ok(response) => {
+ if response.status() == 200 {
+ let len = response
+ .header("Content-Length")
+ .and_then(|s| s.parse::<usize>().ok())
+ .unwrap_or(DEFAULT_MANIFEST_RESPONSE_SIZE); // todo figure out good max to accept
- let xmp_copy = xmp_opt.clone();
+ let mut response_bytes: Vec<u8> = Vec::with_capacity(len);
- let buf = buf_reader.into_inner();
+ let len64 = u64::value_from(len)
+ .map_err(|_err| Error::BadParam("value out of range".to_string()))?;
- Store::verify_store(
- self,
- xmp_opt,
- &ClaimAssetData::ByteData(buf),
- validation_log,
- )?;
+ response
+ .into_reader()
+ .take(len64)
+ .read_to_end(&mut response_bytes)
+ .map_err(|_err| {
+ Error::RemoteManifestFetch("error reading content stream".to_string())
+ })?;
- // set the provenance if there is xmp otherwise it will default to active manifest
- if let Some(xmp) = xmp_copy {
- if let Some(xmp_provenance) = extract_provenance(&xmp) {
- let claim_label = Store::manifest_label_from_path(&xmp_provenance);
- self.set_provenance_path(&claim_label);
+ Ok(response_bytes)
+ } else {
+ Err(Error::RemoteManifestFetch(format!(
+ "fetch failed: code: {}, status: {}",
+ response.status(),
+ response.status_text()
+ )))
+ }
}
+ Err(uError::Status(code, resp)) => Err(Error::RemoteManifestFetch(format!(
+ "code: {}, response: {}",
+ code,
+ resp.status_text()
+ ))),
+ Err(uError::Transport(_)) => Err(Error::RemoteManifestFetch(format!(
+ "fetch failed: url: {}",
+ url
+ ))),
}
-
- Ok(())
}
/// Return Store from in memory asset
@@ -1816,19 +1845,44 @@ impl Store {
/// in_path - path to source file
/// validation_log - optional vec to contain addition info about the asset
#[cfg(feature = "file_io")]
- pub fn load_cai_from_file(
+ fn load_cai_from_file(
in_path: &Path,
validation_log: &mut impl StatusTracker,
) -> Result<Store> {
- // get jumbf block
- load_jumbf_from_file(in_path).and_then(|buffer| {
- if buffer.is_empty() {
- return Err(Error::JumbfNotFound);
- }
+ let external_manifest = in_path.with_extension(MANIFEST_STORE_EXT);
- // load and validate with CAI toolkit and dump if desired
- Store::from_jumbf(&buffer, validation_log)
- })
+ match load_jumbf_from_file(in_path) {
+ Ok(manifest_bytes) => {
+ // load and validate with CAI toolkit and dump if desired
+ Store::from_jumbf(&manifest_bytes, validation_log)
+ }
+ Err(Error::JumbfNotFound) => {
+ if external_manifest.exists() {
+ let external_manifest_bytes = std::fs::read(external_manifest)?;
+ Store::from_jumbf(&external_manifest_bytes, validation_log)
+ } else {
+ // check for remote manifest
+ let mut asset_reader = std::fs::File::open(in_path)?;
+ let ext = get_file_extension(in_path).ok_or(Error::UnsupportedType)?;
+ if let Some(ext_ref) = crate::utils::xmp_inmemory_utils::XmpInfo::from_source(
+ &mut asset_reader,
+ &ext,
+ )
+ .provenance
+ {
+ if cfg!(feature = "fetch_remote_manifests") {
+ let remote_manifest_bytes = Store::fetch_remote_manifest(&ext_ref)?;
+ Store::from_jumbf(&remote_manifest_bytes, validation_log)
+ } else {
+ Err(Error::JumbfNotFound)
+ }
+ } else {
+ Err(Error::JumbfNotFound)
+ }
+ }
+ }
+ Err(e) => Err(e),
+ }
}
/// Load Store from claims in an existing asset
@@ -1896,6 +1950,24 @@ impl Store {
})
}
+ /// Returns embedded remote manifest URL if available
+ /// asset_type: extentions or mime type of the data
+ /// data: byte array containing the asset
+ pub fn get_remote_manifest_url(asset_type: &str, data: &[u8]) -> Option<String> {
+ let mut buf_reader = Cursor::new(data);
+
+ if let Some(ext_ref) =
+ crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut buf_reader, asset_type)
+ .provenance
+ {
+ // make sure it parses
+ let _u = url::Url::parse(&ext_ref).ok()?;
+ Some(ext_ref)
+ } else {
+ None
+ }
+ }
+
/// Load Store from a in-memory asset
/// asset_type: asset extension or mime type
/// data: reference to bytes of the the file
@@ -1908,26 +1980,11 @@ impl Store {
validation_log: &mut impl StatusTracker,
) -> Result<Store> {
Store::get_store_from_memory(asset_type, data, validation_log).and_then(
- |(mut store, xmp_opt)| {
+ |(store, _xmp_opt)| {
// verify the store
if verify {
- let xmp_copy = xmp_opt.clone();
-
// verify store and claims
- Store::verify_store(
- &store,
- xmp_opt,
- &ClaimAssetData::ByteData(data),
- validation_log,
- )?;
-
- // set the provenance if checks pass & has xmp, otherwise default to active manifest
- if let Some(xmp) = xmp_copy {
- if let Some(xmp_provenance) = extract_provenance(&xmp) {
- let claim_label = Store::manifest_label_from_path(&xmp_provenance);
- store.set_provenance_path(&claim_label);
- }
- }
+ Store::verify_store(&store, &ClaimAssetData::ByteData(data), validation_log)?;
}
Ok(store)
@@ -1946,25 +2003,12 @@ impl Store {
verify: bool,
validation_log: &mut impl StatusTracker,
) -> Result<Store> {
- let (mut store, xmp_opt) = Store::get_store_from_memory(asset_type, data, validation_log)?;
-
- let buf_reader = Cursor::new(data);
+ let (store, _xmp_opt) = Store::get_store_from_memory(asset_type, data, validation_log)?;
// verify the store
if verify {
- let xmp_copy = xmp_opt.clone();
-
// verify store and claims
- Store::verify_store_async(&store, xmp_opt, buf_reader.get_ref(), validation_log)
- .await?;
-
- // set the provenance if checks pass & has xmp, otherwise default to active manifest
- if let Some(xmp) = xmp_copy {
- if let Some(xmp_provenance) = extract_provenance(&xmp) {
- let claim_label = Store::manifest_label_from_path(&xmp_provenance);
- store.set_provenance_path(&claim_label);
- }
- }
+ Store::verify_store_async(&store, data, validation_log).await?;
}
Ok(store)
@@ -2248,10 +2292,7 @@ pub mod tests {
crate::openssl::temp_signer_async::AsyncSignerAdapter::new(SigningAlg::Ps256);
// this would happen on some remote server
- let cose_sign1_box =
- crate::cose_sign::cose_sign_async(&signer, claim_bytes, self.reserve_size()).await;
-
- cose_sign1_box
+ crate::cose_sign::cose_sign_async(&signer, claim_bytes, self.reserve_size()).await
}
fn reserve_size(&self) -> usize {
10000
@@ -2883,24 +2924,13 @@ pub mod tests {
// compare returned to external
assert_eq!(saved_manifest, loaded_manifest);
- // load the jumbf back into a store
+ // test auto loading of sidecar with validation
let mut validation_log = OneShotStatusTracker::default();
- let restored_store = Store::from_jumbf(&loaded_manifest, &mut validation_log).unwrap();
- let pc = restored_store.provenance_claim().unwrap();
-
- // make sure this manifest goes with this asset
- for dh_assertion in pc.data_hash_assertions() {
- if dh_assertion.label_root() == DataHash::LABEL {
- let dh = DataHash::from_assertion(dh_assertion).unwrap();
-
- dh.verify_hash(&op.clone(), Some(pc.alg().to_string()))
- .unwrap();
- }
- }
+ Store::load_from_asset(&op, true, &mut validation_log).unwrap();
}
#[test]
- fn test_external_manifest_embedded() {
+ fn test_external_manifest_embedded_url() {
// test adding to actual image
let ap = fixture_path("libpng-test.png");
let temp_dir = tempdir().expect("temp dir");
@@ -2939,10 +2969,6 @@ pub mod tests {
assert_eq!(saved_manifest, loaded_manifest);
// load the jumbf back into a store
- let mut validation_log = OneShotStatusTracker::default();
- let restored_store = Store::from_jumbf(&loaded_manifest, &mut validation_log).unwrap();
- let pc = restored_store.provenance_claim().unwrap();
-
let mut asset_reader = std::fs::File::open(op.clone()).unwrap();
let ext_ref =
crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut asset_reader, "png")
@@ -2951,19 +2977,13 @@ pub mod tests {
assert_eq!(ext_ref, url_string);
- // make sure this manifest goes with this asset
- for dh_assertion in pc.data_hash_assertions() {
- if dh_assertion.label_root() == DataHash::LABEL {
- let dh = DataHash::from_assertion(dh_assertion).unwrap();
-
- dh.verify_hash(&op.clone(), Some(pc.alg().to_string()))
- .unwrap();
- }
- }
+ // make sure it validates
+ let mut validation_log = OneShotStatusTracker::default();
+ Store::load_from_asset(&op, true, &mut validation_log).unwrap();
}
#[test]
- fn test_user_guid_external_manifest_embedded() {
+ fn test_external_manifest_embedded_manifest_embedded_url() {
// test adding to actual image
let ap = fixture_path("libpng-test.png");
let temp_dir = tempdir().expect("temp dir");
@@ -2975,7 +2995,7 @@ pub mod tests {
let mut store = Store::new();
// Create a new claim.
- let mut claim = Claim::new_with_user_guid("store unit test", "my guid");
+ let mut claim = create_test_claim().unwrap();
// Do we generate JUMBF?
let signer = temp_signer();
@@ -2985,8 +3005,9 @@ pub mod tests {
let url = url::Url::parse(&fp).unwrap();
let url_string: String = url.into();
+
// set claim for side car with remote manifest embedding generation
- claim.set_remote_manifest(url_string).unwrap();
+ claim.set_embed_remote_manifest(url_string.clone()).unwrap();
store.commit_claim(claim).unwrap();
@@ -2999,5 +3020,17 @@ pub mod tests {
// compare returned to external
assert_eq!(saved_manifest, loaded_manifest);
+
+ let mut asset_reader = std::fs::File::open(op.clone()).unwrap();
+ let ext_ref =
+ crate::utils::xmp_inmemory_utils::XmpInfo::from_source(&mut asset_reader, "png")
+ .provenance
+ .unwrap();
+
+ assert_eq!(ext_ref, url_string);
+
+ // make sure it validates
+ let mut validation_log = OneShotStatusTracker::default();
+ Store::load_from_asset(&op, true, &mut validation_log).unwrap();
}
}
diff --git a/sdk/src/time_stamp.rs b/sdk/src/time_stamp.rs
@@ -331,7 +331,7 @@ pub fn verify_timestamp(ts: &[u8], data: &[u8]) -> Result<TstInfo> {
h.update(data);
let digest = h.finish();
- if !vec_compare(digest.as_ref(), &mi.hashed_message.to_bytes().to_vec()) {
+ if !vec_compare(digest.as_ref(), &mi.hashed_message.to_bytes()) {
return Err(Error::CoseTimeStampMismatch);
}
@@ -373,13 +373,13 @@ pub fn get_timestamp_response(tsresp: &[u8]) -> Result<TimeStampResponse> {
Ok(ts)
}
-#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
pub struct TstToken {
#[serde(with = "serde_bytes")]
pub val: Vec<u8>,
}
-#[derive(Deserialize, Serialize, Debug, PartialEq, Clone)]
+#[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)]
pub struct TstContainer {
#[serde(rename = "tstTokens")]
pub tst_tokens: Vec<TstToken>,
diff --git a/sdk/src/utils/cbor_types.rs b/sdk/src/utils/cbor_types.rs
@@ -26,7 +26,7 @@ use serde_cbor::tags::Tagged;
// Based on samples from cbor rust git repository.
//
// https://tools.ietf.org/html/rfc7049#section-2.4.1
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DateT(pub String);
impl Serialize for DateT {
@@ -58,7 +58,7 @@ impl fmt::Display for DateT {
}
// https://tools.ietf.org/html/rfc7049#section-2.4.4.3
-#[derive(Clone, Debug, Default, PartialEq)]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct UriT(pub String);
impl Serialize for UriT {
@@ -89,7 +89,7 @@ impl fmt::Display for UriT {
}
}
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BytesT(pub Vec<u8>);
impl Serialize for BytesT {
diff --git a/sdk/src/utils/hash_utils.rs b/sdk/src/utils/hash_utils.rs
@@ -31,7 +31,7 @@ use crate::{Error, Result};
const MAX_HASH_BUF: usize = 256 * 1024 * 1024; // cap memory usage to 256MB
-#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)]
+#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)]
pub struct Exclusion {
start: usize,
length: usize,