Skip to content

Commit

Permalink
(MINOR) Updates needed for v2 JavaScript SDK (#521)
Browse files Browse the repository at this point in the history
* Add missing `json_schema` traits, add serde traits

* Provide access to a resource from the Reader

* Generate schemas for ManifestStore and Settings

* Add ManifestDefinition

* Pass stream by reference

* Fix schemars tag

* Add mutable references

* Undo mutable changes

* Undo API changes

* Update AsyncSigner for Wasm

* Try to separate out AsyncSigner

* Add temp function for debugging

* Use `from_bytes` API

* Add from_manifest_store function

* Revert "Add from_manifest_store function"

This reverts commit fcb1856.

* Make async flow for adding an ingredient via the Builder

* Add clippy, fix args

* Don't expose buffer

* Update serde-json

* Update serde_json version in make_test_images

* Make clippy fixes

* Update image crate

* Pin to image version

* Update serde-json

---------

Co-authored-by: Gavin  Peacock <[email protected]>
  • Loading branch information
Dave Kozma and gpeacock authored Jul 23, 2024
1 parent ce91afb commit d72c43d
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 50 deletions.
6 changes: 3 additions & 3 deletions export_schema/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@ rust-version = "1.74.0"

[dependencies]
anyhow = "1.0.40"
c2pa = { path = "../sdk", features = ["file_io", "json_schema"] }
schemars = "0.8.13"
serde_json = "1.0.114"
c2pa = { path = "../sdk", features = ["json_schema"] }
schemars = "0.8.21"
serde_json = "1.0.117"
27 changes: 18 additions & 9 deletions export_schema/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,28 @@
use std::{fs, path::Path};

use anyhow::Result;
use c2pa::ManifestStore;
use schemars::gen::SchemaSettings;
use c2pa::{settings::Settings, ManifestDefinition, ManifestStore};
use schemars::{schema::RootSchema, schema_for};

fn main() -> Result<()> {
println!("Exporting JSON schema");
let settings = SchemaSettings::draft07();
let gen = settings.into_generator();
let schema = gen.into_root_schema_for::<ManifestStore>();
let output = serde_json::to_string_pretty(&schema).expect("Failed to serialize schema");
fn write_schema(schema: &RootSchema, name: &str) {
println!("Exporting JSON schema for {}", name);
let output = serde_json::to_string_pretty(schema).expect("Failed to serialize schema");
let output_dir = Path::new("./target/schema");
fs::create_dir_all(output_dir).expect("Could not create schema directory");
let output_path = output_dir.join("ManifestStore.schema.json");
let output_path = output_dir.join(format!("{}.schema.json", name));
fs::write(&output_path, output).expect("Unable to write schema");
println!("Wrote schema to {}", output_path.display());
}

fn main() -> Result<()> {
let manifest_definition = schema_for!(ManifestDefinition);
write_schema(&manifest_definition, "ManifestDefinition");

let manifest_store = schema_for!(ManifestStore);
write_schema(&manifest_store, "ManifestStore");

let settings = schema_for!(Settings);
write_schema(&settings, "Settings");

Ok(())
}
2 changes: 1 addition & 1 deletion make_test_images/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,5 @@ memchr = "2.7.1"
nom = "7.1.3"
regex = "1.5.6"
serde = "1.0.197"
serde_json = { version = "1.0.114", features = ["preserve_order"] }
serde_json = { version = "1.0.117", features = ["preserve_order"] }
tempfile = "3.10.1"
6 changes: 3 additions & 3 deletions sdk/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -109,19 +109,19 @@ rasn-ocsp = "0.12.5"
rasn-pkix = "0.12.5"
rasn = "0.12.5"
riff = "1.0.1"
schemars = { version = "0.8.13", optional = true }
schemars = { version = "0.8.21", optional = true }
serde = { version = "1.0.197", features = ["derive"] }
serde_bytes = "0.11.5"
serde_cbor = "0.11.1"
serde_derive = "1.0.197"
serde_json = { version = "1.0.114", features = ["preserve_order"] }
serde_json = { version = "1.0.117", features = ["preserve_order"] }
serde_with = "3.4.0"
serde-transcode = "1.1.1"
sha2 = "0.10.2"
tempfile = "3.10.1"
thiserror = "1.0.61"
treeline = "0.1.0"
url = "2.2.2, <2.5.1" # Can't use 2.5.1 or newer until new license is reviewed.
url = "2.2.2, <2.5.1" # Can't use 2.5.1 or newer until new license is reviewed.
uuid = { version = "1.7.0", features = ["serde", "v4", "js"] }
x509-parser = "0.15.1"
x509-certificate = "0.19.0"
Expand Down
13 changes: 12 additions & 1 deletion sdk/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ use std::{
};

use async_generic::async_generic;
#[cfg(feature = "json_schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none;
use uuid::Uuid;
Expand Down Expand Up @@ -46,6 +48,7 @@ const ARCHIVE_VERSION: &str = "1";
/// It is used to define a claim that can be signed and embedded into a file
#[skip_serializing_none]
#[derive(Debug, Default, Deserialize, Serialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[non_exhaustive]
pub struct ManifestDefinition {
/// Optional prefix added to the generated Manifest Label
Expand Down Expand Up @@ -103,13 +106,16 @@ fn default_vec<T>() -> Vec<T> {
}

#[derive(Debug, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[serde(untagged)]
pub enum AssertionData {
#[cfg_attr(feature = "json_schema", schemars(skip))]
Cbor(serde_cbor::Value),
Json(serde_json::Value),
}

#[derive(Debug, Deserialize, Serialize, Clone)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[non_exhaustive]
pub struct AssertionDefinition {
pub label: String,
Expand Down Expand Up @@ -319,6 +325,7 @@ impl Builder {
/// * A mutable reference to the [`Ingredient`].
/// # Errors
/// * If the [`Ingredient`] is not valid
#[async_generic()]
pub fn add_ingredient<'a, T, R>(
&'a mut self,
ingredient_json: T,
Expand All @@ -330,7 +337,11 @@ impl Builder {
R: Read + Seek + Send,
{
let ingredient: Ingredient = Ingredient::from_json(&ingredient_json.into())?;
let ingredient = ingredient.with_stream(format, stream)?;
let ingredient = if _sync {
ingredient.with_stream(format, stream)?
} else {
ingredient.with_stream_async(format, stream).await?
};
self.definition.ingredients.push(ingredient);
#[allow(clippy::unwrap_used)]
Ok(self.definition.ingredients.last_mut().unwrap()) // ok since we just added it
Expand Down
73 changes: 50 additions & 23 deletions sdk/src/ingredient.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use std::path::{Path, PathBuf};
use std::{borrow::Cow, io::Cursor};

use async_generic::async_generic;
use log::{debug, error};
#[cfg(feature = "json_schema")]
use schemars::JsonSchema;
Expand Down Expand Up @@ -808,6 +809,7 @@ impl Ingredient {
/// Instance_id, document_id, and provenance will be overridden if found in the stream.
/// Format will be overridden only if it is the default (application/octet-stream).
#[cfg(feature = "unstable_api")]
#[async_generic()]
pub(crate) fn with_stream<S: Into<String>>(
mut self,
format: S,
Expand Down Expand Up @@ -841,36 +843,61 @@ impl Ingredient {
};

stream.rewind()?;
self.add_stream_internal(&format, stream)

if _sync {
self.add_stream_internal(&format, stream)
} else {
self.add_stream_internal_async(&format, stream).await
}
}

// Internal implementation to avoid code bloat.
#[async_generic()]
fn add_stream_internal(mut self, format: &str, stream: &mut dyn CAIRead) -> Result<Self> {
let mut validation_log = DetailedStatusTracker::new();

// retrieve the manifest bytes from embedded, sidecar or remote and convert to store if found
let (result, manifest_bytes) = match load_jumbf_from_stream(format, stream) {
Ok(manifest_bytes) => {
(
// generate a store from the buffer and then validate from the asset path
Store::from_jumbf(&manifest_bytes, &mut validation_log)
.and_then(|mut store| {
// verify the store
store.verify_from_stream(stream, format, &mut validation_log)?;
Ok(store)
})
.map_err(|e| {
// add a log entry for the error so we act like verify
validation_log.log_silent(
log_item!("asset", "error loading file", "Ingredient::from_file")
.set_error(&e),
);
e
}),
Some(manifest_bytes),
)
}
Err(err) => (Err(err), None),
let jumbf_stream = load_jumbf_from_stream(format, stream);

// We can't use functional combinators since we can't use async callbacks (https://github.com/rust-lang/rust/issues/62290)
let (result, manifest_bytes) = if let Ok(manifest_bytes) = jumbf_stream {
let jumbf_store = Store::from_jumbf(&manifest_bytes, &mut validation_log);
let result = if let Ok(mut store) = jumbf_store {
if _sync {
match store.verify_from_stream(stream, format, &mut validation_log) {
Ok(_) => Ok(store),
Err(err) => Err(err),
}
} else {
match store
.verify_from_stream_async(stream, format, &mut validation_log)
.await
{
Ok(_) => Ok(store),
Err(err) => Err(err),
}
}
} else {
// This will always be Err in this situation
#[allow(clippy::unwrap_used)]
Err(jumbf_store.unwrap_err())
};

(
result.map_err(|e| {
// add a log entry for the error so we act like verify
validation_log.log_silent(
log_item!("asset", "error loading file", "Ingredient::from_file")
.set_error(&e),
);
e
}),
Some(manifest_bytes),
)
} else {
// This will always be Err in this situation
#[allow(clippy::unwrap_used)]
(Err(jumbf_stream.unwrap_err()), None)
};

// set validation status from result and log
Expand Down
5 changes: 5 additions & 0 deletions sdk/src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ use std::fs::{read, File};
use std::io::{Read, Seek, Write};

use async_generic::async_generic;
#[cfg(feature = "json_schema")]
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[cfg(feature = "file_io")]
use crate::error::Error;
Expand All @@ -29,6 +32,8 @@ use crate::{
};

/// A reader for the manifest store.
#[derive(Serialize, Deserialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
pub struct Reader {
pub(crate) manifest_store: ManifestStore,
}
Expand Down
9 changes: 8 additions & 1 deletion sdk/src/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ use std::{

use config::{Config, FileFormat};
use lazy_static::lazy_static;
#[cfg(feature = "json_schema")]
use schemars::JsonSchema;
use serde_derive::{Deserialize, Serialize};

use crate::{utils::base64, Error, Result};
Expand All @@ -39,6 +41,7 @@ pub(crate) trait SettingsValidate {

// Settings for trust list feature
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[allow(unused)]
pub(crate) struct Trust {
private_anchors: Option<String>,
Expand Down Expand Up @@ -116,6 +119,7 @@ impl SettingsValidate for Trust {

// Settings for core C2PA-RS functionality
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[allow(unused)]
pub(crate) struct Core {
debug: bool,
Expand Down Expand Up @@ -152,6 +156,7 @@ impl SettingsValidate for Core {

// Settings for verification options
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[allow(unused)]
pub(crate) struct Verify {
verify_after_reading: bool,
Expand All @@ -177,6 +182,7 @@ impl SettingsValidate for Verify {}

// Settings for Builder API options
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[allow(unused)]
pub(crate) struct Builder {
auto_thumbnail: bool,
Expand All @@ -197,8 +203,9 @@ impl SettingsValidate for Builder {}
// file or by setting specific value via code. There is a single configuration
// setting for the entire C2PA-RS instance.
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)]
#[cfg_attr(feature = "json_schema", derive(JsonSchema))]
#[allow(unused)]
pub(crate) struct Settings {
pub struct Settings {
trust: Trust,
core: Core,
verify: Verify,
Expand Down
58 changes: 54 additions & 4 deletions sdk/src/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ use async_trait::async_trait;
/// This trait exists to allow the signature mechanism to be extended.
///
/// Use this when the implementation is asynchronous.
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
#[cfg(not(target_arch = "wasm32"))]
#[async_trait]
pub trait AsyncSigner: Sync {
/// Returns a new byte array which is a signature over the original.
async fn sign(&self, data: Vec<u8>) -> Result<Vec<u8>>;
Expand Down Expand Up @@ -163,7 +163,6 @@ pub trait AsyncSigner: Sync {
///
/// The default implementation will send the request to the URL
/// provided by [`Self::time_authority_url()`], if any.
#[cfg(not(target_arch = "wasm32"))]
async fn send_timestamp_request(&self, message: &[u8]) -> Option<Result<Vec<u8>>> {
// NOTE: This is currently synchronous, but may become
// async in the future.
Expand All @@ -177,7 +176,58 @@ pub trait AsyncSigner: Sync {
}
None
}
#[cfg(target_arch = "wasm32")]

/// OCSP response for the signing cert if available
/// This is the only C2PA supported cert revocation method.
/// By pre-querying the value for a your signing cert the value can
/// be cached taking pressure off of the CA (recommended by C2PA spec)
async fn ocsp_val(&self) -> Option<Vec<u8>> {
None
}

/// If this returns true the sign function is responsible for for direct handling of the COSE structure.
///
/// This is useful for cases where the signer needs to handle the COSE structure directly.
/// Not recommended for general use.
fn direct_cose_handling(&self) -> bool {
false
}
}

#[cfg(target_arch = "wasm32")]
#[async_trait(?Send)]
pub trait AsyncSigner {
/// Returns a new byte array which is a signature over the original.
async fn sign(&self, data: Vec<u8>) -> Result<Vec<u8>>;

/// Returns the algorithm of the Signer.
fn alg(&self) -> SigningAlg;

/// Returns the certificates as a Vec containing a Vec of DER bytes for each certificate.
fn certs(&self) -> Result<Vec<Vec<u8>>>;

/// Returns the size in bytes of the largest possible expected signature.
/// Signing will fail if the result of the `sign` function is larger
/// than this value.
fn reserve_size(&self) -> usize;

/// URL for time authority to time stamp the signature
fn time_authority_url(&self) -> Option<String> {
None
}

/// Additional request headers to pass to the time stamp authority.
///
/// IMPORTANT: You should not include the "Content-type" header here.
/// That is provided by default.
fn timestamp_request_headers(&self) -> Option<Vec<(String, String)>> {
None
}

fn timestamp_request_body(&self, message: &[u8]) -> Result<Vec<u8>> {
crate::time_stamp::default_rfc3161_message(message)
}

async fn send_timestamp_request(&self, message: &[u8]) -> Option<Result<Vec<u8>>>;

/// OCSP response for the signing cert if available
Expand Down
Loading

0 comments on commit d72c43d

Please sign in to comment.