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
aa52999
feat: Allow `Builder::sign_async` future to be spawnable on Tokio run…
ok-nick Feb 10, 2026
e9bd453
fix: change `MaybeSync` to `Sync` for async HTTP resolver methods
ok-nick Feb 10, 2026
1a2804d
fix: return `BoxedAsyncResolver` for `Context::resolver_async`
ok-nick Feb 11, 2026
617bf3a
feat: pass HTTP resolvers to timestamp networking
ok-nick Feb 11, 2026
7a89ba5
refactor: return arc-wrapped resolver from context
ok-nick Feb 12, 2026
fe274bd
docs: add back docs for HTTP resolver types
ok-nick Feb 12, 2026
41f7d8c
Merge branch 'ok-nick/sign-async-send' into ok-nick/send-timestamp-re…
ok-nick Feb 12, 2026
1622beb
fix: use arc for sync resolver and use impls instead of trait objects
ok-nick Feb 12, 2026
59dd42c
refactor: require MaybeSend and MaybeSync on the core HTTP resolver t…
ok-nick Feb 12, 2026
0b2fcdf
refactor: simplify by requiring MaybeSend and MaybeSync bounds on HTT…
ok-nick Feb 12, 2026
4c6b2e4
Merge branch 'main' of github.com:contentauth/c2pa-rs into ok-nick/se…
ok-nick Feb 12, 2026
deb378d
fix: new code to use http resolvers in send_timestamp_request
ok-nick Feb 12, 2026
40eb0ca
docs: fix doc links
ok-nick Feb 12, 2026
82ee8d9
fix: private http module for now in this PR
ok-nick Feb 12, 2026
be355f1
Merge branch 'ok-nick/sign-async-send' into ok-nick/send-timestamp-re…
ok-nick Feb 12, 2026
0811740
docs: link TODO to issue
ok-nick Feb 12, 2026
0e9ff94
Merge branch 'ok-nick/send-timestamp-request-respect' into ok-nick/ts…
ok-nick Feb 12, 2026
58e3aca
feat!: call `Signer::send_timestamp_request` for timestamp assertions
ok-nick Feb 12, 2026
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
122 changes: 104 additions & 18 deletions sdk/src/assertions/timestamp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ use crate::{
error::Result,
http::{AsyncHttpResolver, SyncHttpResolver},
status_tracker::StatusTracker,
Error,
AsyncSigner, Error, Signer,
};

/// Helper class to create a `TimeStamp` assertion.
Expand Down Expand Up @@ -60,22 +60,24 @@ impl TimeStamp {
/// 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`].
// The `signature` is normally obtained via [`Claim::cose_sign1`] using the [`CoseSign1::signature`]
// field.
//
// [`Store::get_cose_sign1_signature`][crate::store::Store::get_cose_sign1_structure].
// [`Claim::cose_sign1`][crate::claim::Claim::cose_sign1].
// [`CoseSign1::signature`][coset::CoseSign1::signature].
#[async_generic(async_signature(
&mut self,
tsa_url: &str,
manifest_id: &str,
signature: &[u8],
http_resolver: &(impl AsyncHttpResolver + ?Sized),
http_resolver: &impl AsyncHttpResolver,
))]
pub(crate) fn refresh_timestamp(
pub fn refresh_timestamp(
&mut self,
tsa_url: &str,
manifest_id: &str,
signature: &[u8],
http_resolver: &(impl SyncHttpResolver + ?Sized),
http_resolver: &impl SyncHttpResolver,
) -> Result<()> {
let timestamp_token = if _sync {
TimeStamp::send_timestamp_token_request(tsa_url, signature, http_resolver)?
Expand All @@ -97,12 +99,12 @@ impl TimeStamp {
#[async_generic(async_signature(
tsa_url: &str,
message: &[u8],
http_resolver: &(impl AsyncHttpResolver + ?Sized),
http_resolver: &impl AsyncHttpResolver,
))]
pub(crate) fn send_timestamp_token_request(
pub fn send_timestamp_token_request(
tsa_url: &str,
message: &[u8],
http_resolver: &(impl SyncHttpResolver + ?Sized),
http_resolver: &impl SyncHttpResolver,
) -> Result<Vec<u8>> {
let body = crate::crypto::time_stamp::default_rfc3161_message(message)?;
let headers = None;
Expand All @@ -127,21 +129,110 @@ impl TimeStamp {
}
.map_err(|err| Error::OtherError(format!("timestamp token not found: {err:?}").into()))?;

// make sure it is a good response
if _sync {
Self::verify_timestamp_response(&bytes, message)?;
} else {
Self::verify_timestamp_response_async(&bytes, message).await?;
}

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

Ok(token)
}

/// Equivalent to [`TimeStamp::refresh_timestamp`] except using a [`Signer`].
///
/// [`Signer`]: crate::Signer
#[async_generic(async_signature(
&mut self,
manifest_id: &str,
signature: &[u8],
http_resolver: &impl AsyncHttpResolver,
signer: &(impl AsyncSigner + ?Sized),
))]
pub(crate) fn refresh_timestamp_with_signer(
&mut self,
manifest_id: &str,
signature: &[u8],
http_resolver: &impl SyncHttpResolver,
signer: &(impl Signer + ?Sized),
) -> Result<()> {
let timestamp_token = if _sync {
TimeStamp::send_timestamp_token_request_with_signer(signature, http_resolver, signer)?
} else {
TimeStamp::send_timestamp_token_request_with_signer_async(
signature,
http_resolver,
signer,
)
.await?
};

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

Ok(())
}

