ruby-c2pa 0.5.0 → 0.6.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 +4 -4
- data/CHANGELOG.md +55 -1
- data/CONTRIBUTING.md +12 -3
- data/README.md +137 -23
- data/ext/c2pa_native/Cargo.lock +61 -294
- data/ext/c2pa_native/Cargo.toml +6 -1
- data/ext/c2pa_native/src/lib.rs +90 -8
- data/lib/c2pa/config.rb +33 -3
- data/lib/c2pa/manifest.rb +42 -6
- data/lib/c2pa/version.rb +1 -1
- data/lib/c2pa.rb +52 -18
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: fd571866dd99e9811e93072f9e642625f844a418bc7878d437905231b5cda679
|
|
4
|
+
data.tar.gz: 8854aaaf99e78cce57e4ba8f60102cf64e9aeda243c17b1395878dedb83eb904
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3839eadfa75e5d7f3bd2eb22164cec8301195a4fdb7eee7d39cccafdf8abc67fa09b855ece3a43a0bcc5a6e3dddcb408bc1c4f5c73fa0ba5097fd51623384e1
|
|
7
|
+
data.tar.gz: d84bd7979e4a3b14f7039ca3e0f5c3b3cf4b35f663d333cd8d246d52afcbc910602d7a462e5bf5dd22d32b5be3fa603a29f224bc84a848cfd13da46b81bb66cc
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,59 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.6.0] — 2026-09-29
|
|
11
|
+
|
|
12
|
+
Adds capability on top of 0.5.0. Nothing is removed and no existing call
|
|
13
|
+
changes behaviour, so this is a minor release. Building it needs Rust 1.96
|
|
14
|
+
or newer, which c2pa-rs 0.91 requires.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Detached manifests. `C2PA.sign(sidecar:)` writes the manifest to its own
|
|
19
|
+
`.c2pa` file instead of embedding it, and leaves the asset as it was hashed.
|
|
20
|
+
`C2PA.read(manifest_file:)` and `C2PA.read_buffer(manifest_data:)` validate an
|
|
21
|
+
asset against a sidecar. A sidecar with the asset's name and a `.c2pa`
|
|
22
|
+
extension is found without being named. The verify guard checks the pair and
|
|
23
|
+
removes both files if it fails.
|
|
24
|
+
- Manifests stored under the `c2md` JUMBF type are read. The specification
|
|
25
|
+
requires readers to accept them; c2pa-rs 0.91 does, and earlier versions
|
|
26
|
+
skipped them.
|
|
27
|
+
- Signing and reading EPUB, Word (DOCX), OpenDocument text (ODT) and OpenXPS
|
|
28
|
+
files. The manifest goes in `META-INF/content_credential.c2pa`, and every
|
|
29
|
+
other part of the package is covered by the signature. c2pa-rs could only
|
|
30
|
+
read uncompressed ZIP entries, which no real document uses, so the extension
|
|
31
|
+
now enables deflate in its zip dependency. `C2PA.read_buffer` needs
|
|
32
|
+
`format:` for these, since they all start with the same ZIP header.
|
|
33
|
+
- `C2PA::Config#exclude_free_and_skip_boxes`. Set it to false when signing
|
|
34
|
+
MP4, MOV and other BMFF files so that any later change to their `free` and
|
|
35
|
+
`skip` padding boxes breaks the signature. Left unset, c2pa-rs excludes
|
|
36
|
+
them, as it always has.
|
|
37
|
+
- `C2PA::Config#allow_redirects`. Set it to false to stop network fetches
|
|
38
|
+
during reading and validation from following redirects. Left unset, c2pa-rs
|
|
39
|
+
follows redirects but refuses those that point at internal addresses.
|
|
40
|
+
- Signing PDFs. c2pa-rs added a PDF writer in 0.91.1, after years of having
|
|
41
|
+
none, so `C2PA.sign` and `C2PA.sign_buffer` now accept PDFs and the manifest
|
|
42
|
+
goes into the document's associated files. Every release up to 0.5.0
|
|
43
|
+
documented PDF signing as impossible, which it was at the time.
|
|
44
|
+
- `Manifest#add_ingredient` takes `digital_source_type:`, recording how an
|
|
45
|
+
ingredient with no content credentials was produced. Giving one for a file
|
|
46
|
+
that carries credentials raises `C2PA::InvalidManifestError`.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- `Manifest#add_action` raises `C2PA::InvalidManifestError` when
|
|
51
|
+
`parameters` includes `relatedAssertions`. c2pa-rs 0.91 checks these links
|
|
52
|
+
when reading but cannot create them, so the gem has no genuine value to
|
|
53
|
+
send.
|
|
54
|
+
- Built against c2pa-rs 0.91.1, which needs Rust 1.96 or newer to compile.
|
|
55
|
+
0.91.1 also carries a batch of hardening fixes over 0.91.0: a panic on
|
|
56
|
+
oversized XMP when writing JPEG APP1, an underflow in XMP trailer parsing,
|
|
57
|
+
unbounded TIFF IFD entry counts, uncapped ID3v2 frame decompression, and
|
|
58
|
+
corrections to OCSP responder validation. That release validates manifests
|
|
59
|
+
before writing by default; the gem turns that off and keeps its own
|
|
60
|
+
verify-after-sign guard, which reads the finished output back and checks
|
|
61
|
+
its hashes. Errors and the `verify:` option behave as before.
|
|
62
|
+
|
|
10
63
|
## [0.5.0] — 2026-09-12
|
|
11
64
|
|
|
12
65
|
Adds capability on top of 0.4.0. Nothing is removed and no existing call
|
|
@@ -183,7 +236,8 @@ Tagged retroactively. See the v0.2.1 tag for the defects it shipped with.
|
|
|
183
236
|
|
|
184
237
|
Tagged retroactively.
|
|
185
238
|
|
|
186
|
-
[Unreleased]: https://github.com/eddorre/ruby-c2pa/compare/v0.
|
|
239
|
+
[Unreleased]: https://github.com/eddorre/ruby-c2pa/compare/v0.6.0...HEAD
|
|
240
|
+
[0.6.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.5.0...v0.6.0
|
|
187
241
|
[0.5.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.4.0...v0.5.0
|
|
188
242
|
[0.4.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.3.0...v0.4.0
|
|
189
243
|
[0.3.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.2.1...v0.3.0
|
data/CONTRIBUTING.md
CHANGED
|
@@ -14,7 +14,7 @@ Regenerating the media fixtures is the one thing that needs extra tooling, and
|
|
|
14
14
|
only if you are changing them:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
|
-
brew install ffmpeg webp jpeg-xl exiftool # macOS
|
|
17
|
+
brew install ffmpeg webp jpeg-xl exiftool # macOS; zip ships with it
|
|
18
18
|
./test/fixtures/generate.sh
|
|
19
19
|
```
|
|
20
20
|
|
|
@@ -29,7 +29,7 @@ that suite passed while the gem:
|
|
|
29
29
|
|
|
30
30
|
- aborted the Ruby process on any TIFF input
|
|
31
31
|
- emitted manifests that modern verifiers reject
|
|
32
|
-
- advertised PDF signing, which c2pa-rs
|
|
32
|
+
- advertised PDF signing, which no c2pa-rs release could do until 0.91.1
|
|
33
33
|
- documented an editing workflow that has never produced a valid file
|
|
34
34
|
|
|
35
35
|
The suite passed because its assertions compared the code to itself:
|
|
@@ -75,7 +75,7 @@ test is specific as well as present.
|
|
|
75
75
|
Mutating the Rust in `ext/c2pa_native/` counts double — it proves the test
|
|
76
76
|
reaches through the FFI boundary rather than stopping at Ruby.
|
|
77
77
|
|
|
78
|
-
###
|
|
78
|
+
### Four traps, all hit while building this suite
|
|
79
79
|
|
|
80
80
|
**A mutation that does not do what you think.** A NUL-handling test appeared to
|
|
81
81
|
survive a mutation that stripped NUL bytes, which would have meant the test was
|
|
@@ -100,6 +100,15 @@ that works measures CPU parallelism, which waiting cannot inflate. When a
|
|
|
100
100
|
timing test passes under the mutation it was written to catch, suspect the
|
|
101
101
|
probe before the code.
|
|
102
102
|
|
|
103
|
+
**An assertion satisfied by the wrong thing.** A test for "signing a PDF
|
|
104
|
+
leaves it a readable PDF" checked that the output still contained
|
|
105
|
+
`ruby-c2pa`, the text on the fixture's page. It passed under a mutation that
|
|
106
|
+
rewrote the page content before signing, because `ruby-c2pa` also appears in
|
|
107
|
+
the manifest's claim generator: the assertion was reading the manifest, not
|
|
108
|
+
the document. Matching the page's text operator, `(ruby-c2pa) Tj`, fixed it.
|
|
109
|
+
When a string you assert on could come from more than one place in the
|
|
110
|
+
output, match something only the thing under test can produce.
|
|
111
|
+
|
|
103
112
|
## Where there is no oracle
|
|
104
113
|
|
|
105
114
|
Most assertions can be checked against c2pa-rs, by signing a file and reading
|
data/README.md
CHANGED
|
@@ -27,7 +27,9 @@ The binding layer is a native Ruby extension written in Rust using [magnus](http
|
|
|
27
27
|
## Requirements
|
|
28
28
|
|
|
29
29
|
- Ruby >= 3.0
|
|
30
|
-
- Rust and Cargo (to compile the native library)
|
|
30
|
+
- Rust >= 1.96 and Cargo (to compile the native library). c2pa-rs 0.91 sets
|
|
31
|
+
that floor; an older toolchain stops with
|
|
32
|
+
`c2pa@0.91.1 requires rustc 1.96.0` during installation.
|
|
31
33
|
- OpenSSL (usually already present on macOS and Linux)
|
|
32
34
|
|
|
33
35
|
### Installing Rust
|
|
@@ -256,6 +258,23 @@ Omitting `file:` records the description alone. Nothing binds it to any bytes,
|
|
|
256
258
|
so a verifier cannot check the claim. That form is kept for compatibility;
|
|
257
259
|
prefer the file.
|
|
258
260
|
|
|
261
|
+
An ingredient with no credentials can say how it was produced, the same way a
|
|
262
|
+
`c2pa.created` action does for the asset itself:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
manifest.add_ingredient(
|
|
266
|
+
title: "Generated background",
|
|
267
|
+
format: "image/png",
|
|
268
|
+
instance_id: "xmp:iid:background-uuid-here",
|
|
269
|
+
relationship: "componentOf",
|
|
270
|
+
file: "background.png",
|
|
271
|
+
digital_source_type: C2PA::DigitalSourceTypes::TRAINED_ALGORITHMIC_MEDIA
|
|
272
|
+
)
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
An ingredient that carries content credentials already records its own origin,
|
|
276
|
+
so `add_ingredient` raises `C2PA::InvalidManifestError` if you give it one.
|
|
277
|
+
|
|
259
278
|
### Updating an existing asset
|
|
260
279
|
|
|
261
280
|
For a non-editorial change to an asset, such as correcting metadata, use
|
|
@@ -382,6 +401,40 @@ result = C2PA.read_buffer(data: signed)
|
|
|
382
401
|
result = C2PA.read_buffer(data: svg_bytes, format: "image/svg+xml")
|
|
383
402
|
```
|
|
384
403
|
|
|
404
|
+
### Keeping the manifest in a separate file
|
|
405
|
+
|
|
406
|
+
Pass `sidecar:` to write the manifest to its own `.c2pa` file instead of
|
|
407
|
+
embedding it. The output is the asset exactly as it was hashed, which is
|
|
408
|
+
usually byte-for-byte the input. Use this when the file can't be modified, or
|
|
409
|
+
when the manifest is served separately.
|
|
410
|
+
|
|
411
|
+
```ruby
|
|
412
|
+
C2PA.sign(
|
|
413
|
+
file: "photo.jpg",
|
|
414
|
+
output: "published/photo.jpg",
|
|
415
|
+
sidecar: "published/photo.c2pa",
|
|
416
|
+
certificate: "cert.pem",
|
|
417
|
+
key: "key.pem",
|
|
418
|
+
manifest: manifest
|
|
419
|
+
)
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
The asset is only valid together with its sidecar. Changing either one, or
|
|
423
|
+
pairing the sidecar with a different file, makes it invalid.
|
|
424
|
+
|
|
425
|
+
When a file has no embedded manifest, `C2PA.read` looks beside it for the same
|
|
426
|
+
name with a `.c2pa` extension, so the pair above reads with no extra argument.
|
|
427
|
+
For a sidecar kept anywhere else, name it:
|
|
428
|
+
|
|
429
|
+
```ruby
|
|
430
|
+
C2PA.read(file: "photo.jpg", manifest_file: "manifests/1234.c2pa")
|
|
431
|
+
C2PA.read_buffer(data: bytes, format: "image/jpeg", manifest_data: sidecar_bytes)
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
Reading a `.c2pa` file on its own returns the manifest, but it always reports
|
|
435
|
+
`Invalid` with `assertion.dataHash.mismatch` (or the matching BMFF code),
|
|
436
|
+
because there's no asset for the hash to be checked against.
|
|
437
|
+
|
|
385
438
|
### Naming your application
|
|
386
439
|
|
|
387
440
|
Signed files credit `ruby-c2pa` by default. To credit your own application
|
|
@@ -405,7 +458,7 @@ The signed manifest then reads:
|
|
|
405
458
|
"name": "Acme Editor",
|
|
406
459
|
"version": "2.0",
|
|
407
460
|
"org.contentauth.c2pa_rs": "0.90.22",
|
|
408
|
-
"org.rubygems.ruby_c2pa": "0.
|
|
461
|
+
"org.rubygems.ruby_c2pa": "0.6.0"
|
|
409
462
|
}
|
|
410
463
|
```
|
|
411
464
|
|
|
@@ -487,6 +540,18 @@ C2PA.configure do |config|
|
|
|
487
540
|
end
|
|
488
541
|
```
|
|
489
542
|
|
|
543
|
+
When fetches stay on, c2pa-rs follows HTTP redirects but refuses one that
|
|
544
|
+
points at an internal address, such as localhost, a private network or a cloud
|
|
545
|
+
metadata endpoint. The URL can come from the asset being read, so this stops a
|
|
546
|
+
crafted file from reaching your internal services. To refuse redirects
|
|
547
|
+
altogether:
|
|
548
|
+
|
|
549
|
+
```ruby
|
|
550
|
+
C2PA.configure do |config|
|
|
551
|
+
config.allow_redirects = false
|
|
552
|
+
end
|
|
553
|
+
```
|
|
554
|
+
|
|
490
555
|
### Thumbnails
|
|
491
556
|
|
|
492
557
|
c2pa-rs can embed a thumbnail of the asset in its manifest, and of each
|
|
@@ -509,6 +574,22 @@ images.
|
|
|
509
574
|
Thumbnails are produced for JPEG, PNG, WebP and TIFF. Other formats sign
|
|
510
575
|
without one; c2pa-rs treats that as non-fatal.
|
|
511
576
|
|
|
577
|
+
### Padding in MP4 and other BMFF files
|
|
578
|
+
|
|
579
|
+
MP4, MOV and related formats can hold `free` and `skip` boxes, which are
|
|
580
|
+
padding. Tools often rewrite them after a file is signed, so by default the
|
|
581
|
+
signature leaves them out and those edits don't break it. To make any change
|
|
582
|
+
to the padding break the signature, as a change to the media would:
|
|
583
|
+
|
|
584
|
+
```ruby
|
|
585
|
+
C2PA.configure do |config|
|
|
586
|
+
config.exclude_free_and_skip_boxes = false
|
|
587
|
+
end
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
This only affects signing. The choice is recorded in the signed file, and any
|
|
591
|
+
reader holds the file to it.
|
|
592
|
+
|
|
512
593
|
### Everything configurable
|
|
513
594
|
|
|
514
595
|
| Setting | Default | Purpose |
|
|
@@ -519,14 +600,18 @@ without one; c2pa-rs treats that as non-fatal.
|
|
|
519
600
|
| `verify_trust` | `true` | whether trust is checked at all |
|
|
520
601
|
| `remote_manifest_fetch` | `true` | whether reading may fetch over the network |
|
|
521
602
|
| `ocsp_fetch` | `false` | whether revocation is checked over OCSP |
|
|
603
|
+
| `allow_redirects` | `true` | whether network fetches follow redirects; internal targets are always refused |
|
|
604
|
+
| `exclude_free_and_skip_boxes` | `true` | whether BMFF padding boxes are left out of the signature |
|
|
522
605
|
| `thumbnails` | `false` | embed a thumbnail of the asset and of file-backed ingredients |
|
|
523
606
|
| `thumbnail_size` | 1024 | longest edge of the thumbnail, in pixels |
|
|
524
607
|
| `thumbnail_format` | smallest | `:jpeg`, `:png` or `:gif` |
|
|
525
608
|
| `thumbnail_quality` | `:medium` | `:low`, `:medium` or `:high` |
|
|
526
609
|
|
|
527
610
|
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
|
|
529
|
-
|
|
611
|
+
sent, so anything left alone keeps c2pa-rs's own default. Two exceptions are
|
|
612
|
+
always sent, because this gem's default differs from c2pa-rs's: `thumbnails`,
|
|
613
|
+
and c2pa-rs's own verify-after-sign check, which the gem turns off in favour of
|
|
614
|
+
its `verify:` guard.
|
|
530
615
|
`C2PA.configure` with no block resets everything.
|
|
531
616
|
|
|
532
617
|
Turning `verify_trust` off means nothing is ever reported as untrusted, which
|
|
@@ -550,31 +635,59 @@ signed, read back, and asserted to validate.
|
|
|
550
635
|
| MOV | `video/quicktime` |
|
|
551
636
|
| MP3 | `audio/mpeg` |
|
|
552
637
|
| WAV | `audio/wav` |
|
|
553
|
-
|
|
|
638
|
+
| EPUB | `application/epub+zip` |
|
|
639
|
+
| Word (DOCX) | `application/vnd.openxmlformats-officedocument.wordprocessingml.document` |
|
|
640
|
+
| OpenDocument text (ODT) | `application/vnd.oasis.opendocument.text` |
|
|
641
|
+
| OpenXPS | `application/oxps` |
|
|
642
|
+
| PDF | `application/pdf` |
|
|
554
643
|
|
|
555
644
|
The format is detected automatically from the file extension.
|
|
556
645
|
|
|
646
|
+
### EPUB, Office and OpenDocument files
|
|
647
|
+
|
|
648
|
+
These formats are ZIP packages. The manifest is added as
|
|
649
|
+
`META-INF/content_credential.c2pa`, and every other part of the package is
|
|
650
|
+
covered by the signature, so editing the document, or adding or removing a
|
|
651
|
+
file in it, makes it invalid. The document still opens in its usual app after
|
|
652
|
+
signing.
|
|
653
|
+
|
|
654
|
+
c2pa-rs lists the other Office Open XML and OpenDocument types as well: Excel
|
|
655
|
+
and PowerPoint, their macro-enabled forms, OpenDocument spreadsheets,
|
|
656
|
+
presentations, drawings and templates. They use the same code, but only the
|
|
657
|
+
four above have fixtures in the suite.
|
|
658
|
+
|
|
659
|
+
When reading one of these from memory, pass `format:` to `C2PA.read_buffer`.
|
|
660
|
+
They all begin with the same ZIP header, so c2pa-rs can't identify them from
|
|
661
|
+
the bytes alone.
|
|
662
|
+
|
|
557
663
|
JPEG XL must be in the ISOBMFF container form. A bare codestream has no boxes
|
|
558
664
|
to hold a manifest, and c2pa-rs rejects it.
|
|
559
665
|
|
|
560
|
-
### PDF
|
|
666
|
+
### PDF
|
|
667
|
+
|
|
668
|
+
PDFs sign and read like any other format. The manifest goes into the
|
|
669
|
+
document's associated files, as `/AFRelationship /C2PA_Manifest`, and the file
|
|
670
|
+
still opens in a PDF reader afterwards.
|
|
561
671
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
672
|
+
```ruby
|
|
673
|
+
C2PA.sign(
|
|
674
|
+
file: "contract.pdf",
|
|
675
|
+
output: "contract_signed.pdf",
|
|
676
|
+
certificate: "cert.pem",
|
|
677
|
+
key: "key.pem",
|
|
678
|
+
manifest: manifest
|
|
679
|
+
)
|
|
680
|
+
```
|
|
568
681
|
|
|
569
|
-
|
|
682
|
+
This is new. c2pa-rs had no PDF writer until 0.91.1 (September 2026):
|
|
683
|
+
`get_writer` returned `None`, every write path returned `NotImplemented`, and
|
|
684
|
+
upstream had closed the request for one in December 2025. Releases of this gem
|
|
685
|
+
up to 0.5.0 said PDF signing was impossible, and while they shipped that was
|
|
686
|
+
true. It is not true now, and 0.6.0 is the first release where a PDF signs.
|
|
570
687
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
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.
|
|
688
|
+
Expect the signed file to be much larger than the original when the document
|
|
689
|
+
is small. The manifest carries a full certificate chain, which is a few
|
|
690
|
+
kilobytes whatever the document weighs.
|
|
578
691
|
|
|
579
692
|
### Test fixtures
|
|
580
693
|
|
|
@@ -584,10 +697,11 @@ sources, so they carry no third-party content and no licence obligations. They
|
|
|
584
697
|
are deliberately real files rather than placeholders — 160×120 images with
|
|
585
698
|
actual detail, real audio samples, real video frames, and EXIF metadata on the
|
|
586
699
|
JPEG — because C2PA writes into container structures that an empty file would
|
|
587
|
-
not exercise.
|
|
700
|
+
not exercise. The PDF and the four ZIP-based documents are written by hand in
|
|
701
|
+
the same script. All fifteen total about 68 KB.
|
|
588
702
|
|
|
589
|
-
Regenerating them needs `ffmpeg`, `cjxl` and `
|
|
590
|
-
not.
|
|
703
|
+
Regenerating them needs `ffmpeg`, `cwebp`, `cjxl`, `exiftool` and `zip`; running
|
|
704
|
+
the tests does not.
|
|
591
705
|
|
|
592
706
|
Signing certificates are generated on demand, one chain per key type, so every
|
|
593
707
|
supported algorithm is covered:
|