ruby-c2pa 0.3.0 → 0.5.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: 98842ac83d8c5e7dacdd28d148cc0e0a0997d2b8ad20205e2d4275238992b813
4
- data.tar.gz: 0dd78073b9b631c901658564b3ce5dc8b80e5e4edaaae6e7a9966df760b0b1c5
3
+ metadata.gz: 41362e47552927fd69a0bec7203dc35f59442af7eeaeb1d5f787216e08c18b79
4
+ data.tar.gz: 6e93a632b24a7baddf4ba4792d8a9f8043559fd5c168df1a685b8094a73f9689
5
5
  SHA512:
6
- metadata.gz: 05eb4f8d6c31fb6fc6af2facb25c1c3a335fc505b97b46d00c9a846fbc6b72cd5dd0048805fd985f24a615f6a4dc8dde991a089e7d3423256fff63a3cb5566c3
7
- data.tar.gz: ca81e533198e62ffe4ae92ef7302653191cf3b5dfb14ae1088b878a97c44b5281073b68db2f04d9ab04dc11662e59c7300db7957db99836e677b0774c9f4baad
6
+ metadata.gz: b9d9de8da30c033cc5e64ea2d27609ce8be1a9b3d08c400c98f4a092c9ad85f27a985f347477ecd3b019366cf75f9c9c65d17a40f94ce1f82476b753b5dc4a85
7
+ data.tar.gz: d888086e87c7eec8fd09d5f8fadb4f6e5f9493d3ecc7199264c10c38bfc254aead1e380d881bbc3b9ad3c7569e30220f57764e85bebe1f98bd0f1a8b816c4e54
data/CHANGELOG.md CHANGED
@@ -7,6 +7,90 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.5.0] — 2026-09-12
11
+
12
+ Adds capability on top of 0.4.0. Nothing is removed and no existing call
13
+ changes behaviour, so this is a minor release. The one change every caller
14
+ will feel is that native calls no longer hold the GVL, which only makes other
15
+ threads faster.
16
+
17
+ ### Added
18
+
19
+ - Buffer signing. `C2PA.sign_buffer(data:, format:, ...)` signs bytes held in
20
+ memory and returns the signed bytes; `C2PA.read_buffer(data:, format: nil)`
21
+ reads them back. Both require a binary string and raise `ArgumentError` for
22
+ any other encoding rather than transcoding the asset. The verify-after-sign
23
+ guard applies to buffers as it does to files. Memory use is about four
24
+ times the asset.
25
+ - The native calls (`sign_file`, `sign_buffer`, `read_file`, `read_buffer`)
26
+ release Ruby's global VM lock while c2pa-rs runs. Other Ruby threads keep
27
+ running during a sign; previously they were blocked until it returned.
28
+ Measured alongside a busy Ruby thread, the process now uses about 1.9
29
+ CPU-seconds per wall-second against 1.0 before. `Thread#kill` and
30
+ `Timeout` take effect when the native call returns, not during it.
31
+ - Thumbnails. `C2PA.configure` gains `thumbnails`, `thumbnail_size`,
32
+ `thumbnail_format` and `thumbnail_quality`. When enabled, a thumbnail of the
33
+ asset is embedded in its manifest, and of each ingredient supplied as a file.
34
+ Off by default: c2pa-rs upscales to its long-edge setting, so at its default
35
+ of 1024 a 160×120 image gets a 1024×768 thumbnail ten times its own size.
36
+ Produced for JPEG, PNG, WebP and TIFF; other formats sign without one.
37
+
38
+ ### Changed
39
+
40
+ - rb-sys is now a direct dependency of the native extension, for
41
+ `rb_thread_call_without_gvl`, which magnus does not wrap. It resolves to the
42
+ same copy magnus already uses.
43
+ - The native extension is built with c2pa-rs's `add_thumbnails` feature, which
44
+ adds the `image` crate: 16 more crates, about 17 seconds on a cold compile,
45
+ and 1.5 MB on the compiled extension.
46
+
47
+ ## [0.4.0] — 2026-09-11
48
+
49
+ Adds capability on top of 0.3.0. Nothing is removed and no existing call
50
+ changes behaviour, so this is a minor release.
51
+
52
+ ### Added
53
+
54
+ - Provenance chaining through ingredients. `add_ingredient` takes an optional
55
+ `file:`. When the file carries content credentials, its manifest is embedded
56
+ in the signed output and the ingredient points at it, so a verifier can follow
57
+ the chain from your asset back through the original. The description-only
58
+ form is unchanged.
59
+ - `intent: :update` on `C2PA::Manifest`, for non-editorial changes such as
60
+ correcting metadata. The source file is the parent.
61
+ - `C2PA.configure`, for trust and verification settings. Add a private CA's
62
+ root as a trust anchor and its certificates validate as `Trusted`; disable
63
+ `remote_manifest_fetch` and `ocsp_fetch` for environments without network
64
+ access. Only values you set are sent, so defaults are c2pa-rs's own.
65
+ - Reading content credentials from PDFs. `C2PA.read` now parses a PDF and
66
+ returns its manifest if one is present. Signing a PDF remains impossible;
67
+ c2pa-rs has no PDF writer and upstream closed the request to add one.
68
+ - `C2PA::InvalidSettingsError`, raised by `C2PA.configure` for unusable input.
69
+
70
+ ### Changed
71
+
72
+ - c2pa-rs 0.90.15 → 0.90.22. The range includes security fixes: `h2` updated
73
+ for RUSTSEC-2026-0258, `chacha20` moved off a yanked version, hardening of
74
+ BMFF chunk-index handling and CAWG identity bindings. The lockfile ships in
75
+ the gem, so installers get these once they upgrade.
76
+ - The native layer uses c2pa-rs's Context API rather than the deprecated
77
+ `Builder::from_json` and `Reader::from_file`, which read settings from
78
+ thread-local state. One shared Context is reused across calls. Signing from
79
+ several threads concurrently is now tested.
80
+ - CI fails if a deprecated c2pa-rs API reappears.
81
+
82
+ ### Documentation
83
+
84
+ - The certificate section no longer claims certificates must chain to a CA in
85
+ the C2PA trust list, or that self-signed certificates are rejected as such.
86
+ A private CA works; its certificates carry `signingCredential.untrusted`
87
+ until the root is added as an anchor. The certificate profile c2pa-rs
88
+ enforces is listed, including the Authority Key Identifier that
89
+ `openssl x509 -req` omits by default.
90
+ - PDF is listed as read-only, with a note on what the test suite can and
91
+ cannot show: nothing available can produce a C2PA-signed PDF, so reading one
92
+ is untested here, though the code doing it is c2pa-rs's own.
93
+
10
94
  ## [0.3.0] — 2026-08-25
