ruby-c2pa 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc05300f370b0f4e0ac3edf08d92b55b305e8a16d6554342a077f0ac3f397eea
4
- data.tar.gz: c790eb285655a76e3fbe9ae46c0803718b5b08bbda9ff0841e4716775811ac9a
3
+ metadata.gz: 66e048e5359881f08def1d3fd82ed11fb8b9b3dfc0d8232d9dbc145026841d39
4
+ data.tar.gz: bd9108538b9e6d47bd3684d974d2145963345cbdbe67ec44250a1892dd277957
5
5
  SHA512:
6
- metadata.gz: 144409841d84d8a311ffb785e60a5f59a0616d420df89c0daf03da4c0f772e005e6a4bc93da375fcfa5a078856bb2182f133be9d4a79319850f62c7324474343
7
- data.tar.gz: 0c8b604f64fd03404bbb031478047859a965f40ed1328bd061d64ae68acef999d0ad351f3f365167c4bd36d96888a772b26056d1daff94772992f5d4d79dd13b
6
+ metadata.gz: 65b8729aa03c0fb5145f1631fc5c847960be95cfd792553d0352c9c9e7f54451a077b3c7eb599c45ff3b7007698aaa9a91d3f05848c0307f192c5d5de2333c25
7
+ data.tar.gz: 0f46873209bbc9b081a2ef24c276adb0daf079466366a01a712a504d3f0df1eb64b20949fa2f171c469eba9e0da48f429c569b09cba8d5ee48f6e978d3548f72
data/CHANGELOG.md ADDED
@@ -0,0 +1,153 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.4.0] — 2026-09-11
11
+
12
+ Adds capability on top of 0.3.0. Nothing is removed and no existing call
13
+ changes behaviour, so this is a minor release.
14
+
15
+ ### Added
16
+
17
+ - Provenance chaining through ingredients. `add_ingredient` takes an optional
18
+ `file:`. When the file carries content credentials, its manifest is embedded
19
+ in the signed output and the ingredient points at it, so a verifier can follow
20
+ the chain from your asset back through the original. The description-only
21
+ form is unchanged.
22
+ - `intent: :update` on `C2PA::Manifest`, for non-editorial changes such as
23
+ correcting metadata. The source file is the parent.
24
+ - `C2PA.configure`, for trust and verification settings. Add a private CA's
25
+ root as a trust anchor and its certificates validate as `Trusted`; disable
26
+ `remote_manifest_fetch` and `ocsp_fetch` for environments without network
27
+ access. Only values you set are sent, so defaults are c2pa-rs's own.
28
+ - Reading content credentials from PDFs. `C2PA.read` now parses a PDF and
29
+ returns its manifest if one is present. Signing a PDF remains impossible;
30
+ c2pa-rs has no PDF writer and upstream closed the request to add one.
31
+ - `C2PA::InvalidSettingsError`, raised by `C2PA.configure` for unusable input.
32
+
33
+ ### Changed
34
+
35
+ - c2pa-rs 0.90.15 → 0.90.22. The range includes security fixes: `h2` updated
36
+ for RUSTSEC-2026-0258, `chacha20` moved off a yanked version, hardening of
37
+ BMFF chunk-index handling and CAWG identity bindings. The lockfile ships in
38
+ the gem, so installers get these once they upgrade.
39
+ - The native layer uses c2pa-rs's Context API rather than the deprecated
40
+ `Builder::from_json` and `Reader::from_file`, which read settings from
41
+ thread-local state. One shared Context is reused across calls. Signing from
42
+ several threads concurrently is now tested.
43
+ - CI fails if a deprecated c2pa-rs API reappears.
44
+
45
+ ### Documentation
46
+
47
+ - The certificate section no longer claims certificates must chain to a CA in
48
+ the C2PA trust list, or that self-signed certificates are rejected as such.
49
+ A private CA works; its certificates carry `signingCredential.untrusted`
50
+ until the root is added as an anchor. The certificate profile c2pa-rs
51
+ enforces is listed, including the Authority Key Identifier that
52
+ `openssl x509 -req` omits by default.
53
+ - PDF is listed as read-only, with a note on what the test suite can and
54
+ cannot show: nothing available can produce a C2PA-signed PDF, so reading one
55
+ is untested here, though the code doing it is c2pa-rs's own.
56
+
57
+ ## [0.3.0] — 2026-08-25
58
+
59
+ Runs on c2pa-rs 0.90, and fixes every defect found while building a test suite
60
+ that verifies against c2pa-rs rather than against itself.
61
+
62
+ ### If you have signed files with an earlier version
63
+
64
+ **Re-sign them.** Assets signed by 0.2.1 and earlier are rejected by current
65
+ verifiers, because their `c2pa.created` action carries no `digitalSourceType`.
66
+ c2pa-rs began requiring one in 0.90, and validation happens when a file is
67
+ read, not when it is written — so files that were valid when signed became
68
+ invalid as verifiers updated around them.
69
+
70
+ Anything signed using the README's editing example never validated at all. See
71
+ `c2pa.opened` under Fixed.
72
+
73
+ ### Breaking
74
+
75
+ - `c2pa.created` now requires a `digital_source_type`. c2pa-rs accepts any
76
+ string, so a default would validate while asserting something untrue about an
77
+ asset's origin — a camera original and generative AI output are not
78
+ interchangeable claims. Use `C2PA::DigitalSourceTypes::UNSPECIFIED` when the
79
+ origin is genuinely unknown.
80
+ - `C2PA.sign` now reads the signed file back and raises `C2PA::SigningError` if
81
+ it does not validate, deleting the output. Pass `verify: false` to keep it.
82
+ Code that previously produced invalid files now fails instead of succeeding
83
+ quietly.
84
+ - `add_action(C2PA::Actions::OPENED)` now raises. The action must reference its
85
+ parent ingredient by hashed URI, which cannot be built from Ruby. Pass
86
+ `intent: :edit` to `C2PA::Manifest.new` instead.
87
+ - `c2pa.translated` now requires `sourceLanguage` and `targetLanguage`
88
+ parameters, as RFC 5646 codes.
89
+ - Invalid UTF-8 in a manifest now raises `C2PA::InvalidManifestError` rather
90
+ than `JSON::GeneratorError`, so `rescue C2PA::Error` catches it as the README
91
+ has always claimed.
92
+
93
+ ### Fixed
94
+
95
+ - **Signing a TIFF aborted the Ruby process.** An invalid free in `atree`
96
+ 0.5.3, pulled transitively by c2pa-rs 0.78.3. No exception was raised, so it
97
+ could not be rescued; in a web process it took down the worker. Fixed
98
+ upstream in c2pa-rs 0.78.4, one day before 0.2.1 was published.
99
+ - **`c2pa.opened` produced files that never validated.** The README documented
100
+ adding the action directly, which cannot work. Editing is now supported
101
+ through `intent: :edit`, which lets c2pa-rs derive the parent ingredient from
102
+ the source and wire the action to it.
103
+ - **Signed files credited `c2pa-rs` as the claim generator.** They now credit
104
+ `ruby-c2pa`, and an application can name itself with `generator_name:`.
105
+ - **The gemspec homepage pointed at a repository that does not exist**, so the
106
+ link from RubyGems returned 404.
107
+ - **The gem shipped 1.3 MB of Rust build artifacts**, and its contents varied
108
+ with whatever had been compiled on the machine that built it.
109
+ - **`Cargo.lock` was never packaged.** The extension compiles at install time,
110
+ so every installer resolved dependencies afresh — which is how 0.2.1 shipped
111
+ against the broken `atree` even though the repository pinned it.
112
+ - **PDF was advertised as signable.** c2pa-rs has no PDF writer at any version;
113
+ signing one raises, and the documentation now says so.
114
+
115
+ ### Added
116
+
117
+ - `C2PA::DigitalSourceTypes` — the IPTC vocabulary, plus `UNSPECIFIED` for
118
+ declining to claim an origin rather than guessing.
119
+ - `intent: :edit` on `C2PA::Manifest`, for signing an asset derived from
120
+ another one.
121
+ - `generator_name:` and `generator_version:`, for naming your application as
122
+ the claim generator.
123
+ - `verify:` on `C2PA.sign`, defaulting to `true`.
124
+ - Continuous integration on Linux and macOS. macOS is not redundant: the
125
+ invalid free behind the TIFF crash was surfaced by macOS libmalloc, where
126
+ glibc may corrupt the heap silently.
127
+ - `CONTRIBUTING.md`, recording the rule that every test must be shown to fail
128
+ before it is merged.
129
+
130
+ ### Changed
131
+
132
+ - c2pa-rs 0.78.3 → 0.90.15.
133
+ - Verified format support: JPEG, PNG, WebP, TIFF, AVIF, JPEG XL, MP4, MOV, MP3,
134
+ WAV. Each has a fixture and a signing test. MOV, MP3 and JPEG XL were
135
+ previously undocumented; PDF was documented and never worked.
136
+ - All seven signing algorithms are exercised — es256, es384, es512, ps256,
137
+ ps384, ps512, ed25519 — each against a key of the matching type.
138
+ - The test suite went from 10 tests that compared the code to itself to 85 that
139
+ sign real files and assert on what c2pa-rs reads back.
140
+
141
+ ## [0.2.1] — 2026-03-17
142
+
143
+ Tagged retroactively. See the v0.2.1 tag for the defects it shipped with.
144
+
145
+ ## [0.2.0] — 2026-03-17
146
+
147
+ Tagged retroactively.
148
+
149
+ [Unreleased]: https://github.com/eddorre/ruby-c2pa/compare/v0.4.0...HEAD
150
+ [0.4.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.3.0...v0.4.0
151
+ [0.3.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.2.1...v0.3.0
152
+ [0.2.1]: https://github.com/eddorre/ruby-c2pa/compare/v0.2.0...v0.2.1
153
+ [0.2.0]: https://github.com/eddorre/ruby-c2pa/releases/tag/v0.2.0
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,117 @@
1
+ # Contributing
2
+
3
+ ## Running the tests
4
+
5
+ ```bash
6
+ bundle install
7
+ bundle exec rake test
8
+ ```
9
+
10
+ That compiles the native extension, generates signing certificates, and runs
11
+ the suite. A clean checkout needs no manual setup and no network access.
12
+
13
+ Regenerating the media fixtures is the one thing that needs extra tooling, and
14
+ only if you are changing them:
15
+
16
+ ```bash
17
+ brew install ffmpeg webp jpeg-xl exiftool # macOS
18
+ ./test/fixtures/generate.sh
19
+ ```
20
+
21
+ ## The testing rule
22
+
23
+ **Every test must be shown to fail.** Break the code it covers on purpose,
24
+ confirm the test catches it, put the evidence in the pull request.
25
+
26
+ This is not a general principle borrowed from somewhere. It comes from this
27
+ repository's own history. Version 0.2.1 shipped with a green test suite, and
28
+ that suite passed while the gem:
29
+
30
+ - aborted the Ruby process on any TIFF input
31
+ - emitted manifests that modern verifiers reject
32
+ - advertised PDF signing, which c2pa-rs cannot do at any version
33
+ - documented an editing workflow that has never produced a valid file
34
+
35
+ The suite passed because its assertions compared the code to itself:
36
+
37
+ ```ruby
38
+ assert_equal "c2pa.created", C2PA::Actions::CREATED # a literal equals itself
39
+ assert_equal "c2pa.created", actions[0]["action"] # JSON contains what we put in
40
+ ```
41
+
42
+ Ten tests, and not one of them could have failed for any of those defects. A
43
+ test that cannot fail is decoration.
44
+
45
+ ### How to check
46
+
47
+ Change the implementation so the behaviour under test is wrong, run the suite,
48
+ confirm the right test fails, then restore:
49
+
50
+ ```ruby
51
+ # lib/c2pa/manifest.rb
52
+ "title" => @title.reverse, # deliberately wrong
53
+ ```
54
+
55
+ ```bash
56
+ bundle exec ruby -Ilib -Itest test/c2pa_test.rb
57
+ git checkout -- lib/c2pa/manifest.rb
58
+ ```
59
+
60
+ Then state it in the pull request:
61
+
62
+ > Verified by reversing the title in `Manifest#to_json`: 19 failures, against 0
63
+ > for the unmutated suite.
64
+
65
+ A broad mutation like that trips many tests, which is fine. A narrow one that
66
+ trips exactly the test you just wrote is better evidence, because it shows the
67
+ test is specific as well as present.
68
+
69
+ Mutating the Rust in `ext/c2pa_native/` counts double — it proves the test
70
+ reaches through the FFI boundary rather than stopping at Ruby.
71
+
72
+ ### Two traps, both hit while building this suite
73
+
74
+ **A mutation that does not do what you think.** A NUL-handling test appeared to
75
+ survive a mutation that stripped NUL bytes, which would have meant the test was
76
+ worthless. The mutation was wrong — a shell escape that deleted backslash-zero
77
+ rather than an actual NUL. Written correctly, the test failed immediately.
78
+ Prefer editing the file directly over shell substitution, and check the mutated
79
+ source before trusting the result.
80
+
81
+ **A harness that only fails one way.** For tests that assert on an external
82
+ verdict rather than on our own code, mutate in both directions. A harness made
83
+ to report no failures and a harness made to report spurious ones fail different
84
+ tests; checking only one leaves the other half unverified.
85
+
86
+ ## Where there is no oracle
87
+
88
+ Most assertions can be checked against c2pa-rs, by signing a file and reading
89
+ back its verdict. Some cannot, and those must say so rather than pretend:
90
+
91
+ - **Action names** — c2pa-rs does not validate them. `acme.nonsense` signs and
92
+ reads back clean, so `C2PA::Actions` can only be checked for duplicates and a
93
+ correct namespace.
94
+ - **Digital source types** — any string is accepted, so the constants are only
95
+ as good as the IPTC vocabulary they were transcribed from.
96
+
97
+ Where a rule is enforced more strictly here than by c2pa-rs, label it as a
98
+ deliberate divergence and add a test that fails when upstream changes its mind.
99
+ The gem may be stricter than the SDK; it may not claim the SDK agrees.
100
+
101
+ ## Working on an issue
102
+
103
+ Issues are grouped into sprints by label and tracked against a release
104
+ milestone. Each carries acceptance criteria and a verification command.
105
+
106
+ ```bash
107
+ git checkout -b fix/42-short-description
108
+ # ...
109
+ gh pr create --milestone "<milestone>" --label "<sprint label>"
110
+ ```
111
+
112
+ Reference the issue with `Closes #42` so it closes on merge. CI runs the suite
113
+ on Linux and macOS for every pull request; both must pass.
114
+
115
+ macOS is not redundant in that matrix. The invalid free behind the TIFF crash
116
+ was surfaced by macOS libmalloc, which aborts on a bad free where glibc may
117
+ corrupt the heap silently.
data/README.md CHANGED
@@ -64,22 +64,44 @@ The native Rust library is compiled automatically during installation. This take
64
64
 
65
65
  ## Preparing your certificate and key
66
66
 
67
- C2PA signing requires an X.509 certificate chain and private key in PEM format. The certificate must chain to a CA that is trusted by the C2PA ecosystem — **self-signed certificates are rejected by c2pa-rs**.
67
+ C2PA signing requires an X.509 certificate chain and private key in PEM format.
68
+ The certificate file contains the end-entity certificate first, then any
69
+ intermediates, and must **not** include the root.
70
+
71
+ c2pa-rs enforces a certificate profile. An end-entity certificate is rejected
72
+ unless it carries all of:
73
+
74
+ - Basic Constraints `CA:FALSE`, critical
75
+ - Key Usage with `digitalSignature` or `nonRepudiation`, critical
76
+ - an Extended Key Usage that is present and not `any`, critical
77
+ - a Subject Key Identifier
78
+ - an **Authority Key Identifier**
79
+
80
+ The last one is easy to miss — `openssl x509 -req` omits it by default, and the
81
+ certificate is then rejected with nothing more specific than
82
+ `the certificate is invalid`.
83
+
84
+ ### Whose CA?
85
+
86
+ A certificate from a CA in the C2PA trust list validates as `Trusted` with no
87
+ configuration.
88
+
89
+ A certificate from your own CA validates as `Valid` and carries
90
+ `signingCredential.untrusted` — which does **not** make the manifest invalid.
91
+ To have it treated as trusted, add your root as a trust anchor; see
92
+ [Configuring trust](#configuring-trust).
68
93
 
69
94
  ### Development and testing
70
95
 
71
- The c2pa-rs project publishes test certificates that work for local development:
96
+ The test suite generates its own certificates for every supported algorithm:
72
97
 
73
98
  ```bash
74
- curl -sL -o test_cert.pem https://raw.githubusercontent.com/contentauth/c2pa-rs/main/sdk/tests/fixtures/certs/es256.pub
75
- curl -sL -o test_key.pem https://raw.githubusercontent.com/contentauth/c2pa-rs/main/sdk/tests/fixtures/certs/es256.pem
99
+ bundle exec rake fixtures:certs
76
100
  ```
77
101
 
78
- Files signed with these test certificates will include a `signingCredential.untrusted` validation warning since the test CA is not in the public trust list, but are otherwise valid for development purposes.
79
-
80
- ### Production
81
-
82
- Obtain a certificate from a CA trusted by the C2PA ecosystem. The certificate file must contain the full chain (end-entity certificate first, then any intermediates), but must **not** include the root CA.
102
+ They land in `test/fixtures/certs/` and are not committed. See
103
+ [`test/fixtures/generate_certs.rb`](test/fixtures/generate_certs.rb) for a
104
+ worked example of building a chain c2pa-rs accepts.
83
105
 
84
106
  The supported signing algorithms are: `es256`, `es384`, `es512`, `ps256`, `ps384`, `ps512`, `ed25519`.
85
107
 
@@ -93,18 +115,63 @@ Every signed file requires a `C2PA::Manifest` with at least one action. Actions
93
115
  require "c2pa"
94
116
 
95
117
  manifest = C2PA::Manifest.new(title: "Sunset over the bay")
96
- manifest.add_action(C2PA::Actions::CREATED)
118
+ manifest.add_action(
119
+ C2PA::Actions::CREATED,
120
+ digital_source_type: C2PA::DigitalSourceTypes::DIGITAL_CAPTURE
121
+ )
97
122
  ```
98
123
 
124
+ `c2pa.created` must declare how the asset came into being. c2pa-rs rejects a
125
+ manifest without it, and accepts any string you supply — so a wrong value
126
+ validates while asserting something untrue. The gem therefore requires you to
127
+ choose rather than defaulting on your behalf:
128
+
129
+ | Constant | Use for |
130
+ |----------|---------|
131
+ | `DIGITAL_CAPTURE` | a camera original |
132
+ | `TRAINED_ALGORITHMIC_MEDIA` | generative AI output |
133
+ | `COMPOSITE_WITH_TRAINED_ALGORITHMIC_MEDIA` | edited using generative AI |
134
+ | `SCREEN_CAPTURE` | a screenshot |
135
+ | `HUMAN_EDITS` | human-edited media |
136
+ | `UNSPECIFIED` | the origin is genuinely unknown |
137
+
138
+ `C2PA::DigitalSourceTypes::ALL` lists them all. Reach for `UNSPECIFIED` when you
139
+ do not know — it is what c2pa-rs uses in its own fixtures, and it is honest in a
140
+ way that guessing is not.
141
+
142
+ This requirement arrived in c2pa-rs 0.90. Manifests signed by releases before
143
+ 0.3.0 omit the field and are rejected by current verifiers.
144
+
99
145
  Actions can be chained:
100
146
 
101
147
  ```ruby
102
- manifest = C2PA::Manifest.new(title: "Edited photo")
103
- .add_action(C2PA::Actions::OPENED)
148
+ manifest = C2PA::Manifest.new(title: "Sunset over the bay")
149
+ .add_action(C2PA::Actions::CREATED,
150
+ digital_source_type: C2PA::DigitalSourceTypes::DIGITAL_CAPTURE)
151
+ .add_action(C2PA::Actions::PUBLISHED)
152
+ ```
153
+
154
+ ### Editing an existing asset
155
+
156
+ When the file you are signing derives from another one, declare the intent
157
+ rather than adding `c2pa.opened` yourself:
158
+
159
+ ```ruby
160
+ manifest = C2PA::Manifest.new(title: "Edited photo", intent: :edit)
104
161
  .add_action(C2PA::Actions::EDITED)
105
162
  .add_action(C2PA::Actions::PUBLISHED)
106
163
  ```
107
164
 
165
+ c2pa-rs derives the parent ingredient from the source file and adds a
166
+ `c2pa.opened` action tied to it, so the signed manifest records
167
+ `c2pa.opened`, `c2pa.edited`, `c2pa.published` and a `parentOf` ingredient.
168
+
169
+ `c2pa.opened` cannot be added by hand. The specification requires it to
170
+ reference its parent ingredient by hashed URI, and that hash is computed over
171
+ the ingredient as c2pa-rs serialises it — so `add_action(C2PA::Actions::OPENED)`
172
+ raises and points here. Earlier releases of this gem documented adding it
173
+ directly; manifests built that way never validated.
174
+
108
175
  Each action accepts optional fields from the C2PA specification:
109
176
 
110
177
  ```ruby
@@ -167,23 +234,50 @@ manifest.add_assertion(
167
234
 
168
235
  ### Adding ingredients
169
236
 
170
- Ingredients record the source assets a file was derived from:
237
+ Ingredients record the source assets a file was derived from. Supply the file
238
+ so c2pa-rs can read it:
171
239
 
172
240
  ```ruby
173
241
  manifest.add_ingredient(
174
242
  title: "Original photo",
175
243
  format: "image/jpeg",
176
- instance_id: "xmp:iid:original-uuid-here"
244
+ instance_id: "xmp:iid:original-uuid-here",
245
+ relationship: "componentOf",
246
+ file: "original.jpg"
177
247
  )
178
248
  ```
179
249
 
250
+ If the ingredient already carries content credentials, its manifest is embedded
251
+ in the signed output and the ingredient points at it. A verifier can then
252
+ follow the chain from your asset back through the original. For a file with no
253
+ credentials there is nothing to carry forward.
254
+
255
+ Omitting `file:` records the description alone. Nothing binds it to any bytes,
256
+ so a verifier cannot check the claim. That form is kept for compatibility;
257
+ prefer the file.
258
+
259
+ ### Updating an existing asset
260
+
261
+ For a non-editorial change to an asset, such as correcting metadata, use
262
+ `intent: :update`. The source file is the parent, and the change is recorded
263
+ against it without opening a new editing lineage:
264
+
265
+ ```ruby
266
+ manifest = C2PA::Manifest.new(title: "Metadata corrected", intent: :update)
267
+ .add_action(C2PA::Actions::EDITED_METADATA)
268
+ ```
269
+
270
+ c2pa-rs restricts this mode: there is exactly one ingredient, it is the source
271
+ itself, and the hashed content must not change.
272
+
180
273
  ### Signing a file
181
274
 
182
275
  The `output` path must not already exist — `C2PA.sign` will raise a `C2PA::SigningError` if the file is already there.
183
276
 
184
277
  ```ruby
185
278
  manifest = C2PA::Manifest.new(title: "Sunset over the bay")
186
- .add_action(C2PA::Actions::CREATED)
279
+ .add_action(C2PA::Actions::CREATED,
280
+ digital_source_type: C2PA::DigitalSourceTypes::DIGITAL_CAPTURE)
187
281
 
188
282
  C2PA.sign(
189
283
  file: "photo.jpg",
@@ -194,6 +288,28 @@ C2PA.sign(
194
288
  )
195
289
  ```
196
290
 
291
+ `C2PA.sign` reads the signed file back and confirms it validates before
292
+ returning. If it does not, the output file is deleted and a
293
+ `C2PA::SigningError` is raised naming the failure codes.
294
+
295
+ This matters because c2pa-rs applies its rules when *reading*, not when
296
+ writing. Signing reports success for manifests that every verifier rejects,
297
+ which is exactly what earlier versions of this gem did — silently, for months.
298
+ The check costs one extra read of the output.
299
+
300
+ Turn it off with `verify: false` if you want the file kept for inspection:
301
+
302
+ ```ruby
303
+ C2PA.sign(
304
+ file: "photo.jpg",
305
+ output: "photo_signed.jpg",
306
+ certificate: "cert.pem",
307
+ key: "key.pem",
308
+ manifest: manifest,
309
+ verify: false
310
+ )
311
+ ```
312
+
197
313
  Specify a different signing algorithm with `algorithm:` (default is `"es256"`):
198
314
 
199
315
  ```ruby
@@ -214,9 +330,43 @@ result = C2PA.read(file: "photo_signed.jpg")
214
330
 
215
331
  active = result["manifests"][result["active_manifest"]]
216
332
  puts active["title"]
217
- puts active["claim_generator_info"].first["name"]
333
+ puts active["claim_generator_info"].first["name"] # => "ruby-c2pa"
218
334
  ```
219
335
 
336
+ ### Naming your application
337
+
338
+ Signed files credit `ruby-c2pa` by default. To credit your own application
339
+ instead:
340
+
341
+ ```ruby
342
+ manifest = C2PA::Manifest.new(
343
+ title: "Sunset over the bay",
344
+ generator_name: "Acme Editor",
345
+ generator_version: "2.0"
346
+ ).add_action(
347
+ C2PA::Actions::CREATED,
348
+ digital_source_type: C2PA::DigitalSourceTypes::DIGITAL_CAPTURE
349
+ )
350
+ ```
351
+
352
+ The signed manifest then reads:
353
+
354
+ ```json
355
+ {
356
+ "name": "Acme Editor",
357
+ "version": "2.0",
358
+ "org.rubygems.ruby_c2pa": "0.3.0",
359
+ "org.contentauth.c2pa_rs": "0.78.8"
360
+ }
361
+ ```
362
+
363
+ c2pa-rs permits exactly one claim generator entry, so your application replaces
364
+ the gem as the name rather than preceding it. The gem is recorded in a
365
+ namespaced field alongside it, which is how c2pa-rs records itself.
366
+
367
+ Releases before 0.3.0 credited `c2pa-rs` and named neither the gem nor the
368
+ calling application.
369
+
220
370
  ### Checking the SDK version
221
371
 
222
372
  ```ruby
@@ -250,25 +400,122 @@ rescue C2PA::Error => e
250
400
  end
251
401
  ```
252
402
 
403
+ ## Configuring trust
404
+
405
+ c2pa-rs checks the signing certificate against a trust list, and reports
406
+ `Trusted` when it chains to a root that list contains. That happens by default,
407
+ so a certificate from a CA in the C2PA trust list needs no configuration.
408
+
409
+ For a private or enterprise CA, add its root:
410
+
411
+ ```ruby
412
+ C2PA.configure do |config|
413
+ config.trust_anchors = "ca/root.pem" # a path, or the PEM text itself
414
+ end
415
+ ```
416
+
417
+ Files signed by a certificate chaining to it then validate as `Trusted` rather
418
+ than carrying `signingCredential.untrusted`.
419
+
420
+ ### Offline and air-gapped environments
421
+
422
+ Reading an asset may fetch a remote manifest over the network, and revocation
423
+ checking may contact an OCSP responder. Both can be turned off:
424
+
425
+ ```ruby
426
+ C2PA.configure do |config|
427
+ config.remote_manifest_fetch = false
428
+ config.ocsp_fetch = false
429
+ end
430
+ ```
431
+
432
+ ### Everything configurable
433
+
434
+ | Setting | Default | Purpose |
435
+ |---------|---------|---------|
436
+ | `trust_anchors` | none | additional roots to trust, as PEM |
437
+ | `trust_list` | C2PA list | replaces the trust list rather than adding to it |
438
+ | `allowed_certificates` | none | explicitly allowed certificates, as PEM |
439
+ | `verify_trust` | `true` | whether trust is checked at all |
440
+ | `remote_manifest_fetch` | `true` | whether reading may fetch over the network |
441
+ | `ocsp_fetch` | `false` | whether revocation is checked over OCSP |
442
+
443
+ Settings are global and apply to subsequent calls. Only values you set are
444
+ sent, so anything left alone keeps c2pa-rs's own default. `C2PA.configure` with
445
+ no block resets everything.
446
+
447
+ Turning `verify_trust` off means nothing is ever reported as untrusted, which
448
+ in a library for establishing provenance is rarely what you want. It exists for
449
+ environments that cannot reach a trust list at all.
450
+
253
451
  ## Supported file formats
254
452
 
255
- Signing and reading are supported for any format supported by c2pa-rs, including:
453
+ Each format below has a fixture and a signing test in the suite: the file is
454
+ signed, read back, and asserted to validate.
256
455
 
257
- | Format | MIME type |
258
- |--------|-----------|
259
- | JPEG | `image/jpeg` |
456
+ | Format | MIME type | |
457
+ |--------|-----------|--|
458
+ | JPEG | `image/jpeg` | |
260
459
  | PNG | `image/png` |
261
460
  | WebP | `image/webp` |
262
461
  | TIFF | `image/tiff` |
263
462
  | AVIF | `image/avif` |
264
- | MP4 / M4V | `video/mp4` |
463
+ | JPEG XL | `image/jxl` |
464
+ | MP4 | `video/mp4` |
265
465
  | MOV | `video/quicktime` |
266
466
  | MP3 | `audio/mpeg` |
267
467
  | WAV | `audio/wav` |
268
- | PDF | `application/pdf` |
468
+ | PDF | `application/pdf` | read only, see below |
269
469
 
270
470
  The format is detected automatically from the file extension.
271
471
 
472
+ JPEG XL must be in the ISOBMFF container form. A bare codestream has no boxes
473
+ to hold a manifest, and c2pa-rs rejects it.
474
+
475
+ ### PDF is read-only
476
+
477
+ `C2PA.read` works on a PDF that carries content credentials, such as one signed
478
+ by Adobe Acrobat. `C2PA.sign` does not: c2pa-rs has no PDF writer at any
479
+ version. `get_writer` returns `None` and `save_cai_store` returns
480
+ `NotImplemented`, and upstream closed the request to expose one in December
481
+ 2025 (contentauth/c2pa-rs#527). Signing a PDF raises `C2PA::SigningError`
482
+ with `type is unsupported`.
483
+
484
+ Earlier releases of this gem listed PDF as signable. That was never correct.
485
+
486
+ One limit on what the test suite can show. It proves the PDF handler is active,
487
+ by reading a PDF with no credentials and getting `no JUMBF data found` rather
488
+ than `type is unsupported`. It cannot prove that a signed PDF returns its
489
+ manifest, because nothing available can produce one: c2pa-rs cannot write
490
+ them, and no local tool can either. The code doing the reading is c2pa-rs's
491
+ own and is tested upstream; what is untested here is only this gem's
492
+ integration with the success path.
493
+
494
+ ### Test fixtures
495
+
496
+ The media fixtures are generated by
497
+ [`test/fixtures/generate.sh`](test/fixtures/generate.sh) from ffmpeg's built-in
498
+ sources, so they carry no third-party content and no licence obligations. They
499
+ are deliberately real files rather than placeholders — 160×120 images with
500
+ actual detail, real audio samples, real video frames, and EXIF metadata on the
501
+ JPEG — because C2PA writes into container structures that an empty file would
502
+ not exercise. All ten total 92 KB.
503
+
504
+ Regenerating them needs `ffmpeg`, `cjxl` and `exiftool`; running the tests does
505
+ not.
506
+
507
+ Signing certificates are generated on demand, one chain per key type, so every
508
+ supported algorithm is covered:
509
+
510
+ ```bash
511
+ bundle exec rake fixtures:certs
512
+ ```
513
+
514
+ `rake test` does this for you. The certificates are not committed — they are
515
+ development material, and regenerating costs a fraction of a second. Ruby's
516
+ OpenSSL binding is used rather than the `openssl` command because macOS ships
517
+ LibreSSL, which cannot generate Ed25519 keys.
518
+
272
519
  ## How it works
273
520
 
274
521
  ```
@@ -293,6 +540,13 @@ The Rust extension (`ext/c2pa_native/src/lib.rs`) defines `C2PA::Native` with th
293
540
 
294
541
  Input validation (missing files, invalid manifests) is handled in Ruby before calling into Rust. Errors from the native layer are caught and re-raised as typed `C2PA::Error` subclasses.
295
542
 
543
+ ## Contributing
544
+
545
+ See [CONTRIBUTING.md](CONTRIBUTING.md). The short version: every test must be
546
+ shown to fail before it is merged. The suite that shipped with 0.2.1 passed
547
+ while the gem crashed the Ruby process on TIFF input, so a green run is only
548
+ worth what its assertions can catch.
549
+
296
550
  ## License
297
551
 
298
552
  MIT