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