/// Equivalent to [`TimeStamp::send_timestamp_token_request`] except using a [`Signer`].
///
/// [`Signer`]: crate::Signer
#[async_generic(async_signature(
message: &[u8],
http_resolver: &impl AsyncHttpResolver,
signer: &(impl AsyncSigner + ?Sized),
))]
pub(crate) fn send_timestamp_token_request_with_signer(
message: &[u8],
http_resolver: &impl SyncHttpResolver,
signer: &(impl Signer + ?Sized),
) -> Result<Vec<u8>> {
let bytes = if _sync {
signer.send_timestamp_request(http_resolver, message)
} else {
signer.send_timestamp_request(http_resolver, message).await
}
// TODO: more explicit error
.ok_or_else(|| Error::UnsupportedType)?
.map_err(|err| Error::OtherError(format!("timestamp token not found: {err:?}").into()))?;

if _sync {
Self::verify_timestamp_response(&bytes, message)?;
} else {
Self::verify_timestamp_response_async(&bytes, message).await?;
}

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

Ok(token)
}

/// Verifies a timestamp respponse given the response bytes and original message.
#[async_generic(async_signature(
bytes: &[u8],
message: &[u8],
))]
fn verify_timestamp_response(bytes: &[u8], message: &[u8]) -> Result<()> {
let ctp = CertificateTrustPolicy::passthrough();
let mut tracker = StatusTracker::default();

if _sync {
crate::crypto::time_stamp::verify_time_stamp(
&bytes,
bytes,
message,
&ctp,
&mut tracker,
false,
)?;
} else {
crate::crypto::time_stamp::verify_time_stamp_async(
&bytes,
bytes,
message,
&ctp,
&mut tracker,
Expand All @@ -150,12 +241,7 @@ impl TimeStamp {
.await?;
}

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

Ok(token)
Ok(())
}
}

Expand Down
55 changes: 40 additions & 15 deletions sdk/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1787,10 +1787,9 @@ impl Builder {
/// [`TimeStampSettings::enabled`]: crate::settings::builder::TimeStampSettings::enabled
#[async_generic(async_signature(
&self,
tsa_url: &str,
provenance_claim: &mut Claim,
))]
fn maybe_add_timestamp(&self, tsa_url: &str, provenance_claim: &mut Claim) -> Result<()> {
fn maybe_add_timestamp(&self, provenance_claim: &mut Claim) -> Result<()> {
let settings = self.context().settings();

if !settings.builder.auto_timestamp_assertion.enabled
Expand All @@ -1799,6 +1798,15 @@ impl Builder {
return Ok(());
}

let signer = if _sync {
self.context().signer()?
} else {
self.context().async_signer()?
};
if signer.time_authority_url().is_none() {
return Ok(());
}

let mut claim_uris = HashSet::new();
match settings.builder.auto_timestamp_assertion.fetch_scope {
TimeStampFetchScope::All => {
Expand Down Expand Up @@ -1868,19 +1876,19 @@ impl Builder {
if let Some(claim) = provenance_claim.claim_ingredient(&manifest_label) {
let signature = claim.cose_sign1()?.signature;
if _sync {
timestamp_assertion.refresh_timestamp(
tsa_url,
timestamp_assertion.refresh_timestamp_with_signer(
&manifest_label,
&signature,
self.context().resolver(),
&self.context().resolver(),
signer,
)?;
} else {
timestamp_assertion
.refresh_timestamp_async(
tsa_url,
.refresh_timestamp_with_signer_async(
&manifest_label,
&signature,
self.context().resolver_async(),
&self.context().resolver_async(),
signer,
)
.await?;
}
Expand Down Expand Up @@ -2074,12 +2082,10 @@ impl Builder {

let mut claim = self.to_claim()?;

if let Some(tsa_url) = signer.time_authority_url() {
if _sync {
self.maybe_add_timestamp(&tsa_url, &mut claim)?;
} else {
self.maybe_add_timestamp_async(&tsa_url, &mut claim).await?
}
if _sync {
self.maybe_add_timestamp(&mut claim)?;
} else {
self.maybe_add_timestamp_async(&mut claim).await?
}

let mut store = self.to_store_with_claim(claim)?;
Expand Down Expand Up @@ -2475,7 +2481,10 @@ impl std::fmt::Display for Builder {
mod tests {
#![allow(clippy::expect_used)]
#![allow(clippy::unwrap_used)]
use std::{io::Cursor, vec};
use std::{
io::{self, Cursor},
vec,
};

use c2pa_macros::c2pa_test_async;
use serde_json::json;
Expand All @@ -2489,6 +2498,7 @@ mod tests {
assertions::{c2pa_action, BoxHash, DigitalSourceType},
crypto::raw_signature::SigningAlg,
hash_stream_by_alg,
maybe_send_sync::MaybeSend,
settings::Settings,
utils::{
test::{test_context, write_jpeg_placeholder_stream},
Expand Down Expand Up @@ -4628,4 +4638,19 @@ mod tests {

assert!(reader.active_manifest().is_some());
}

// Ensures that the future returned by `Builder::sign_async` implements `Send`, thus making it
// possible to spawn on a Tokio runtime.
#[test]
fn test_sign_async_future_is_send() {
fn assert_send<T: MaybeSend>(_: T) {}

let signer = async_test_signer(SigningAlg::Ps256);
let mut builder = Builder::new();
let mut src = io::empty();
let mut dst = io::empty();

let future = builder.sign_async(&signer, "image/jpeg", &mut src, &mut dst);
assert_send(future);
}
}
Loading
Loading