ruby-c2pa 0.4.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 +4 -4
- data/CHANGELOG.md +39 -1
- data/CONTRIBUTING.md +19 -2
- data/README.md +110 -10
- data/ext/c2pa_native/Cargo.lock +146 -1
- data/ext/c2pa_native/Cargo.toml +4 -1
- data/ext/c2pa_native/src/lib.rs +187 -38
- data/lib/c2pa/config.rb +33 -1
- data/lib/c2pa/version.rb +1 -1
- data/lib/c2pa.rb +109 -0
- 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: 41362e47552927fd69a0bec7203dc35f59442af7eeaeb1d5f787216e08c18b79
|
|
4
|
+
data.tar.gz: 6e93a632b24a7baddf4ba4792d8a9f8043559fd5c168df1a685b8094a73f9689
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b9d9de8da30c033cc5e64ea2d27609ce8be1a9b3d08c400c98f4a092c9ad85f27a985f347477ecd3b019366cf75f9c9c65d17a40f94ce1f82476b753b5dc4a85
|
|
7
|
+
data.tar.gz: d888086e87c7eec8fd09d5f8fadb4f6e5f9493d3ecc7199264c10c38bfc254aead1e380d881bbc3b9ad3c7569e30220f57764e85bebe1f98bd0f1a8b816c4e54
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,43 @@ 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
|
+
|
|
10
47
|
## [0.4.0] — 2026-09-11
|
|
11
48
|
|
|
12
49
|
Adds capability on top of 0.3.0. Nothing is removed and no existing call
|
|
@@ -146,7 +183,8 @@ Tagged retroactively. See the v0.2.1 tag for the defects it shipped with.
|
|
|
146
183
|
|
|
147
184
|
Tagged retroactively.
|
|
148
185
|
|
|
149
|
-
[Unreleased]: https://github.com/eddorre/ruby-c2pa/compare/v0.
|
|
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
|
|
150
188
|
[0.4.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.3.0...v0.4.0
|
|
151
189
|
[0.3.0]: https://github.com/eddorre/ruby-c2pa/compare/v0.2.1...v0.3.0
|
|
152
190
|
[0.2.1]: https://github.com/eddorre/ruby-c2pa/compare/v0.2.0...v0.2.1
|
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`:
|
|
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
|
-
###
|
|
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
|
@@ -323,6 +323,46 @@ C2PA.sign(
|
|
|
323
323
|
)
|
|
324
324
|
```
|
|
325
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
|
+
|
|
326
366
|
### Reading a manifest
|
|
327
367
|
|
|
328
368
|
```ruby
|
|
@@ -333,6 +373,15 @@ puts active["title"]
|
|
|
333
373
|
puts active["claim_generator_info"].first["name"] # => "ruby-c2pa"
|
|
334
374
|
```
|
|
335
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
|
+
|
|
336
385
|
### Naming your application
|
|
337
386
|
|
|
338
387
|
Signed files credit `ruby-c2pa` by default. To credit your own application
|
|
@@ -355,8 +404,8 @@ The signed manifest then reads:
|
|
|
355
404
|
{
|
|
356
405
|
"name": "Acme Editor",
|
|
357
406
|
"version": "2.0",
|
|
358
|
-
"org.
|
|
359
|
-
"org.
|
|
407
|
+
"org.contentauth.c2pa_rs": "0.90.22",
|
|
408
|
+
"org.rubygems.ruby_c2pa": "0.5.0"
|
|
360
409
|
}
|
|
361
410
|
```
|
|
362
411
|
|
|
@@ -370,12 +419,15 @@ calling application.
|
|
|
370
419
|
### Checking the SDK version
|
|
371
420
|
|
|
372
421
|
```ruby
|
|
373
|
-
puts C2PA.sdk_version # => "0.
|
|
422
|
+
puts C2PA.sdk_version # => "0.90.22" (the c2pa-rs version the gem was built against)
|
|
374
423
|
```
|
|
375
424
|
|
|
376
425
|
### Error handling
|
|
377
426
|
|
|
378
|
-
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.
|
|
379
431
|
|
|
380
432
|
```ruby
|
|
381
433
|
begin
|
|
@@ -392,6 +444,12 @@ rescue C2PA::ReadError => e
|
|
|
392
444
|
puts "Could not read manifest: #{e.message}"
|
|
393
445
|
end
|
|
394
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
|
+
|
|
395
453
|
# Or rescue any C2PA error broadly
|
|
396
454
|
begin
|
|
397
455
|
C2PA.sign(file: "photo.jpg", output: "photo_signed.jpg", certificate: "cert.pem", key: "key.pem", manifest: manifest)
|
|
@@ -429,6 +487,28 @@ C2PA.configure do |config|
|
|
|
429
487
|
end
|
|
430
488
|
```
|
|
431
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
|
+
|
|
432
512
|
### Everything configurable
|
|
433
513
|
|
|
434
514
|
| Setting | Default | Purpose |
|
|
@@ -439,10 +519,15 @@ end
|
|
|
439
519
|
| `verify_trust` | `true` | whether trust is checked at all |
|
|
440
520
|
| `remote_manifest_fetch` | `true` | whether reading may fetch over the network |
|
|
441
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` |
|
|
442
526
|
|
|
443
527
|
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
|
|
445
|
-
|
|
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.
|
|
446
531
|
|
|
447
532
|
Turning `verify_trust` off means nothing is ever reported as untrusted, which
|
|
448
533
|
in a library for establishing provenance is rarely what you want. It exists for
|
|
@@ -525,20 +610,35 @@ Ruby (C2PA.sign)
|
|
|
525
610
|
▼
|
|
526
611
|
Rust (C2PA::Native.sign_file)
|
|
527
612
|
│
|
|
528
|
-
│
|
|
613
|
+
│ c2pa-rs Builder, through a shared Context
|
|
529
614
|
▼
|
|
530
615
|
c2pa-rs — embeds signed manifest into the file
|
|
531
616
|
```
|
|
532
617
|
|
|
533
|
-
The Rust extension (`ext/c2pa_native/src/lib.rs`) defines `C2PA::Native` with
|
|
618
|
+
The Rust extension (`ext/c2pa_native/src/lib.rs`) defines `C2PA::Native` with six methods:
|
|
534
619
|
|
|
535
620
|
| Method | Description |
|
|
536
621
|
|--------|-------------|
|
|
537
|
-
| `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 |
|
|
538
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 |
|
|
539
627
|
| `C2PA::Native.sdk_version` | Return the c2pa-rs version string |
|
|
540
628
|
|
|
541
|
-
|
|
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.
|
|
542
642
|
|
|
543
643
|
## Contributing
|
|
544
644
|
|
data/ext/c2pa_native/Cargo.lock
CHANGED
|
@@ -329,12 +329,24 @@ version = "3.20.3"
|
|
|
329
329
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
330
330
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
|
331
331
|
|
|
332
|
+
[[package]]
|
|
333
|
+
name = "bytemuck"
|
|
334
|
+
version = "1.25.2"
|
|
335
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
336
|
+
checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797"
|
|
337
|
+
|
|
332
338
|
[[package]]
|
|
333
339
|
name = "byteorder"
|
|
334
340
|
version = "1.5.0"
|
|
335
341
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
336
342
|
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
|
337
343
|
|
|
344
|
+
[[package]]
|
|
345
|
+
name = "byteorder-lite"
|
|
346
|
+
version = "0.1.0"
|
|
347
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
348
|
+
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
|
349
|
+
|
|
338
350
|
[[package]]
|
|
339
351
|
name = "byteordered"
|
|
340
352
|
version = "0.6.0"
|
|
@@ -382,6 +394,7 @@ dependencies = [
|
|
|
382
394
|
"hex",
|
|
383
395
|
"http",
|
|
384
396
|
"id3",
|
|
397
|
+
"image",
|
|
385
398
|
"img-parts",
|
|
386
399
|
"iref",
|
|
387
400
|
"jfifdump",
|
|
@@ -459,6 +472,7 @@ version = "0.1.0"
|
|
|
459
472
|
dependencies = [
|
|
460
473
|
"c2pa",
|
|
461
474
|
"magnus",
|
|
475
|
+
"rb-sys",
|
|
462
476
|
"serde_json",
|
|
463
477
|
]
|
|
464
478
|
|
|
@@ -584,6 +598,12 @@ dependencies = [
|
|
|
584
598
|
"libloading",
|
|
585
599
|
]
|
|
586
600
|
|
|
601
|
+
[[package]]
|
|
602
|
+
name = "color_quant"
|
|
603
|
+
version = "1.1.0"
|
|
604
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
605
|
+
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
|
606
|
+
|
|
587
607
|
[[package]]
|
|
588
608
|
name = "console_log"
|
|
589
609
|
version = "1.1.0"
|
|
@@ -1049,6 +1069,21 @@ version = "2.5.0"
|
|
|
1049
1069
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1050
1070
|
checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
|
1051
1071
|
|
|
1072
|
+
[[package]]
|
|
1073
|
+
name = "fax"
|
|
1074
|
+
version = "0.2.7"
|
|
1075
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1076
|
+
checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a"
|
|
1077
|
+
|
|
1078
|
+
[[package]]
|
|
1079
|
+
name = "fdeflate"
|
|
1080
|
+
version = "0.3.7"
|
|
1081
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1082
|
+
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
|
1083
|
+
dependencies = [
|
|
1084
|
+
"simd-adler32",
|
|
1085
|
+
]
|
|
1086
|
+
|
|
1052
1087
|
[[package]]
|
|
1053
1088
|
name = "ff"
|
|
1054
1089
|
version = "0.13.1"
|
|
@@ -1209,6 +1244,16 @@ dependencies = [
|
|
|
1209
1244
|
"wasm-bindgen",
|
|
1210
1245
|
]
|
|
1211
1246
|
|
|
1247
|
+
[[package]]
|
|
1248
|
+
name = "gif"
|
|
1249
|
+
version = "0.14.2"
|
|
1250
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1251
|
+
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
|
1252
|
+
dependencies = [
|
|
1253
|
+
"color_quant",
|
|
1254
|
+
"weezl 0.1.12",
|
|
1255
|
+
]
|
|
1256
|
+
|
|
1212
1257
|
[[package]]
|
|
1213
1258
|
name = "glob"
|
|
1214
1259
|
version = "0.3.4"
|
|
@@ -1559,6 +1604,35 @@ dependencies = [
|
|
|
1559
1604
|
"icu_properties",
|
|
1560
1605
|
]
|
|
1561
1606
|
|
|
1607
|
+
[[package]]
|
|
1608
|
+
name = "image"
|
|
1609
|
+
version = "0.25.10"
|
|
1610
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1611
|
+
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
|
1612
|
+
dependencies = [
|
|
1613
|
+
"bytemuck",
|
|
1614
|
+
"byteorder-lite",
|
|
1615
|
+
"color_quant",
|
|
1616
|
+
"gif",
|
|
1617
|
+
"image-webp",
|
|
1618
|
+
"moxcms",
|
|
1619
|
+
"num-traits",
|
|
1620
|
+
"png",
|
|
1621
|
+
"tiff",
|
|
1622
|
+
"zune-core",
|
|
1623
|
+
"zune-jpeg",
|
|
1624
|
+
]
|
|
1625
|
+
|
|
1626
|
+
[[package]]
|
|
1627
|
+
name = "image-webp"
|
|
1628
|
+
version = "0.2.4"
|
|
1629
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1630
|
+
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
|
|
1631
|
+
dependencies = [
|
|
1632
|
+
"byteorder-lite",
|
|
1633
|
+
"quick-error",
|
|
1634
|
+
]
|
|
1635
|
+
|
|
1562
1636
|
[[package]]
|
|
1563
1637
|
name = "img-parts"
|
|
1564
1638
|
version = "0.4.0"
|
|
@@ -1801,7 +1875,7 @@ dependencies = [
|
|
|
1801
1875
|
"stringprep",
|
|
1802
1876
|
"thiserror 2.0.20",
|
|
1803
1877
|
"time",
|
|
1804
|
-
"weezl",
|
|
1878
|
+
"weezl 0.2.1",
|
|
1805
1879
|
]
|
|
1806
1880
|
|
|
1807
1881
|
[[package]]
|
|
@@ -1862,6 +1936,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
|
1862
1936
|
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
|
1863
1937
|
dependencies = [
|
|
1864
1938
|
"adler2",
|
|
1939
|
+
"simd-adler32",
|
|
1865
1940
|
]
|
|
1866
1941
|
|
|
1867
1942
|
[[package]]
|
|
@@ -1885,6 +1960,16 @@ dependencies = [
|
|
|
1885
1960
|
"windows-sys 0.61.2",
|
|
1886
1961
|
]
|
|
1887
1962
|
|
|
1963
|
+
[[package]]
|
|
1964
|
+
name = "moxcms"
|
|
1965
|
+
version = "0.8.1"
|
|
1966
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1967
|
+
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
|
1968
|
+
dependencies = [
|
|
1969
|
+
"num-traits",
|
|
1970
|
+
"pxfm",
|
|
1971
|
+
]
|
|
1972
|
+
|
|
1888
1973
|
[[package]]
|
|
1889
1974
|
name = "multiversion"
|
|
1890
1975
|
version = "0.9.0"
|
|
@@ -2195,6 +2280,19 @@ version = "0.3.34"
|
|
|
2195
2280
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2196
2281
|
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
|
|
2197
2282
|
|
|
2283
|
+
[[package]]
|
|
2284
|
+
name = "png"
|
|
2285
|
+
version = "0.18.1"
|
|
2286
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2287
|
+
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
|
2288
|
+
dependencies = [
|
|
2289
|
+
"bitflags 2.13.2",
|
|
2290
|
+
"crc32fast",
|
|
2291
|
+
"fdeflate",
|
|
2292
|
+
"flate2",
|
|
2293
|
+
"miniz_oxide 0.8.9",
|
|
2294
|
+
]
|
|
2295
|
+
|
|
2198
2296
|
[[package]]
|
|
2199
2297
|
name = "png_pong"
|
|
2200
2298
|
version = "0.10.0"
|
|
@@ -2304,6 +2402,18 @@ dependencies = [
|
|
|
2304
2402
|
"unarray",
|
|
2305
2403
|
]
|
|
2306
2404
|
|
|
2405
|
+
[[package]]
|
|
2406
|
+
name = "pxfm"
|
|
2407
|
+
version = "0.1.30"
|
|
2408
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2409
|
+
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
|
2410
|
+
|
|
2411
|
+
[[package]]
|
|
2412
|
+
name = "quick-error"
|
|
2413
|
+
version = "2.0.1"
|
|
2414
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
2415
|
+
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
|
2416
|
+
|
|
2307
2417
|
[[package]]
|
|
2308
2418
|
name = "quick-xml"
|
|
2309
2419
|
version = "0.41.0"
|
|
@@ -3372,6 +3482,20 @@ dependencies = [
|
|
|
3372
3482
|
"syn 3.0.5",
|
|
3373
3483
|
]
|
|
3374
3484
|
|
|
3485
|
+
[[package]]
|
|
3486
|
+
name = "tiff"
|
|
3487
|
+
version = "0.11.3"
|
|
3488
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3489
|
+
checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52"
|
|
3490
|
+
dependencies = [
|
|
3491
|
+
"fax",
|
|
3492
|
+
"flate2",
|
|
3493
|
+
"half",
|
|
3494
|
+
"quick-error",
|
|
3495
|
+
"weezl 0.1.12",
|
|
3496
|
+
"zune-jpeg",
|
|
3497
|
+
]
|
|
3498
|
+
|
|
3375
3499
|
[[package]]
|
|
3376
3500
|
name = "time"
|
|
3377
3501
|
version = "0.3.55"
|
|
@@ -3835,6 +3959,12 @@ dependencies = [
|
|
|
3835
3959
|
"rustls-pki-types",
|
|
3836
3960
|
]
|
|
3837
3961
|
|
|
3962
|
+
[[package]]
|
|
3963
|
+
name = "weezl"
|
|
3964
|
+
version = "0.1.12"
|
|
3965
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3966
|
+
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
|
3967
|
+
|
|
3838
3968
|
[[package]]
|
|
3839
3969
|
name = "weezl"
|
|
3840
3970
|
version = "0.2.1"
|
|
@@ -4213,3 +4343,18 @@ name = "zmij"
|
|
|
4213
4343
|
version = "1.0.23"
|
|
4214
4344
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
4215
4345
|
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
|
4346
|
+
|
|
4347
|
+
[[package]]
|
|
4348
|
+
name = "zune-core"
|
|
4349
|
+
version = "0.5.3"
|
|
4350
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
4351
|
+
checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b"
|
|
4352
|
+
|
|
4353
|
+
[[package]]
|
|
4354
|
+
name = "zune-jpeg"
|
|
4355
|
+
version = "0.5.15"
|
|
4356
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
4357
|
+
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
|
4358
|
+
dependencies = [
|
|
4359
|
+
"zune-core",
|
|
4360
|
+
]
|
data/ext/c2pa_native/Cargo.toml
CHANGED
|
@@ -9,7 +9,10 @@ crate-type = ["cdylib"]
|
|
|
9
9
|
|
|
10
10
|
[dependencies]
|
|
11
11
|
magnus = { version = "0.8", features = [] }
|
|
12
|
-
|
|
12
|
+
# Only for rb_thread_call_without_gvl, which magnus does not wrap. Kept at the
|
|
13
|
+
# range magnus itself depends on so Cargo resolves a single copy.
|
|
14
|
+
rb-sys = { version = "0.9.113", default-features = false }
|
|
15
|
+
c2pa = { version = "0.90", features = ["file_io", "pdf", "add_thumbnails"] }
|
|
13
16
|
serde_json = "1"
|
|
14
17
|
|
|
15
18
|
[profile.release]
|
data/ext/c2pa_native/src/lib.rs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
use std::fs::File;
|
|
2
|
+
use std::io::Cursor;
|
|
2
3
|
use std::path::Path;
|
|
3
4
|
use std::sync::{Arc, RwLock, OnceLock};
|
|
4
5
|
use c2pa::{create_signer, Builder, BuilderIntent, Context, Reader, SigningAlg};
|
|
5
|
-
use magnus::{function, prelude::*, Error, Ruby};
|
|
6
|
+
use magnus::{function, prelude::*, Error, RString, Ruby};
|
|
6
7
|
|
|
7
8
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
8
9
|
|
|
@@ -18,9 +19,21 @@ use magnus::{function, prelude::*, Error, Ruby};
|
|
|
18
19
|
//
|
|
19
20
|
// It currently carries defaults. Exposing settings to Ruby is a separate piece
|
|
20
21
|
// of work; this is the seam that makes it possible.
|
|
22
|
+
// The one place this gem departs from c2pa-rs's defaults. c2pa-rs generates
|
|
23
|
+
// thumbnails when the feature is compiled in, scaling to a 1024px long edge —
|
|
24
|
+
// and it upscales, so a 160x120 source gets a 1024x768 "thumbnail" ten times
|
|
25
|
+
// its size. Off unless asked for; Config::to_json always states the choice.
|
|
26
|
+
const DEFAULT_SETTINGS: &str = r#"{"builder":{"thumbnail":{"enabled":false}}}"#;
|
|
27
|
+
|
|
21
28
|
fn context_slot() -> &'static RwLock<Arc<Context>> {
|
|
22
29
|
static CONTEXT: OnceLock<RwLock<Arc<Context>>> = OnceLock::new();
|
|
23
|
-
CONTEXT.get_or_init(||
|
|
30
|
+
CONTEXT.get_or_init(|| {
|
|
31
|
+
let context = Context::new()
|
|
32
|
+
.with_settings(DEFAULT_SETTINGS)
|
|
33
|
+
.expect("built-in default settings are valid")
|
|
34
|
+
.into_shared();
|
|
35
|
+
RwLock::new(context)
|
|
36
|
+
})
|
|
24
37
|
}
|
|
25
38
|
|
|
26
39
|
fn shared_context() -> Arc<Context> {
|
|
@@ -116,51 +129,89 @@ fn add_ingredient_files(
|
|
|
116
129
|
Ok(())
|
|
117
130
|
}
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
132
|
+
type BoxError = Box<dyn std::error::Error>;
|
|
133
|
+
|
|
134
|
+
// The description of what to sign, shared by the file and buffer paths.
|
|
135
|
+
struct SigningRequest<'a> {
|
|
136
|
+
cert_path: &'a str,
|
|
137
|
+
key_path: &'a str,
|
|
138
|
+
alg: &'a str,
|
|
139
|
+
manifest_json: Option<&'a str>,
|
|
140
|
+
intent: Option<&'a str>,
|
|
141
|
+
ingredient_files_json: Option<&'a str>,
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn build_signer(cert_path: &str, key_path: &str, alg_str: &str) -> Result<Box<dyn c2pa::Signer + Send + Sync>, BoxError> {
|
|
129
145
|
let cert = std::fs::read(cert_path)
|
|
130
146
|
.map_err(|e| format!("Cannot read certificate '{}': {}", cert_path, e))?;
|
|
131
147
|
let key = std::fs::read(key_path)
|
|
132
148
|
.map_err(|e| format!("Cannot read key '{}': {}", key_path, e))?;
|
|
133
149
|
|
|
134
150
|
let alg = alg_from_str(alg_str)?;
|
|
135
|
-
|
|
136
|
-
.map_err(|e| format!("Failed to create signer: {}", e))
|
|
151
|
+
create_signer::from_keys(&cert, &key, alg, None)
|
|
152
|
+
.map_err(|e| format!("Failed to create signer: {}", e).into())
|
|
153
|
+
}
|
|
137
154
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
let default_json = format!(r#"{{"title": "{}"}}"#, title);
|
|
144
|
-
let json = manifest_json.unwrap_or(&default_json);
|
|
155
|
+
// A Builder carrying the manifest, intent and ingredients, ready to sign.
|
|
156
|
+
// `fallback_title` is used only when no manifest JSON was supplied.
|
|
157
|
+
fn build_builder(request: &SigningRequest, fallback_title: &str) -> Result<Builder, BoxError> {
|
|
158
|
+
let default_json = format!(r#"{{"title": "{}"}}"#, fallback_title.replace('"', "\\\""));
|
|
159
|
+
let json = request.manifest_json.unwrap_or(&default_json);
|
|
145
160
|
|
|
146
161
|
let mut builder = Builder::from_shared_context(&shared_context())
|
|
147
162
|
.with_definition(json)
|
|
148
163
|
.map_err(|e| format!("Invalid manifest JSON: {}", e))?;
|
|
149
164
|
|
|
150
|
-
if let Some(intent) =
|
|
165
|
+
if let Some(intent) = request.intent {
|
|
151
166
|
builder.set_intent(intent_from_str(intent)?);
|
|
152
167
|
}
|
|
153
168
|
|
|
154
|
-
if let Some(files) = ingredient_files_json {
|
|
169
|
+
if let Some(files) = request.ingredient_files_json {
|
|
155
170
|
add_ingredient_files(&mut builder, files)?;
|
|
156
171
|
}
|
|
157
172
|
|
|
158
|
-
builder
|
|
173
|
+
Ok(builder)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fn do_sign_file(source_path: &str, dest_path: &str, request: &SigningRequest) -> Result<(), BoxError> {
|
|
177
|
+
let signer = build_signer(request.cert_path, request.key_path, request.alg)?;
|
|
178
|
+
|
|
179
|
+
let title = Path::new(source_path)
|
|
180
|
+
.file_name()
|
|
181
|
+
.and_then(|n| n.to_str())
|
|
182
|
+
.unwrap_or("unknown");
|
|
183
|
+
let mut builder = build_builder(request, title)?;
|
|
184
|
+
|
|
185
|
+
builder
|
|
186
|
+
.sign_file(&*signer, source_path, dest_path)
|
|
159
187
|
.map_err(|e| format!("Signing failed: {}", e))?;
|
|
160
188
|
|
|
161
189
|
Ok(())
|
|
162
190
|
}
|
|
163
191
|
|
|
192
|
+
// Sign bytes held in memory. The source is read through a Cursor, and the
|
|
193
|
+
// destination has to be one too: c2pa-rs writes the asset and then seeks back
|
|
194
|
+
// to hash it and patch the manifest in, so a write-only sink will not do.
|
|
195
|
+
fn do_sign_buffer(data: &[u8], format: &str, request: &SigningRequest) -> Result<Vec<u8>, BoxError> {
|
|
196
|
+
let signer = build_signer(request.cert_path, request.key_path, request.alg)?;
|
|
197
|
+
let mut builder = build_builder(request, "buffer")?;
|
|
198
|
+
|
|
199
|
+
let mut source = Cursor::new(data);
|
|
200
|
+
let mut dest = Cursor::new(Vec::new());
|
|
201
|
+
builder
|
|
202
|
+
.sign(&*signer, format, &mut source, &mut dest)
|
|
203
|
+
.map_err(|e| format!("Signing failed: {}", e))?;
|
|
204
|
+
|
|
205
|
+
Ok(dest.into_inner())
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
fn do_read_buffer(data: &[u8], format: &str) -> Result<String, BoxError> {
|
|
209
|
+
let reader = Reader::from_shared_context(&shared_context())
|
|
210
|
+
.with_stream(format, Cursor::new(data))
|
|
211
|
+
.map_err(|e| format!("Failed to read manifest from buffer: {}", e))?;
|
|
212
|
+
Ok(reader.json())
|
|
213
|
+
}
|
|
214
|
+
|
|
164
215
|
fn do_read_file(path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
|
165
216
|
let reader = Reader::from_shared_context(&shared_context())
|
|
166
217
|
.with_file(path)
|
|
@@ -168,8 +219,68 @@ fn do_read_file(path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
|
|
168
219
|
Ok(reader.json())
|
|
169
220
|
}
|
|
170
221
|
|
|
222
|
+
// ─── Running without the GVL ──────────────────────────────────────────────────
|
|
223
|
+
//
|
|
224
|
+
// Signing and reading are CPU-bound Rust with no need of the interpreter, so
|
|
225
|
+
// they run with Ruby's global VM lock released and other Ruby threads make
|
|
226
|
+
// progress meanwhile. magnus does not wrap rb_thread_call_without_gvl, hence
|
|
227
|
+
// the trampoline: the closure travels through the void pointer, its result
|
|
228
|
+
// travels back the same way, and nothing inside may touch Ruby.
|
|
229
|
+
//
|
|
230
|
+
// A panic must not unwind across the extern "C" frame (Rust aborts if it
|
|
231
|
+
// does), so it is caught on the far side and resumed once the lock is held.
|
|
232
|
+
|
|
233
|
+
type NoGvlSlot<F, R> = (Option<F>, Option<std::thread::Result<R>>);
|
|
234
|
+
|
|
235
|
+
unsafe extern "C" fn no_gvl_trampoline<F, R>(arg: *mut std::ffi::c_void) -> *mut std::ffi::c_void
|
|
236
|
+
where
|
|
237
|
+
F: FnOnce() -> R,
|
|
238
|
+
{
|
|
239
|
+
let slot = &mut *(arg as *mut NoGvlSlot<F, R>);
|
|
240
|
+
let f = slot.0.take().expect("closure taken twice");
|
|
241
|
+
slot.1 = Some(std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)));
|
|
242
|
+
std::ptr::null_mut()
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
fn without_gvl<F, R>(f: F) -> R
|
|
246
|
+
where
|
|
247
|
+
F: FnOnce() -> R,
|
|
248
|
+
{
|
|
249
|
+
let mut slot: NoGvlSlot<F, R> = (Some(f), None);
|
|
250
|
+
|
|
251
|
+
// RUBY_UBF_IO is a macro, not a symbol, so bindgen has no name for it. It
|
|
252
|
+
// is the sentinel (rb_unblock_function_t *)-1, which tells Ruby to use
|
|
253
|
+
// its own IO unblocker: a Thread#kill or Timeout aimed at this thread
|
|
254
|
+
// interrupts a blocking syscall (a remote manifest fetch, say) rather
|
|
255
|
+
// than waiting for the call to finish. Option<fn> has the null niche, so
|
|
256
|
+
// a non-null bit pattern is a valid Some that Ruby compares by value and
|
|
257
|
+
// never calls.
|
|
258
|
+
let ubf: rb_sys::rb_unblock_function_t = unsafe { std::mem::transmute(-1isize) };
|
|
259
|
+
|
|
260
|
+
unsafe {
|
|
261
|
+
rb_sys::rb_thread_call_without_gvl(
|
|
262
|
+
Some(no_gvl_trampoline::<F, R>),
|
|
263
|
+
&mut slot as *mut NoGvlSlot<F, R> as *mut std::ffi::c_void,
|
|
264
|
+
ubf,
|
|
265
|
+
std::ptr::null_mut(),
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
match slot.1.expect("closure did not run") {
|
|
270
|
+
Ok(value) => value,
|
|
271
|
+
Err(panic) => std::panic::resume_unwind(panic),
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
171
275
|
// ─── Ruby-facing functions ────────────────────────────────────────────────────
|
|
172
276
|
|
|
277
|
+
fn runtime_error(e: BoxError) -> Error {
|
|
278
|
+
Error::new(
|
|
279
|
+
Ruby::get().expect("called from Ruby thread").exception_runtime_error(),
|
|
280
|
+
e.to_string(),
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
|
|
173
284
|
fn sign_file(
|
|
174
285
|
source: String,
|
|
175
286
|
dest: String,
|
|
@@ -180,27 +291,63 @@ fn sign_file(
|
|
|
180
291
|
intent: Option<String>,
|
|
181
292
|
ingredient_files: Option<String>,
|
|
182
293
|
) -> Result<String, Error> {
|
|
183
|
-
let
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
294
|
+
let request = SigningRequest {
|
|
295
|
+
cert_path: &cert,
|
|
296
|
+
key_path: &key,
|
|
297
|
+
alg: alg.as_deref().unwrap_or("es256"),
|
|
298
|
+
manifest_json: manifest_json.as_deref(),
|
|
299
|
+
intent: intent.as_deref(),
|
|
300
|
+
ingredient_files_json: ingredient_files.as_deref(),
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
without_gvl(|| do_sign_file(&source, &dest, &request)).map_err(runtime_error)?;
|
|
189
304
|
Ok(dest)
|
|
190
305
|
}
|
|
191
306
|
|
|
307
|
+
// Takes an RString rather than a String so the bytes arrive untouched: a
|
|
308
|
+
// String argument would be transcoded to UTF-8, which is wrong for a JPEG.
|
|
309
|
+
// The slice is copied out at once, since Ruby may move or free the backing
|
|
310
|
+
// store the moment control returns to it.
|
|
311
|
+
fn sign_buffer(
|
|
312
|
+
ruby: &Ruby,
|
|
313
|
+
data: RString,
|
|
314
|
+
format: String,
|
|
315
|
+
cert: String,
|
|
316
|
+
key: String,
|
|
317
|
+
alg: Option<String>,
|
|
318
|
+
manifest_json: Option<String>,
|
|
319
|
+
intent: Option<String>,
|
|
320
|
+
ingredient_files: Option<String>,
|
|
321
|
+
) -> Result<RString, Error> {
|
|
322
|
+
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
323
|
+
let request = SigningRequest {
|
|
324
|
+
cert_path: &cert,
|
|
325
|
+
key_path: &key,
|
|
326
|
+
alg: alg.as_deref().unwrap_or("es256"),
|
|
327
|
+
manifest_json: manifest_json.as_deref(),
|
|
328
|
+
intent: intent.as_deref(),
|
|
329
|
+
ingredient_files_json: ingredient_files.as_deref(),
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
let signed = without_gvl(|| do_sign_buffer(&bytes, &format, &request)).map_err(runtime_error)?;
|
|
333
|
+
Ok(ruby.str_from_slice(&signed))
|
|
334
|
+
}
|
|
335
|
+
|
|
192
336
|
fn read_file(path: String) -> Result<String, Error> {
|
|
193
|
-
do_read_file(&path)
|
|
194
|
-
|
|
337
|
+
without_gvl(|| do_read_file(&path)).map_err(runtime_error)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// c2pa-rs sniffs the container from the leading bytes and lets the hint win
|
|
341
|
+
// only when it agrees; the hint carries the decision alone when sniffing
|
|
342
|
+
// fails, as it does for SVG.
|
|
343
|
+
fn read_buffer(data: RString, format: Option<String>) -> Result<String, Error> {
|
|
344
|
+
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
345
|
+
let format = format.as_deref().unwrap_or("application/octet-stream");
|
|
346
|
+
without_gvl(|| do_read_buffer(&bytes, format)).map_err(runtime_error)
|
|
195
347
|
}
|
|
196
348
|
|
|
197
349
|
fn configure(settings_json: String) -> Result<(), Error> {
|
|
198
|
-
do_configure(&settings_json).map_err(
|
|
199
|
-
Error::new(
|
|
200
|
-
Ruby::get().expect("called from Ruby thread").exception_runtime_error(),
|
|
201
|
-
e.to_string(),
|
|
202
|
-
)
|
|
203
|
-
})
|
|
350
|
+
do_configure(&settings_json).map_err(runtime_error)
|
|
204
351
|
}
|
|
205
352
|
|
|
206
353
|
fn sdk_version() -> String {
|
|
@@ -215,7 +362,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
215
362
|
let native = c2pa.define_module("Native")?;
|
|
216
363
|
|
|
217
364
|
native.define_singleton_method("sign_file", function!(sign_file, 8))?;
|
|
365
|
+
native.define_singleton_method("sign_buffer", function!(sign_buffer, 8))?;
|
|
218
366
|
native.define_singleton_method("read_file", function!(read_file, 1))?;
|
|
367
|
+
native.define_singleton_method("read_buffer", function!(read_buffer, 2))?;
|
|
219
368
|
native.define_singleton_method("configure", function!(configure, 1))?;
|
|
220
369
|
native.define_singleton_method("sdk_version", function!(sdk_version, 0))?;
|
|
221
370
|
|
data/lib/c2pa/config.rb
CHANGED
|
@@ -38,6 +38,27 @@ module C2PA
|
|
|
38
38
|
# network requests.
|
|
39
39
|
attr_accessor :ocsp_fetch
|
|
40
40
|
|
|
41
|
+
# Whether to embed a thumbnail of the asset in its manifest, and of each
|
|
42
|
+
# ingredient supplied as a file.
|
|
43
|
+
#
|
|
44
|
+
# Off by default, which departs from c2pa-rs. Its thumbnail generation
|
|
45
|
+
# scales to a fixed long edge and upscales to reach it, so a 160x120 image
|
|
46
|
+
# gets a 1024x768 thumbnail ten times its own size. Turn it on for assets
|
|
47
|
+
# that are larger than thumbnail_size, or set thumbnail_size to suit.
|
|
48
|
+
#
|
|
49
|
+
# Thumbnails are produced for JPEG, PNG, WebP and TIFF. Other formats are
|
|
50
|
+
# signed without one; c2pa-rs treats that as non-fatal.
|
|
51
|
+
attr_accessor :thumbnails
|
|
52
|
+
|
|
53
|
+
# Longest edge of the thumbnail in pixels. c2pa-rs's default is 1024.
|
|
54
|
+
attr_accessor :thumbnail_size
|
|
55
|
+
|
|
56
|
+
# :jpeg, :png or :gif. Left unset, c2pa-rs picks the smaller encoding.
|
|
57
|
+
attr_accessor :thumbnail_format
|
|
58
|
+
|
|
59
|
+
# :low, :medium or :high. c2pa-rs's default is :medium.
|
|
60
|
+
attr_accessor :thumbnail_quality
|
|
61
|
+
|
|
41
62
|
def initialize
|
|
42
63
|
@trust_anchors = nil
|
|
43
64
|
@trust_list = nil
|
|
@@ -45,6 +66,10 @@ module C2PA
|
|
|
45
66
|
@verify_trust = nil
|
|
46
67
|
@remote_manifest_fetch = nil
|
|
47
68
|
@ocsp_fetch = nil
|
|
69
|
+
@thumbnails = false
|
|
70
|
+
@thumbnail_size = nil
|
|
71
|
+
@thumbnail_format = nil
|
|
72
|
+
@thumbnail_quality = nil
|
|
48
73
|
end
|
|
49
74
|
|
|
50
75
|
# The settings document c2pa-rs expects.
|
|
@@ -64,7 +89,14 @@ module C2PA
|
|
|
64
89
|
verify["remote_manifest_fetch"] = @remote_manifest_fetch unless @remote_manifest_fetch.nil?
|
|
65
90
|
verify["ocsp_fetch"] = @ocsp_fetch unless @ocsp_fetch.nil?
|
|
66
91
|
|
|
67
|
-
|
|
92
|
+
# enabled is always sent: this gem's default differs from c2pa-rs's, so
|
|
93
|
+
# leaving it out would mean inheriting the wrong one.
|
|
94
|
+
thumbnail = { "enabled" => @thumbnails == true }
|
|
95
|
+
thumbnail["long_edge"] = Integer(@thumbnail_size) unless @thumbnail_size.nil?
|
|
96
|
+
thumbnail["format"] = @thumbnail_format.to_s.downcase unless @thumbnail_format.nil?
|
|
97
|
+
thumbnail["quality"] = @thumbnail_quality.to_s.downcase unless @thumbnail_quality.nil?
|
|
98
|
+
|
|
99
|
+
settings = { "builder" => { "thumbnail" => thumbnail } }
|
|
68
100
|
settings["trust"] = trust unless trust.empty?
|
|
69
101
|
settings["verify"] = verify unless verify.empty?
|
|
70
102
|
|
data/lib/c2pa/version.rb
CHANGED
data/lib/c2pa.rb
CHANGED
|
@@ -97,6 +97,75 @@ module C2PA
|
|
|
97
97
|
output
|
|
98
98
|
end
|
|
99
99
|
|
|
100
|
+
# Sign bytes held in memory, returning the signed bytes.
|
|
101
|
+
#
|
|
102
|
+
# The counterpart to C2PA.sign for data you already have loaded, such as an
|
|
103
|
+
# upload. The format must be given, since there is no filename to infer it
|
|
104
|
+
# from. Input must be binary; a UTF-8-tagged string is rejected rather than
|
|
105
|
+
# transcoded, because that would corrupt the asset.
|
|
106
|
+
#
|
|
107
|
+
# The input, the working copy and the result on both sides of the boundary
|
|
108
|
+
# are resident at once, so budget about four times the asset. Prefer
|
|
109
|
+
# C2PA.sign with paths for large video.
|
|
110
|
+
#
|
|
111
|
+
# @param data [String] the asset, as a binary string
|
|
112
|
+
# @param format [String] MIME type, e.g. "image/jpeg"
|
|
113
|
+
# @param certificate [String] path to a PEM-encoded X.509 certificate chain
|
|
114
|
+
# @param key [String] path to a PEM-encoded private key
|
|
115
|
+
# @param algorithm [String] signing algorithm (default: "es256")
|
|
116
|
+
# @param manifest [C2PA::Manifest] the manifest to embed
|
|
117
|
+
# @param verify [Boolean] read the result back and confirm it validates
|
|
118
|
+
# @return [String] the signed asset, as a binary string
|
|
119
|
+
# @raise [C2PA::SigningError] if signing fails, or if the result does not validate
|
|
120
|
+
#
|
|
121
|
+
# @example
|
|
122
|
+
# signed = C2PA.sign_buffer(
|
|
123
|
+
# data: File.binread("photo.jpg"),
|
|
124
|
+
# format: "image/jpeg",
|
|
125
|
+
# certificate: "cert.pem",
|
|
126
|
+
# key: "key.pem",
|
|
127
|
+
# manifest: manifest
|
|
128
|
+
# )
|
|
129
|
+
def self.sign_buffer(data:, format:, certificate:, key:, algorithm: "es256", manifest:, verify: true)
|
|
130
|
+
manifest_json = manifest.to_json
|
|
131
|
+
data = binary!(data, "data")
|
|
132
|
+
|
|
133
|
+
raise SigningError, "Certificate file not found: '#{certificate}'" unless File.exist?(certificate)
|
|
134
|
+
raise SigningError, "Key file not found: '#{key}'" unless File.exist?(key)
|
|
135
|
+
|
|
136
|
+
signed =
|
|
137
|
+
begin
|
|
138
|
+
intent = manifest.respond_to?(:intent) ? manifest.intent&.to_s : nil
|
|
139
|
+
files = manifest.respond_to?(:ingredient_files) ? manifest.ingredient_files : []
|
|
140
|
+
ingredient_files = files.empty? ? nil : JSON.generate(files)
|
|
141
|
+
Native.sign_buffer(data, format, certificate, key, algorithm, manifest_json,
|
|
142
|
+
intent, ingredient_files)
|
|
143
|
+
rescue RuntimeError => e
|
|
144
|
+
raise SigningError, e.message
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
verify_signed_buffer!(signed, format) if verify
|
|
148
|
+
|
|
149
|
+
signed
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Read the C2PA manifest embedded in bytes held in memory.
|
|
153
|
+
#
|
|
154
|
+
# c2pa-rs identifies most formats from the leading bytes and ignores the
|
|
155
|
+
# hint when the two disagree. The hint matters for formats with no signature
|
|
156
|
+
# to sniff, such as SVG, which cannot be read without it.
|
|
157
|
+
#
|
|
158
|
+
# @param data [String] the asset, as a binary string
|
|
159
|
+
# @param format [String, nil] MIME type or extension, e.g. "image/svg+xml"
|
|
160
|
+
# @return [Hash] parsed manifest JSON
|
|
161
|
+
# @raise [C2PA::ReadError] if the data has no valid manifest
|
|
162
|
+
def self.read_buffer(data:, format: nil)
|
|
163
|
+
data = binary!(data, "data")
|
|
164
|
+
JSON.parse(Native.read_buffer(data, format))
|
|
165
|
+
rescue RuntimeError => e
|
|
166
|
+
raise ReadError, e.message
|
|
167
|
+
end
|
|
168
|
+
|
|
100
169
|
# Read the C2PA manifest embedded in a signed file.
|
|
101
170
|
#
|
|
102
171
|
# @param file [String] path to the signed file
|
|
@@ -155,6 +224,46 @@ module C2PA
|
|
|
155
224
|
end
|
|
156
225
|
private_class_method :verify_signed_output!
|
|
157
226
|
|
|
227
|
+
# The buffer counterpart to verify_signed_output!. Nothing to delete: a
|
|
228
|
+
# rejected result is simply not returned.
|
|
229
|
+
def self.verify_signed_buffer!(signed, format)
|
|
230
|
+
result =
|
|
231
|
+
begin
|
|
232
|
+
read_buffer(data: signed, format: format)
|
|
233
|
+
rescue ReadError => e
|
|
234
|
+
raise SigningError, "signed data failed verification: could not read it back: #{e.message}"
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
state = result["validation_state"]
|
|
238
|
+
return if VALID_STATES.include?(state)
|
|
239
|
+
|
|
240
|
+
failures = Array(result.dig("validation_results", "activeManifest", "failure"))
|
|
241
|
+
.map { |failure| "#{failure["code"]} (#{failure["explanation"]})" }
|
|
242
|
+
detail = failures.empty? ? "no failure detail reported" : failures.uniq.join(", ")
|
|
243
|
+
|
|
244
|
+
raise SigningError,
|
|
245
|
+
"signed data failed verification: validation_state=#{state.inspect}, #{detail}. " \
|
|
246
|
+
"Pass verify: false to receive it anyway."
|
|
247
|
+
end
|
|
248
|
+
private_class_method :verify_signed_buffer!
|
|
249
|
+
|
|
250
|
+
# Asset bytes must be binary. The native layer takes the raw bytes whatever
|
|
251
|
+
# the tag says, so a UTF-8-tagged string could be passed through as-is. It
|
|
252
|
+
# is refused instead because the tag means the bytes came through a text
|
|
253
|
+
# path (File.read rather than File.binread), and on Windows that path has
|
|
254
|
+
# already rewritten line endings. Nothing notices until a verifier rejects
|
|
255
|
+
# the result. Refusing early turns a silent corruption into an error with a
|
|
256
|
+
# fix in the message.
|
|
257
|
+
def self.binary!(data, name)
|
|
258
|
+
raise ArgumentError, "#{name} must be a String, got #{data.class}" unless data.is_a?(String)
|
|
259
|
+
return data if data.encoding == Encoding::BINARY
|
|
260
|
+
|
|
261
|
+
raise ArgumentError,
|
|
262
|
+
"#{name} must be a binary string (Encoding::BINARY), got #{data.encoding}. " \
|
|
263
|
+
"Use File.binread, or call .b on the string."
|
|
264
|
+
end
|
|
265
|
+
private_class_method :binary!
|
|
266
|
+
|
|
158
267
|
# Remove a file this library created and is about to reject.
|
|
159
268
|
def self.discard(path)
|
|
160
269
|
File.delete(path) if File.exist?(path)
|