Skip to content

Commit 990788a

Browse files
feat: Implement CAWG X.509 signing via settings (#1388)
1 parent b12f771 commit 990788a

File tree

8 files changed

+1416
-4
lines changed

8 files changed

+1416
-4
lines changed

cli/docs/cawg_x509_signing.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Using an X.509 certificate for CAWG signing
2+
3+
The `c2patool` uses some custom properties in the `cawg_x509_signer` section of the settings file for signing:
4+
5+
- `private_key`: Path to the private key file.
6+
- `sign_cert`: Path to the signing certificate file.
7+
- `alg`: Algorithm to use, if not the default ES256.
8+
9+
Both the private key and signing certificate must be in PEM (privacy-enhanced mail) format. The signing certificate must contain a PEM certificate chain starting with the end-entity certificate used to sign the claim ending with the intermediate certificate before the root CA certificate.
10+
11+
If the settings file doesn't include the `cawg_x509_signer.sign_cert` and `cawg_x509_signer.private_key` properties, c2patool will not generate a CAWG identity assertion. An example settings file demonstrating how this works is provided in the [c2patool repo sample folder](https://github.com/contentauth/c2pa-rs/tree/main/cli/tests/fixtures/trust/cawg_sign_settings.toml).
12+
13+
If you are using a signing algorithm other than the default `es256`, specify it in the manifest definition field `alg` with one of the following values:
14+
15+
- `ps256`
16+
- `ps384`
17+
- `ps512`
18+
- `es256`
19+
- `es384`
20+
- `es512`
21+
- `ed25519`
22+
23+
The specified algorithm must be compatible with the values of private key and signing certificate. For more information, see [Signing manfiests](https://opensource.contentauthenticity.org/docs/signing-manifests).
24+
25+
To sign an asset using this technique, adapt the following command-line invocation:
26+
27+
```sh
28+
$ c2patool \
29+
--settings (path to settings.toml file) \
30+
(path to source file) \
31+
-m (path to manifest definition file) \
32+
-o (path to output file)
33+
```

cli/src/main.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,11 @@ fn main() -> Result<()> {
740740

741741
Box::new(signer)
742742
} else {
743-
sign_config.signer()?
743+
match Settings::signer() {
744+
Ok(signer) => signer,
745+
Err(Error::MissingSignerSettings) => sign_config.signer()?,
746+
Err(err) => Err(err)?,
747+
}
744748
};
745749

746750
if let Some(output) = args.output {

cli/tests/fixtures/trust/cawg_sign_settings.toml

Lines changed: 581 additions & 0 deletions
Large diffs are not rendered by default.

cli/tests/integration.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,3 +616,37 @@ fn tool_read_image_with_details_with_cawg_data() -> Result<(), Box<dyn Error>> {
616616
.stdout(str::contains("cawg.identity.well-formed"));
617617
Ok(())
618618
}
619+
620+
#[test]
621+
// c2patool --settings .../trust/cawg_test_settings.toml C_with_CAWG_data.jpg
622+
fn tool_sign_image_with_cawg_data() -> Result<(), Box<dyn Error>> {
623+
let tmp_dir = tempdir()?;
624+
let file_path = tmp_dir.path().join("same_image.jpg");
625+
fs::copy(fixture_path(TEST_IMAGE), &file_path)?;
626+
627+
let output_path = tmp_dir.path().join("same_image_cawg_signed.jpg");
628+
629+
Command::cargo_bin("c2patool")?
630+
.arg("--settings")
631+
.arg(fixture_path("trust/cawg_sign_settings.toml"))
632+
.arg(&file_path)
633+
.arg("-m")
634+
.arg(fixture_path("ingredient_test.json"))
635+
.arg("-o")
636+
.arg(&output_path)
637+
.arg("-f")
638+
.assert()
639+
.success();
640+
641+
Command::cargo_bin("c2patool")?
642+
.arg("--settings")
643+
.arg(fixture_path("trust/cawg_sign_settings.toml"))
644+
.arg(&output_path)
645+
.assert()
646+
.success()
647+
.stdout(str::contains("cawg.identity"))
648+
.stdout(str::contains("c2pa.assertions/cawg.training-mining"));
649+
// .stdout(str::contains("cawg.identity.well-formed"));
650+
// ^^ Enable this when #1356 lands.
651+
Ok(())
652+
}

sdk/src/settings/mod.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,8 @@ pub struct Settings {
259259
builder: BuilderSettings,
260260
#[serde(skip_serializing_if = "Option::is_none")]
261261
signer: Option<SignerSettings>,
262+
#[serde(skip_serializing_if = "Option::is_none")]
263+
cawg_x509_signer: Option<SignerSettings>,
262264
}
263265

264266
impl Settings {
@@ -412,7 +414,7 @@ impl Settings {
412414
Ok(toml::to_string_pretty(&settings)?)
413415
}
414416

415-
/// Returns the construct signer from the `signer` field.
417+
/// Returns the constructed signer from the `signer` field.
416418
///
417419
/// If the signer settings aren't specified, this function will return [Error::MissingSignerSettings].
418420
#[inline]
@@ -432,6 +434,7 @@ impl Default for Settings {
432434
verify: Default::default(),
433435
builder: Default::default(),
434436
signer: None,
437+
cawg_x509_signer: None,
435438
}
436439
}
437440
}
@@ -446,6 +449,9 @@ impl SettingsValidate for Settings {
446449
if let Some(signer) = &self.signer {
447450
signer.validate()?;
448451
}
452+
if let Some(cawg_x509_signer) = &self.cawg_x509_signer {
453+
cawg_x509_signer.validate()?;
454+
}
449455
self.trust.validate()?;
450456
self.cawg_trust.validate()?;
451457
self.core.validate()?;

sdk/src/settings/signer.rs

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@ use serde::{Deserialize, Serialize};
1515

1616
use crate::{
1717
create_signer,
18+
crypto::raw_signature::RawSigner,
19+
dynamic_assertion::DynamicAssertion,
20+
identity::{builder::IdentityAssertionBuilder, x509::X509CredentialHolder},
1821
settings::{Settings, SettingsValidate},
1922
Error, Result, Signer, SigningAlg,
2023
};
2124

22-
/// Settings for configuring a local or remote [Signer][crate::Signer].
25+
/// Settings for configuring a local or remote [`Signer`].
2326
///
24-
/// A [Signer][crate::Signer] can be obtained by calling [BuilderSettings::signer].
27+
/// A [`Signer`] can be obtained by calling the [`signer()`] function.
28+
///
29+
/// [`Signer`]: crate::Signer
30+
/// [`signer()`]: Builder::signer
2531
#[allow(unused)]
2632
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
2733
#[serde(rename_all = "lowercase")]
@@ -58,7 +64,46 @@ impl SignerSettings {
5864
///
5965
/// If the signer settings aren't specified, this function will return [Error::MissingSignerSettings][crate::Error::MissingSignerSettings].
6066
pub fn signer() -> Result<Box<dyn Signer>> {
67+
let c2pa_signer = Self::c2pa_signer()?;
68+
69+
// TO DISCUSS: What if get_value returns an Err(...)?
70+
if let Ok(Some(cawg_x509_settings)) =
71+
Settings::get_value::<Option<SignerSettings>>("cawg_x509_signer")
72+
{
73+
match cawg_x509_settings {
74+
SignerSettings::Local {
75+
alg: cawg_alg,
76+
sign_cert: cawg_sign_cert,
77+
private_key: cawg_private_key,
78+
tsa_url: cawg_tsa_url,
79+
} => {
80+
let cawg_dual_signer = CawgX509IdentitySigner {
81+
c2pa_signer,
82+
cawg_alg,
83+
cawg_sign_cert,
84+
cawg_private_key,
85+
cawg_tsa_url,
86+
};
87+
88+
Ok(Box::new(cawg_dual_signer))
89+
}
90+
91+
SignerSettings::Remote {
92+
url: _url,
93+
alg: _alg,
94+
sign_cert: _sign_cert,
95+
tsa_url: _tsa_url,
96+
} => todo!("Remote CAWG X.509 signing not yet supported"),
97+
}
98+
} else {
99+
Ok(c2pa_signer)
100+
}
101+
}
102+
103+
/// Returns a C2PA-only signer from the [`BuilderSettings::signer`] field.
104+
fn c2pa_signer() -> Result<Box<dyn Signer>> {
61105
let signer_info = Settings::get_value::<Option<SignerSettings>>("signer");
106+
62107
match signer_info {
63108
Ok(Some(signer_info)) => match signer_info {
64109
SignerSettings::Local {
@@ -107,6 +152,85 @@ impl SettingsValidate for SignerSettings {
107152
}
108153
}
109154

155+
struct CawgX509IdentitySigner {
156+
c2pa_signer: Box<dyn Signer>,
157+
cawg_alg: SigningAlg,
158+
cawg_sign_cert: String,
159+
cawg_private_key: String,
160+
cawg_tsa_url: Option<String>,
161+
// NOTE: The CAWG signing settings are stored here because
162+
// we can't clone or transfer ownership of an `X509CredentialHolder`
163+
// inside the dynamic_assertions callback.
164+
}
165+
166+
impl Signer for CawgX509IdentitySigner {
167+
fn sign(&self, data: &[u8]) -> Result<Vec<u8>> {
168+
Signer::sign(&self.c2pa_signer, data)
169+
}
170+
171+
fn alg(&self) -> SigningAlg {
172+
Signer::alg(&self.c2pa_signer)
173+
}
174+
175+
fn certs(&self) -> Result<Vec<Vec<u8>>> {
176+
self.c2pa_signer.certs()
177+
}
178+
179+
fn reserve_size(&self) -> usize {
180+
Signer::reserve_size(&self.c2pa_signer)
181+
}
182+
183+
fn time_authority_url(&self) -> Option<String> {
184+
self.c2pa_signer.time_authority_url()
185+
}
186+
187+
fn timestamp_request_headers(&self) -> Option<Vec<(String, String)>> {
188+
self.c2pa_signer.timestamp_request_headers()
189+
}
190+
191+
fn timestamp_request_body(&self, message: &[u8]) -> Result<Vec<u8>> {
192+
self.c2pa_signer.timestamp_request_body(message)
193+
}
194+
195+
fn send_timestamp_request(&self, message: &[u8]) -> Option<Result<Vec<u8>>> {
196+
self.c2pa_signer.send_timestamp_request(message)
197+
}
198+
199+
fn ocsp_val(&self) -> Option<Vec<u8>> {
200+
self.c2pa_signer.ocsp_val()
201+
}
202+
203+
fn direct_cose_handling(&self) -> bool {
204+
self.c2pa_signer.direct_cose_handling()
205+
}
206+
207+
fn dynamic_assertions(&self) -> Vec<Box<dyn DynamicAssertion>> {
208+
let Ok(raw_signer) = crate::crypto::raw_signature::signer_from_cert_chain_and_private_key(
209+
self.cawg_sign_cert.as_bytes(),
210+
self.cawg_private_key.as_bytes(),
211+
self.cawg_alg,
212+
self.cawg_tsa_url.clone(),
213+
) else {
214+
// dynamic_assertions() API doesn't let us fail.
215+
// signer_from_cert_chain_and_private_key rarely fails,
216+
// so when it does, we do so silently.
217+
return vec![];
218+
};
219+
220+
let x509_credential_holder = X509CredentialHolder::from_raw_signer(raw_signer);
221+
222+
let iab = IdentityAssertionBuilder::for_credential_holder(x509_credential_holder);
223+
224+
// TODO: Configure referenced assertions and role.
225+
226+
vec![Box::new(iab)]
227+
}
228+
229+
fn raw_signer(&self) -> Option<Box<&dyn RawSigner>> {
230+
self.c2pa_signer.raw_signer()
231+
}
232+
}
233+
110234
#[cfg(not(target_arch = "wasm32"))]
111235
#[derive(Debug)]
112236
pub(crate) struct RemoteSigner {

0 commit comments

Comments
 (0)