11
95
 
12
96
  Runs on c2pa-rs 0.90, and fixes every defect found while building a test suite
@@ -99,7 +183,9 @@ Tagged retroactively. See the v0.2.1 tag for the defects it shipped with.
99
183
 
100
184
  Tagged retroactively.
101
185
 
102
- [Unreleased]: https://github.com/eddorre/ruby-c2pa/compare/v0.3.0...HEAD
186
+ [Unreleased]: https://github.com/eddorre/ruby-c2pa/compare/v0.5.0...HEAD
187
+ [0.5.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.4.0...v0.5.0
188
+ [0.4.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.3.0...v0.4.0
103
189
  [0.3.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.2.1...v0.3.0
104
190
  [0.2.1]: https://github.com/eddorre/ruby-c2pa/compare/v0.2.0...v0.2.1
105
191
  [0.2.0]: https://github.com/eddorre/ruby-c2pa/releases/tag/v0.2.0
data/CONTRIBUTING.md CHANGED
@@ -57,9 +57,15 @@ bundle exec ruby -Ilib -Itest test/c2pa_test.rb
57
57
  git checkout -- lib/c2pa/manifest.rb
58
58
  ```
59
59
 
60
+ `git checkout --` is only safe if the file has no uncommitted changes of its
61
+ own. Mid-change it reverts your work along with the mutation, and the suite
62
+ then fails for a reason that has nothing to do with the test. Copy the file
63
+ aside before mutating and restore from the copy, and run the unmutated suite
64
+ afterwards as a control. That control run is what catches a bad restore.
65
+
60
66
  Then state it in the pull request:
61
67
 
62
- > Verified by reversing the title in `Manifest#to_json`: 19 failures, against 0
68
+ > Verified by reversing the title in `Manifest#to_json`: 21 failures, against 0
63
69
  > for the unmutated suite.
64
70
 
65
71
  A broad mutation like that trips many tests, which is fine. A narrow one that
@@ -69,7 +75,7 @@ test is specific as well as present.
69
75
  Mutating the Rust in `ext/c2pa_native/` counts double — it proves the test
70
76
  reaches through the FFI boundary rather than stopping at Ruby.
71
77
 
72
- ### Two traps, both hit while building this suite
78
+ ### Three traps, all hit while building this suite
73
79
 
74
80
  **A mutation that does not do what you think.** A NUL-handling test appeared to
75
81
  survive a mutation that stripped NUL bytes, which would have meant the test was
@@ -83,6 +89,17 @@ verdict rather than on our own code, mutate in both directions. A harness made
83
89
  to report no failures and a harness made to report spurious ones fail different
84
90
  tests; checking only one leaves the other half unverified.
85
91
 
92
+ **A probe that measures the wrong thing.** The first two designs for the
93
+ GVL tests looked at wall-clock effects: how long a Ruby thread stalled during
94
+ a native call, and how much progress it made. Both reported the lock released
95
+ when it was held, because `C2PA.sign`'s own `File.exist?` checks release the
96
+ lock (stat does) and the wait to get it back swamped everything else. A
97
+ `sample` of the process showed the main thread spending three quarters of its
98
+ time waiting on the lock after a stat, not inside the native call. The test
99
+ that works measures CPU parallelism, which waiting cannot inflate. When a
100
+ timing test passes under the mutation it was written to catch, suspect the
101
+ probe before the code.
102
+
86
103
  ## Where there is no oracle
87
104
 
88
105
  Most assertions can be checked against c2pa-rs, by signing a file and reading
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
 
@@ -212,16 +234,42 @@ manifest.add_assertion(
212
234
 
213
235
  ### Adding ingredients
214
236
 
215
- 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:
216
239
 
217
240
  ```ruby
218
241
  manifest.add_ingredient(
219
242
  title: "Original photo",
220
243
  format: "image/jpeg",
221
- instance_id: "xmp:iid:original-uuid-here"
244
+ instance_id: "xmp:iid:original-uuid-here",
245
+ relationship: "componentOf",
246
+ file: "original.jpg"
222
247
  )
223
248
  ```
224
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
+
225
273
  ### Signing a file
226
274
 
227
275
  The `output` path must not already exist — `C2PA.sign` will raise a `C2PA::SigningError` if the file is already there.
@@ -275,6 +323,46 @@ C2PA.sign(
275
323
  )
276
324
  ```
277
325
 
326
+ ### Signing bytes in memory
327
+
328
+ For data that never touches the filesystem, such as an upload held in a
329
+ request body or an image your application generated, `C2PA.sign_buffer` takes
330
+ the bytes and returns the signed bytes. The format must be given, since there
331
+ is no filename to infer it from.
332
+
333
+ ```ruby
334
+ signed = C2PA.sign_buffer(
335
+ data: request.body.read,
336
+ format: "image/jpeg",
337
+ certificate: "cert.pem",
338
+ key: "key.pem",
339
+ manifest: manifest
340
+ )
341
+ ```
342
+
343
+ The input must be a binary string (`Encoding::BINARY`, which is what
344
+ `File.binread` and `IO#read` on a binary-mode stream return). A string tagged
345
+ UTF-8 is rejected with an `ArgumentError` rather than transcoded, because a
346
+ transcoded JPEG is a corrupt JPEG and nothing notices until a verifier rejects
347
+ it. If you have such a string and know the bytes are intact, call `.b` on it.
348
+
349
+ The same verify-after-sign guard applies. A result that does not validate is
350
+ never returned; `C2PA::SigningError` is raised instead, and `verify: false`
351
+ returns it anyway. `algorithm:` and everything on the manifest, including
352
+ intents and ingredient files, work as they do for `C2PA.sign`.
353
+
354
+ Memory is the trade-off. The input, the copy c2pa-rs works on, and the signed
355
+ result on both sides of the Ruby boundary are resident at once at the peak,
356
+ so budget about four times the size of the asset per call. For a photo that
357
+ is nothing; for a feature-length video it is a reason to use `C2PA.sign` with
358
+ paths instead.
359
+
360
+ Signing and reading release Ruby's global VM lock while c2pa-rs works, so
361
+ other threads in the process keep running. A threaded server signing a large
362
+ video does not stall its other requests for the duration. One consequence:
363
+ `Thread#kill` and `Timeout` cannot interrupt a native call in progress; they
364
+ take effect when it returns.
365
+
278
366
  ### Reading a manifest
279
367
 
280
368
  ```ruby
@@ -285,6 +373,15 @@ puts active["title"]
285
373
  puts active["claim_generator_info"].first["name"] # => "ruby-c2pa"
286
374
  ```
287
375
 
376
+ From memory, `C2PA.read_buffer` takes the bytes. c2pa-rs identifies most
377
+ formats from the leading bytes, so the format is optional; it is needed for a
378
+ format with no signature to sniff, such as SVG.
379
+
380
+ ```ruby
381
+ result = C2PA.read_buffer(data: signed)
382
+ result = C2PA.read_buffer(data: svg_bytes, format: "image/svg+xml")
383
+ ```
384
+
288
385
  ### Naming your application
289
386
 
290
387
  Signed files credit `ruby-c2pa` by default. To credit your own application
@@ -307,8 +404,8 @@ The signed manifest then reads:
307
404
  {
308
405
  "name": "Acme Editor",
309
406
  "version": "2.0",
310
- "org.rubygems.ruby_c2pa": "0.3.0",
311
- "org.contentauth.c2pa_rs": "0.78.8"
407
+ "org.contentauth.c2pa_rs": "0.90.22",
408
+ "org.rubygems.ruby_c2pa": "0.5.0"
312
409
  }
313
410
  ```
314
411
 
@@ -322,12 +419,15 @@ calling application.
322
419
  ### Checking the SDK version
323
420
 
324
421
  ```ruby
325
- puts C2PA.sdk_version # => "0.78.3" (depends on the c2pa-rs version bundled with the gem)
422
+ puts C2PA.sdk_version # => "0.90.22" (the c2pa-rs version the gem was built against)
326
423
  ```
327
424
 
328
425
  ### Error handling
329
426
 
330
- All errors inherit from `C2PA::Error`, so you can rescue broadly or narrowly:
427
+ All errors inherit from `C2PA::Error`, so you can rescue broadly or narrowly.
428
+ `SigningError` covers signing and the post-signing verification, `ReadError`
429
+ reading, `InvalidManifestError` anything the builder rejects, and
430
+ `InvalidSettingsError` anything `C2PA.configure` cannot use.
331
431
 
332
432
  ```ruby
333
433
  begin
@@ -344,6 +444,12 @@ rescue C2PA::ReadError => e
344
444
  puts "Could not read manifest: #{e.message}"
345
445
  end
346
446
 
447
+ begin
448
+ C2PA.configure { |config| config.trust_anchors = "ca/root.pem" }
449
+ rescue C2PA::InvalidSettingsError => e
450
+ puts "Settings not usable: #{e.message}"
451
+ end
452
+
347
453
  # Or rescue any C2PA error broadly
348
454
  begin
349
455
  C2PA.sign(file: "photo.jpg", output: "photo_signed.jpg", certificate: "cert.pem", key: "key.pem", manifest: manifest)
@@ -352,14 +458,89 @@ rescue C2PA::Error => e
352
458
  end
353
459
  ```
354
460
 
461
+ ## Configuring trust
462
+
463
+ c2pa-rs checks the signing certificate against a trust list, and reports
464
+ `Trusted` when it chains to a root that list contains. That happens by default,
465
+ so a certificate from a CA in the C2PA trust list needs no configuration.
466
+
467
+ For a private or enterprise CA, add its root:
468
+
469
+ ```ruby
470
+ C2PA.configure do |config|
471
+ config.trust_anchors = "ca/root.pem" # a path, or the PEM text itself
472
+ end
473
+ ```
474
+
475
+ Files signed by a certificate chaining to it then validate as `Trusted` rather
476
+ than carrying `signingCredential.untrusted`.
477
+
478
+ ### Offline and air-gapped environments
479
+
480
+ Reading an asset may fetch a remote manifest over the network, and revocation
481
+ checking may contact an OCSP responder. Both can be turned off:
482
+
483
+ ```ruby
484
+ C2PA.configure do |config|
485
+ config.remote_manifest_fetch = false
486
+ config.ocsp_fetch = false
487
+ end
488
+ ```
489
+
490
+ ### Thumbnails
491
+
492
+ c2pa-rs can embed a thumbnail of the asset in its manifest, and of each
493
+ ingredient supplied as a file. Verify tools show it alongside the credentials.
494
+ It is off unless you turn it on:
495
+
496
+ ```ruby
497
+ C2PA.configure do |config|
498
+ config.thumbnails = true
499
+ config.thumbnail_size = 512 # longest edge in pixels
500
+ end
501
+ ```
502
+
503
+ The default is off because c2pa-rs scales to a fixed long edge and upscales to
504
+ reach it. Its own default is 1024, so a 160×120 image gets a 1024×768
505
+ thumbnail, roughly ten times the size of the asset it describes. Set
506
+ `thumbnail_size` no larger than your assets, or leave thumbnails off for small
507
+ images.
508
+
509
+ Thumbnails are produced for JPEG, PNG, WebP and TIFF. Other formats sign
510
+ without one; c2pa-rs treats that as non-fatal.
511
+
512
+ ### Everything configurable
513
+
514
+ | Setting | Default | Purpose |
515
+ |---------|---------|---------|
516
+ | `trust_anchors` | none | additional roots to trust, as PEM |
517
+ | `trust_list` | C2PA list | replaces the trust list rather than adding to it |
518
+ | `allowed_certificates` | none | explicitly allowed certificates, as PEM |
519
+ | `verify_trust` | `true` | whether trust is checked at all |
520
+ | `remote_manifest_fetch` | `true` | whether reading may fetch over the network |
521
+ | `ocsp_fetch` | `false` | whether revocation is checked over OCSP |
522
+ | `thumbnails` | `false` | embed a thumbnail of the asset and of file-backed ingredients |
523
+ | `thumbnail_size` | 1024 | longest edge of the thumbnail, in pixels |
524
+ | `thumbnail_format` | smallest | `:jpeg`, `:png` or `:gif` |
525
+ | `thumbnail_quality` | `:medium` | `:low`, `:medium` or `:high` |
526
+
527
+ Settings are global and apply to subsequent calls. Only values you set are
528
+ sent, so anything left alone keeps c2pa-rs's own default, with one exception:
529
+ `thumbnails` is always sent, because this gem's default differs from c2pa-rs's.
530
+ `C2PA.configure` with no block resets everything.
531
+
532
+ Turning `verify_trust` off means nothing is ever reported as untrusted, which
533
+ in a library for establishing provenance is rarely what you want. It exists for
534
+ environments that cannot reach a trust list at all.
535
+
355
536
  ## Supported file formats
356
537
 
357
538
  Each format below has a fixture and a signing test in the suite: the file is
358
539
  signed, read back, and asserted to validate.
359
540
 
360
- | Format | MIME type |
361
- |--------|-----------|
362
- | JPEG | `image/jpeg` |
541
+ | Format | MIME type | |
542
+ |--------|-----------|--|
543
+ | JPEG | `image/jpeg` | |
363
544
  | PNG | `image/png` |
364
545
  | WebP | `image/webp` |
365
546
  | TIFF | `image/tiff` |
@@ -369,19 +550,31 @@ signed, read back, and asserted to validate.
369
550
  | MOV | `video/quicktime` |
370
551
  | MP3 | `audio/mpeg` |
371
552
  | WAV | `audio/wav` |
553
+ | PDF | `application/pdf` | read only, see below |
372
554
 
373
555
  The format is detected automatically from the file extension.
374
556
 
375
557
  JPEG XL must be in the ISOBMFF container form. A bare codestream has no boxes
376
558
  to hold a manifest, and c2pa-rs rejects it.
377
559
 
378
- ### Not supported
560
+ ### PDF is read-only
561
+
562
+ `C2PA.read` works on a PDF that carries content credentials, such as one signed
563
+ by Adobe Acrobat. `C2PA.sign` does not: c2pa-rs has no PDF writer at any
564
+ version. `get_writer` returns `None` and `save_cai_store` returns
565
+ `NotImplemented`, and upstream closed the request to expose one in December
566
+ 2025 (contentauth/c2pa-rs#527). Signing a PDF raises `C2PA::SigningError`
567
+ with `type is unsupported`.
379
568
 
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.
569
+ Earlier releases of this gem listed PDF as signable. That was never correct.
570
+
571
+ One limit on what the test suite can show. It proves the PDF handler is active,
572
+ by reading a PDF with no credentials and getting `no JUMBF data found` rather
573
+ than `type is unsupported`. It cannot prove that a signed PDF returns its
574
+ manifest, because nothing available can produce one: c2pa-rs cannot write
575
+ them, and no local tool can either. The code doing the reading is c2pa-rs's
576
+ own and is tested upstream; what is untested here is only this gem's
577
+ integration with the success path.
385
578
 
386
579
  ### Test fixtures
387
580
 
@@ -417,20 +610,35 @@ Ruby (C2PA.sign)
417
610
 
418
611
  Rust (C2PA::Native.sign_file)
419
612
 
420
- calls c2pa-rs Builder API
613
+ │ c2pa-rs Builder, through a shared Context
421
614
 
422
615
  c2pa-rs — embeds signed manifest into the file
423
616
  ```
424
617
 
425
- The Rust extension (`ext/c2pa_native/src/lib.rs`) defines `C2PA::Native` with three methods:
618
+ The Rust extension (`ext/c2pa_native/src/lib.rs`) defines `C2PA::Native` with six methods:
426
619
 
427
620
  | Method | Description |
428
621
  |--------|-------------|
429
- | `C2PA::Native.sign_file` | Sign a file and write the result |
622
+ | `C2PA::Native.sign_file` | Sign a file and write the result. Takes the manifest JSON, an optional intent, and any ingredient files |
623
+ | `C2PA::Native.sign_buffer` | The same over bytes: a binary string in, the signed binary string out |
430
624
  | `C2PA::Native.read_file` | Read and return the manifest JSON |
625
+ | `C2PA::Native.read_buffer` | The same over bytes, with an optional format hint |
626
+ | `C2PA::Native.configure` | Replace the shared c2pa-rs Context with one built from a settings document |
431
627
  | `C2PA::Native.sdk_version` | Return the c2pa-rs version string |
432
628
 
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.
629
+ All four run with the global VM lock released, so other Ruby threads are not
630
+ blocked while c2pa-rs hashes and signs. The bytes and paths are copied out of
631
+ Ruby before the lock goes, and the result is turned into a Ruby object after
632
+ it is back; nothing in between touches the interpreter.
633
+
634
+ Signing and reading go through one c2pa-rs `Context`, built once and shared
635
+ across threads. `C2PA.configure` replaces it rather than mutating it, so a
636
+ signing call already in flight keeps the settings it started with.
637
+
638
+ Input validation (missing files, invalid manifests, unreadable ingredient
639
+ files, unusable settings) is handled in Ruby before calling into Rust. Errors
640
+ from the native layer are caught and re-raised as typed `C2PA::Error`
641
+ subclasses.
434
642
 
435
643
  ## Contributing
436
644