Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0f923b9
feat: setting to enable timestamp assertions for the parent ingredien…
ok-nick Nov 14, 2025
c344c1b
feat: add Claim::parent_claim and change timestamp setting to apply o…
ok-nick Nov 17, 2025
fc0777b
fix: return uri instead of claim inside of claim since they can be un…
ok-nick Nov 17, 2025
99e8fcc
Merge branch 'main' of github.com:contentauth/c2pa-rs into ok-nick/ti…
ok-nick Nov 17, 2025
ba21217
test: update manifest timestamp assertions
ok-nick Nov 17, 2025
5d2b845
test: use child manifest label instead of active when verifying times…
ok-nick Nov 17, 2025
86c5051
docs: clarify update_manifest_timestamp_assertion setting
ok-nick Nov 17, 2025
5c81c84
fix: align new error cases with same errors used in other places
ok-nick Nov 17, 2025
14f97a2
Merge branch 'main' of github.com:contentauth/c2pa-rs into ok-nick/ti…
ok-nick Nov 17, 2025
ca5c35a
style: fix clippy lints
ok-nick Nov 17, 2025
4f48857
test: fix test_bogus_cert test by moving settings changes before crea…
ok-nick Nov 17, 2025
cd48c51
fix: use existing timestamp assertion if it exists and move timestamp…
ok-nick Nov 19, 2025
7cbf4ca
fix: replace existing timestamp assertion rather than adding another
ok-nick Nov 19, 2025
c853bbb
fix: add_assertion if it doesn't already exist instead of replace
ok-nick Nov 19, 2025
8ef0806
fix: simplify timestamp function signatures, add docs, and add more c…
ok-nick Nov 21, 2025
e75c94b
fix: add more context to timestamp errors
ok-nick Nov 21, 2025
d35960e
style: fix clippy lints
ok-nick Nov 21, 2025
fa7b342
Merge branch 'main' into ok-nick/timestamps
ok-nick Nov 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/src/assertions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ mod thumbnail;
pub(crate) use thumbnail::Thumbnail;

mod timestamp;
pub(crate) use timestamp::TimeStamp;
pub use timestamp::TimeStamp;

mod user;
pub(crate) use user::User;
Expand Down
114 changes: 70 additions & 44 deletions sdk/src/assertions/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,118 +20,144 @@ use serde_bytes::ByteBuf;
use crate::{
assertion::{Assertion, AssertionBase, AssertionCbor},
assertions::labels,
crypto::cose::CertificateTrustPolicy,
error::Result,
http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver},
http::{AsyncHttpResolver, SyncHttpResolver},
status_tracker::StatusTracker,
Error,
};

/// Helper class to create Timestamp assertions
/// Helper class to create a `TimeStamp` assertion.
///
/// <https://spec.c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#timestamp_assertion>
#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq)]
pub struct TimeStamp(HashMap<String, ByteBuf>);

#[allow(dead_code)]
impl TimeStamp {
/// Label prefix for an [`Timestamp`] assertion.
/// Label prefix for an [`TimeStamp`] assertion.
///
/// See <https://c2pa.org/specifications/specifications/2.2/specs/C2PA_Specification.html#_actions>.
pub const LABEL: &'static str = labels::TIMESTAMP;

/// Construct a new, empty [`TimeStamp`] assertion.
pub fn new() -> Self {
TimeStamp(HashMap::new())
}

//
/// Add a timestamp token for the given manifest id.
pub fn add_timestamp(&mut self, manifest_id: &str, timestamp: &[u8]) {
self.0
.insert(manifest_id.to_string(), ByteBuf::from(timestamp.to_vec()));
}

/// Get the timestamp for a given manifest id
/// Get the timestamp token for a given manifest id.
pub fn get_timestamp(&self, manifest_id: &str) -> Option<&[u8]> {
self.0.get(manifest_id).map(|buf| buf.as_ref())
}

#[async_generic]
pub fn send_timestamp_token_request(tsa_url: &str, message: &[u8]) -> Result<Vec<u8>> {
if _sync {
Self::send_timestamp_token_request_impl(tsa_url, message, &SyncGenericResolver::new())
/// Refresh the timstamp token for a given manifest id.
///
/// The signature is expected to be the `signature` field of the `COSE_Sign1_Tagged` structure
/// found in the C2PA claim signature box of the manifest corresponding to the `manifest_id`.
//
// The `signature` is normally obtained from [`Store::get_cose_sign1_signature`].
//
// [`Store::get_cose_sign1_signature`][crate::store::Store::get_cose_sign1_structure].
#[async_generic(async_signature(
&mut self,
time_authority_url: &str,
manifest_id: &str,
signature: &[u8],
http_resolver: &impl AsyncHttpResolver,
))]
pub(crate) fn refresh_timestamp(
&mut self,
time_authority_url: &str,
manifest_id: &str,
signature: &[u8],
http_resolver: &impl SyncHttpResolver,
) -> Result<()> {
let timestamp_token = if _sync {
TimeStamp::send_timestamp_token_request(time_authority_url, signature, http_resolver)?
} else {
Self::send_timestamp_token_request_impl_async(
tsa_url,
message,
&AsyncGenericResolver::new(),
TimeStamp::send_timestamp_token_request_async(
time_authority_url,
signature,
http_resolver,
)
.await
}
.await?
};

self.0
.insert(manifest_id.to_owned(), ByteBuf::from(timestamp_token));

Ok(())
}

