ruby-c2pa 0.2.1 → 0.3.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: 98842ac83d8c5e7dacdd28d148cc0e0a0997d2b8ad20205e2d4275238992b813
4
+ data.tar.gz: 0dd78073b9b631c901658564b3ce5dc8b80e5e4edaaae6e7a9966df760b0b1c5
5
5
  SHA512:
6
- metadata.gz: 144409841d84d8a311ffb785e60a5f59a0616d420df89c0daf03da4c0f772e005e6a4bc93da375fcfa5a078856bb2182f133be9d4a79319850f62c7324474343
7
- data.tar.gz: 0c8b604f64fd03404bbb031478047859a965f40ed1328bd061d64ae68acef999d0ad351f3f365167c4bd36d96888a772b26056d1daff94772992f5d4d79dd13b
6
+ metadata.gz: 05eb4f8d6c31fb6fc6af2facb25c1c3a335fc505b97b46d00c9a846fbc6b72cd5dd0048805fd985f24a615f6a4dc8dde991a089e7d3423256fff63a3cb5566c3
7
+ data.tar.gz: ca81e533198e62ffe4ae92ef7302653191cf3b5dfb14ae1088b878a97c44b5281073b68db2f04d9ab04dc11662e59c7300db7957db99836e677b0774c9f4baad
data/CHANGELOG.md ADDED
@@ -0,0 +1,105 @@
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.3.0] — 2026-08-25
11
+
12
+ Runs on c2pa-rs 0.90, and fixes every defect found while building a test suite
13
+ that verifies against c2pa-rs rather than against itself.
14
+
15
+ ### If you have signed files with an earlier version
16
+
17
+ **Re-sign them.** Assets signed by 0.2.1 and earlier are rejected by current
18
+ verifiers, because their `c2pa.created` action carries no `digitalSourceType`.
19
+ c2pa-rs began requiring one in 0.90, and validation happens when a file is
20
+ read, not when it is written — so files that were valid when signed became
21
+ invalid as verifiers updated around them.
22
+
23
+ Anything signed using the README's editing example never validated at all. See
24
+ `c2pa.opened` under Fixed.
25
+
26
+ ### Breaking
27
+
28
+ - `c2pa.created` now requires a `digital_source_type`. c2pa-rs accepts any
29
+ string, so a default would validate while asserting something untrue about an
30
+ asset's origin — a camera original and generative AI output are not
31
+ interchangeable claims. Use `C2PA::DigitalSourceTypes::UNSPECIFIED` when the
32
+ origin is genuinely unknown.
33
+ - `C2PA.sign` now reads the signed file back and raises `C2PA::SigningError` if
34
+ it does not validate, deleting the output. Pass `verify: false` to keep it.
35
+ Code that previously produced invalid files now fails instead of succeeding
36
+ quietly.
37
+ - `add_action(C2PA::Actions::OPENED)` now raises. The action must reference its
38
+ parent ingredient by hashed URI, which cannot be built from Ruby. Pass
39
+ `intent: :edit` to `C2PA::Manifest.new` instead.
40
+ - `c2pa.translated` now requires `sourceLanguage` and `targetLanguage`
41
+ parameters, as RFC 5646 codes.
42
+ - Invalid UTF-8 in a manifest now raises `C2PA::InvalidManifestError` rather
43
+ than `JSON::GeneratorError`, so `rescue C2PA::Error` catches it as the README
44
+ has always claimed.
45
+
46
+ ### Fixed
47
+
48
+ - **Signing a TIFF aborted the Ruby process.** An invalid free in `atree`
49
+ 0.5.3, pulled transitively by c2pa-rs 0.78.3. No exception was raised, so it
50
+ could not be rescued; in a web process it took down the worker. Fixed
51
+ upstream in c2pa-rs 0.78.4, one day before 0.2.1 was published.
52
+ - **`c2pa.opened` produced files that never validated.** The README documented
53
+ adding the action directly, which cannot work. Editing is now supported
54
+ through `intent: :edit`, which lets c2pa-rs derive the parent ingredient from
55
+ the source and wire the action to it.
56
+ - **Signed files credited `c2pa-rs` as the claim generator.** They now credit
57
+ `ruby-c2pa`, and an application can name itself with `generator_name:`.
58
+ - **The gemspec homepage pointed at a repository that does not exist**, so the
59
+ link from RubyGems returned 404.
60
+ - **The gem shipped 1.3 MB of Rust build artifacts**, and its contents varied
61
+ with whatever had been compiled on the machine that built it.
62
+ - **`Cargo.lock` was never packaged.** The extension compiles at install time,
63
+ so every installer resolved dependencies afresh — which is how 0.2.1 shipped
64
+ against the broken `atree` even though the repository pinned it.
65
+ - **PDF was advertised as signable.** c2pa-rs has no PDF writer at any version;
66
+ signing one raises, and the documentation now says so.
67
+
68
+ ### Added
69
+
70
+ - `C2PA::DigitalSourceTypes` — the IPTC vocabulary, plus `UNSPECIFIED` for
71
+ declining to claim an origin rather than guessing.
72
+ - `intent: :edit` on `C2PA::Manifest`, for signing an asset derived from
73
+ another one.
74
+ - `generator_name:` and `generator_version:`, for naming your application as
75
+ the claim generator.
76
+ - `verify:` on `C2PA.sign`, defaulting to `true`.
77
+ - Continuous integration on Linux and macOS. macOS is not redundant: the
78
+ invalid free behind the TIFF crash was surfaced by macOS libmalloc, where
79
+ glibc may corrupt the heap silently.
80
+ - `CONTRIBUTING.md`, recording the rule that every test must be shown to fail
81
+ before it is merged.
82
+
83
+ ### Changed
84
+
85
+ - c2pa-rs 0.78.3 → 0.90.15.
86
+ - Verified format support: JPEG, PNG, WebP, TIFF, AVIF, JPEG XL, MP4, MOV, MP3,
87
+ WAV. Each has a fixture and a signing test. MOV, MP3 and JPEG XL were
88
+ previously undocumented; PDF was documented and never worked.
89
+ - All seven signing algorithms are exercised — es256, es384, es512, ps256,
90
+ ps384, ps512, ed25519 — each against a key of the matching type.
91
+ - The test suite went from 10 tests that compared the code to itself to 85 that
92
+ sign real files and assert on what c2pa-rs reads back.
93
+
94
+ ## [0.2.1] — 2026-03-17
95
+
96
+ Tagged retroactively. See the v0.2.1 tag for the defects it shipped with.
97
+
98
+ ## [0.2.0] — 2026-03-17
99
+
100
+ Tagged retroactively.
101
+
102
+ [Unreleased]: https://github.com/eddorre/ruby-c2pa/compare/v0.3.0...HEAD
103
+ [0.3.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.2.1...v0.3.0
104
+ [0.2.1]: https://github.com/eddorre/ruby-c2pa/compare/v0.2.0...v0.2.1
105
+ [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
@@ -93,18 +93,63 @@ Every signed file requires a `C2PA::Manifest` with at least one action. Actions
93
93
  require "c2pa"
94
94
 
95
95
  manifest = C2PA::Manifest.new(title: "Sunset over the bay")
96
- manifest.add_action(C2PA::Actions::CREATED)
96
+ manifest.add_action(
97
+ C2PA::Actions::CREATED,
98
+ digital_source_type: C2PA::DigitalSourceTypes::DIGITAL_CAPTURE
99
+ )
97
100
  ```
98
101
 
102
+ `c2pa.created` must declare how the asset came into being. c2pa-rs rejects a
103
+ manifest without it, and accepts any string you supply — so a wrong value
104
+ validates while asserting something untrue. The gem therefore requires you to
105
+ choose rather than defaulting on your behalf:
106
+
107
+ | Constant | Use for |
108
+ |----------|---------|
109
+ | `DIGITAL_CAPTURE` | a camera original |
110
+ | `TRAINED_ALGORITHMIC_MEDIA` | generative AI output |
111
+ | `COMPOSITE_WITH_TRAINED_ALGORITHMIC_MEDIA` | edited using generative AI |
112
+ | `SCREEN_CAPTURE` | a screenshot |
113
+ | `HUMAN_EDITS` | human-edited media |
114
+ | `UNSPECIFIED` | the origin is genuinely unknown |
115
+
116
+ `C2PA::DigitalSourceTypes::ALL` lists them all. Reach for `UNSPECIFIED` when you
117
+ do not know — it is what c2pa-rs uses in its own fixtures, and it is honest in a
118
+ way that guessing is not.
119
+
120
+ This requirement arrived in c2pa-rs 0.90. Manifests signed by releases before
121
+ 0.3.0 omit the field and are rejected by current verifiers.
122
+
99
123
  Actions can be chained:
100
124
 
101
125
  ```ruby
102
- manifest = C2PA::Manifest.new(title: "Edited photo")
103
- .add_action(C2PA::Actions::OPENED)
126
+ manifest = C2PA::Manifest.new(title: "Sunset over the bay")
127
+ .add_action(C2PA::Actions::CREATED,
128
+ digital_source_type: C2PA::DigitalSourceTypes::DIGITAL_CAPTURE)
129
+ .add_action(C2PA::Actions::PUBLISHED)
130
+ ```
131
+
132
+ ### Editing an existing asset
133
+
134
+ When the file you are signing derives from another one, declare the intent
135
+ rather than adding `c2pa.opened` yourself:
136
+
137
+ ```ruby
138
+ manifest = C2PA::Manifest.new(title: "Edited photo", intent: :edit)
104
139
  .add_action(C2PA::Actions::EDITED)
105
140
  .add_action(C2PA::Actions::PUBLISHED)
106
141
  ```
107
142
 
143
+ c2pa-rs derives the parent ingredient from the source file and adds a
144
+ `c2pa.opened` action tied to it, so the signed manifest records
145
+ `c2pa.opened`, `c2pa.edited`, `c2pa.published` and a `parentOf` ingredient.
146
+
147
+ `c2pa.opened` cannot be added by hand. The specification requires it to
148
+ reference its parent ingredient by hashed URI, and that hash is computed over
149
+ the ingredient as c2pa-rs serialises it — so `add_action(C2PA::Actions::OPENED)`
150
+ raises and points here. Earlier releases of this gem documented adding it
151
+ directly; manifests built that way never validated.
152
+
108
153
  Each action accepts optional fields from the C2PA specification:
109
154
 
110
155
  ```ruby
@@ -183,7 +228,8 @@ The `output` path must not already exist — `C2PA.sign` will raise a `C2PA::Sig
183
228
 
184
229
  ```ruby
185
230
  manifest = C2PA::Manifest.new(title: "Sunset over the bay")
186
- .add_action(C2PA::Actions::CREATED)
231
+ .add_action(C2PA::Actions::CREATED,
232
+ digital_source_type: C2PA::DigitalSourceTypes::DIGITAL_CAPTURE)
187
233
 
188
234
  C2PA.sign(
189
235
  file: "photo.jpg",
@@ -194,6 +240,28 @@ C2PA.sign(
194
240
  )
195
241
  ```
196
242
 
243
+ `C2PA.sign` reads the signed file back and confirms it validates before
244
+ returning. If it does not, the output file is deleted and a
245
+ `C2PA::SigningError` is raised naming the failure codes.
246
+
247
+ This matters because c2pa-rs applies its rules when *reading*, not when
248
+ writing. Signing reports success for manifests that every verifier rejects,
249
+ which is exactly what earlier versions of this gem did — silently, for months.
250
+ The check costs one extra read of the output.
251
+
252
+ Turn it off with `verify: false` if you want the file kept for inspection:
253
+
254
+ ```ruby
255
+ C2PA.sign(
256
+ file: "photo.jpg",
257
+ output: "photo_signed.jpg",
258
+ certificate: "cert.pem",
259
+ key: "key.pem",
260
+ manifest: manifest,
261
+ verify: false
262
+ )
263
+ ```
264
+
197
265
  Specify a different signing algorithm with `algorithm:` (default is `"es256"`):
198
266
 
199
267
  ```ruby
@@ -214,9 +282,43 @@ result = C2PA.read(file: "photo_signed.jpg")
214
282
 
215
283
  active = result["manifests"][result["active_manifest"]]
216
284
  puts active["title"]
217
- puts active["claim_generator_info"].first["name"]
285
+ puts active["claim_generator_info"].first["name"] # => "ruby-c2pa"
218
286
  ```
219
287
 
288
+ ### Naming your application
289
+
290
+ Signed files credit `ruby-c2pa` by default. To credit your own application
291
+ instead:
292
+
293
+ ```ruby
294
+ manifest = C2PA::Manifest.new(
295
+ title: "Sunset over the bay",
296
+ generator_name: "Acme Editor",
297
+ generator_version: "2.0"
298
+ ).add_action(
299
+ C2PA::Actions::CREATED,
300
+ digital_source_type: C2PA::DigitalSourceTypes::DIGITAL_CAPTURE
301
+ )
302
+ ```
303
+
304
+ The signed manifest then reads:
305
+
306
+ ```json
307
+ {
308
+ "name": "Acme Editor",
309
+ "version": "2.0",
310
+ "org.rubygems.ruby_c2pa": "0.3.0",
311
+ "org.contentauth.c2pa_rs": "0.78.8"
312
+ }
313
+ ```
314
+
315
+ c2pa-rs permits exactly one claim generator entry, so your application replaces
316
+ the gem as the name rather than preceding it. The gem is recorded in a
317
+ namespaced field alongside it, which is how c2pa-rs records itself.
318
+
319
+ Releases before 0.3.0 credited `c2pa-rs` and named neither the gem nor the
320
+ calling application.
321
+
220
322
  ### Checking the SDK version
221
323
 
222
324
  ```ruby
@@ -252,7 +354,8 @@ end
252
354
 
253
355
  ## Supported file formats
254
356
 
255
- Signing and reading are supported for any format supported by c2pa-rs, including:
357
+ Each format below has a fixture and a signing test in the suite: the file is
358
+ signed, read back, and asserted to validate.
256
359
 
257
360
  | Format | MIME type |
258
361
  |--------|-----------|
@@ -261,14 +364,50 @@ Signing and reading are supported for any format supported by c2pa-rs, including
261
364
  | WebP | `image/webp` |
262
365
  | TIFF | `image/tiff` |
263
366
  | AVIF | `image/avif` |
264
- | MP4 / M4V | `video/mp4` |
367
+ | JPEG XL | `image/jxl` |
368
+ | MP4 | `video/mp4` |
265
369
  | MOV | `video/quicktime` |
266
370
  | MP3 | `audio/mpeg` |
267
371
  | WAV | `audio/wav` |
268
- | PDF | `application/pdf` |
269
372
 
270
373
  The format is detected automatically from the file extension.
271
374
 
375
+ JPEG XL must be in the ISOBMFF container form. A bare codestream has no boxes
376
+ to hold a manifest, and c2pa-rs rejects it.
377
+
378
+ ### Not supported
379
+
380
+ **PDF cannot be signed.** c2pa-rs can read C2PA data out of a PDF but has no
381
+ writer for it — `get_writer` returns `None` and `save_cai_store` returns
382
+ `NotImplemented` — so `C2PA.sign` raises `C2PA::SigningError` with
383
+ `type is unsupported`. This is true at every c2pa-rs version. Earlier releases
384
+ of this gem listed PDF as supported; that was never correct.
385
+
386
+ ### Test fixtures
387
+
388
+ The media fixtures are generated by
389
+ [`test/fixtures/generate.sh`](test/fixtures/generate.sh) from ffmpeg's built-in
390
+ sources, so they carry no third-party content and no licence obligations. They
391
+ are deliberately real files rather than placeholders — 160×120 images with
392
+ actual detail, real audio samples, real video frames, and EXIF metadata on the
393
+ JPEG — because C2PA writes into container structures that an empty file would
394
+ not exercise. All ten total 92 KB.
395
+
396
+ Regenerating them needs `ffmpeg`, `cjxl` and `exiftool`; running the tests does
397
+ not.
398
+
399
+ Signing certificates are generated on demand, one chain per key type, so every
400
+ supported algorithm is covered:
401
+
402
+ ```bash
403
+ bundle exec rake fixtures:certs
404
+ ```
405
+
406
+ `rake test` does this for you. The certificates are not committed — they are
407
+ development material, and regenerating costs a fraction of a second. Ruby's
408
+ OpenSSL binding is used rather than the `openssl` command because macOS ships
409
+ LibreSSL, which cannot generate Ed25519 keys.
410
+
272
411
  ## How it works
273
412
 
274
413
  ```
@@ -293,6 +432,13 @@ The Rust extension (`ext/c2pa_native/src/lib.rs`) defines `C2PA::Native` with th
293
432
 
294
433
  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
434
 
435
+ ## Contributing
436
+
437
+ See [CONTRIBUTING.md](CONTRIBUTING.md). The short version: every test must be
438
+ shown to fail before it is merged. The suite that shipped with 0.2.1 passed
439
+ while the gem crashed the Ruby process on TIFF input, so a green run is only
440
+ worth what its assertions can catch.
441
+
296
442
  ## License
297
443
 
298
444
  MIT
data/Rakefile CHANGED
@@ -14,5 +14,33 @@ Rake::TestTask.new(:test) do |t|
14
14
  t.verbose = true
15
15
  end
16
16
 
17
- task test: :compile
17
+ # The signing tests need an X.509 certificate chain and key per algorithm.
18
+ # These are generated locally rather than downloaded: it needs no network, it
19
+ # covers every algorithm the gem accepts rather than only ES256, and it keeps
20
+ # third-party material out of the repository entirely.
21
+ #
22
+ # The generated chains are rooted in a CA created here, so signed assets carry
23
+ # a signingCredential.untrusted warning. That does not make a manifest invalid.
24
+ #
25
+ # They are written to an ignored directory rather than committed: a PEM private
26
+ # key in a content-authenticity repository reads badly and trips secret
27
+ # scanning, and regenerating them costs a fraction of a second.
28
+ namespace :fixtures do
29
+ desc "Generate the signing certificates used by the test suite"
30
+ task :certs do
31
+ require_relative "test/fixtures/generate_certs"
32
+ CertificateFixtures.generate_all
33
+ end
34
+
35
+ desc "Regenerate the signing certificates, replacing any that exist"
36
+ task :certs_force do
37
+ require_relative "test/fixtures/generate_certs"
38
+ CertificateFixtures.generate_all(force: true)
39
+ end
40
+ end
41
+
42
+ # Tests depend on the certificates, so a checkout can run `rake test` with no
43
+ # manual setup, and a missing certificate fails the run rather than quietly
44
+ # skipping the tests that do the real work.
45
+ task test: [:compile, "fixtures:certs"]
18
46
  task default: :test