/// Send a timestamp token request to the `time_authority_url` with the given `signature`.
///
/// This function will verify the structure of the returned response but not the trust.
///
/// See [`TimeStamp::refresh_timestamp`] for more information.
#[async_generic(async_signature(
tsa_url: &str,
message: &[u8],
time_authority_url: &str,
signature: &[u8],
http_resolver: &impl AsyncHttpResolver,
))]
pub(crate) fn send_timestamp_token_request_impl(
tsa_url: &str,
message: &[u8],
pub(crate) fn send_timestamp_token_request(
time_authority_url: &str,
signature: &[u8],
http_resolver: &impl SyncHttpResolver,
) -> Result<Vec<u8>> {
use crate::{
crypto::cose::CertificateTrustPolicy, settings::Settings,
status_tracker::StatusTracker, Error,
};

let body = crate::crypto::time_stamp::default_rfc3161_message(message)?;
let body = crate::crypto::time_stamp::default_rfc3161_message(signature)?;
let headers = None;

let bytes = if _sync {
crate::crypto::time_stamp::default_rfc3161_request(
tsa_url,
time_authority_url,
headers,
&body,
message,
signature,
http_resolver,
)
} else {
crate::crypto::time_stamp::default_rfc3161_request_async(
tsa_url,
time_authority_url,
headers,
&body,
message,
signature,
http_resolver,
)
.await
}
.map_err(|_e| Error::OtherError("timestamp token not found".into()))?;
.map_err(|err| Error::OtherError(format!("timestamp token not found: {err:?}").into()))?;

// make sure it is a good response
let ctp = CertificateTrustPolicy::passthrough();
let mut tracker = StatusTracker::default();

// TODO: separate verifying time stamp and verifying time stamp trust into separate functions?
// do we need to pass settings here at all if `ctp` is set to pasthrough anyways?
let mut settings = Settings::default();
settings.verify.verify_timestamp_trust = false;

if _sync {
crate::crypto::time_stamp::verify_time_stamp(
&bytes,
message,
signature,
&ctp,
&mut tracker,
&settings,
false,
)?;
} else {
crate::crypto::time_stamp::verify_time_stamp_async(
&bytes,
message,
signature,
&ctp,
&mut tracker,
&settings,
false,
)
.await?;
}

let token = crate::crypto::cose::timestamptoken_from_timestamprsp(&bytes)
.ok_or(Error::OtherError("timestamp token not found".into()))?;
let token =
crate::crypto::cose::timestamptoken_from_timestamprsp(&bytes).map_err(|err| {
Error::OtherError(format!("timestamp token not found: {err:?}").into())
})?;

Ok(token)
}
Expand Down
126 changes: 110 additions & 16 deletions sdk/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ use crate::{
assertion::{AssertionBase, AssertionDecodeError},
assertions::{
c2pa_action, labels, Action, ActionTemplate, Actions, AssertionMetadata, BmffHash, BoxHash,
DataHash, DigitalSourceType, EmbeddedData, Exif, Metadata, SoftwareAgent, Thumbnail, User,
UserCbor,
DataHash, DigitalSourceType, EmbeddedData, Exif, Metadata, SoftwareAgent, Thumbnail,
TimeStamp, User, UserCbor,
},
claim::Claim,
error::{Error, Result},
http::{AsyncGenericResolver, SyncGenericResolver},
http::{AsyncGenericResolver, AsyncHttpResolver, SyncGenericResolver, SyncHttpResolver},
jumbf::labels::manifest_label_from_uri,
jumbf_io,
resource_store::{ResourceRef, ResourceResolver, ResourceStore},
settings::{self, Settings},
Expand Down Expand Up @@ -1355,12 +1356,7 @@ impl Builder {
}

#[cfg(feature = "add_thumbnails")]
fn maybe_add_thumbnail<R>(
&mut self,
format: &str,
stream: &mut R,
settings: &Settings,
) -> Result<&mut Self>
fn maybe_add_thumbnail<R>(&mut self, format: &str, stream: &mut R) -> Result<&mut Self>
where
R: Read + Seek + ?Sized,
{
Expand All @@ -1370,7 +1366,7 @@ impl Builder {
}

// check settings to see if we should auto generate a thumbnail
let auto_thumbnail = settings.builder.thumbnail.enabled;
let auto_thumbnail = self.settings.builder.thumbnail.enabled;

if self.definition.thumbnail.is_none() && auto_thumbnail {
stream.rewind()?;
Expand All @@ -1380,7 +1376,7 @@ impl Builder {
crate::utils::thumbnail::make_thumbnail_bytes_from_stream(
format,
&mut stream,
settings,
&self.settings,
)?
{
stream.rewind()?;
Expand Down Expand Up @@ -1422,6 +1418,81 @@ impl Builder {
Ok(self)
}

#[async_generic(async_signature(
&mut self,
time_authority_url: &str,
store: &mut Store,
http_resolver: &impl AsyncHttpResolver,
))]
fn maybe_add_timestamp(
&mut self,
time_authority_url: &str,
store: &mut Store,
http_resolver: &impl SyncHttpResolver,
) -> Result<()> {
if self.settings.builder.update_manifest_timestamp_assertion {
let provenance_claim = store.provenance_claim().ok_or(Error::ClaimEncoding)?;
if provenance_claim.update_manifest() {
let parent_claim_id = manifest_label_from_uri(
&provenance_claim
.parent_claim_uri()?
.ok_or(Error::ClaimEncoding)?,
)
.ok_or(Error::ClaimEncoding)?;

// First check if a timestamp assertion already exists.
let timestamp_assertions = provenance_claim.timestamp_assertions();
let mut timestamp_assertion = if !timestamp_assertions.is_empty() {
// There can only be one timestamp assertion per the spec.
let timestamp_assertion =
TimeStamp::from_assertion(timestamp_assertions[0].assertion())?;
if timestamp_assertion
.get_timestamp(&parent_claim_id)
.is_some()
{
return Ok(());
}

timestamp_assertion
} else {
TimeStamp::new()
};

match store.get_cose_sign1_signature(&parent_claim_id)? {
Some(signature) => {
if _sync {
timestamp_assertion.refresh_timestamp(
time_authority_url,
&parent_claim_id,
&signature,
http_resolver,
)?;
} else {
timestamp_assertion
.refresh_timestamp_async(
time_authority_url,
&parent_claim_id,
&signature,
http_resolver,
)
.await?;
}
}
None => return Err(Error::ClaimMissingSignatureBox),
}

let claim = store.provenance_claim_mut().ok_or(Error::ClaimEncoding)?;
if claim.timestamp_assertions().is_empty() {
claim.add_assertion(&timestamp_assertion)?;
} else {
claim.replace_assertion(timestamp_assertion.to_assertion()?)?;
}
}
}

Ok(())
}

// Find an assertion in the manifest.
pub(crate) fn find_assertion<T: DeserializeOwned>(&self, label: &str) -> Result<T> {
if let Some(manifest_assertion) = self
Expand Down Expand Up @@ -1575,7 +1646,6 @@ impl Builder {
R: Read + Seek + Send,
W: Write + Read + Seek + Send,
{
let settings = crate::settings::get_settings().unwrap_or_default();
let http_resolver = if _sync {
SyncGenericResolver::new()
} else {
Expand All @@ -1597,17 +1667,41 @@ impl Builder {

// generate thumbnail if we don't already have one
#[cfg(feature = "add_thumbnails")]
self.maybe_add_thumbnail(&format, source, &settings)?;
self.maybe_add_thumbnail(&format, source)?;

// convert the manifest to a store
let mut store = self.to_store(&settings)?;
let mut store = self.to_store(&self.settings)?;

// add timestamp if conditions allow
if let Some(timestamp_authority_url) = signer.time_authority_url() {
if _sync {
self.maybe_add_timestamp(&timestamp_authority_url, &mut store, &http_resolver)?;
} else {
self.maybe_add_timestamp_async(&timestamp_authority_url, &mut store, &http_resolver)
.await?
}
}

// sign and write our store to to the output image file
if _sync {
store.save_to_stream(&format, source, dest, signer, &http_resolver, &settings)
store.save_to_stream(
&format,
source,
dest,
signer,
&http_resolver,
&self.settings,
)
} else {
store
.save_to_stream_async(&format, source, dest, signer, &http_resolver, &settings)
.save_to_stream_async(
&format,
source,
dest,
signer,
&http_resolver,
&self.settings,
)
.await
}
}
Expand Down
Loading
Loading