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
data/ext/c2pa_native/src/lib.rs
CHANGED
|
@@ -2,7 +2,7 @@ use std::fs::File;
|
|
|
2
2
|
use std::io::Cursor;
|
|
3
3
|
use std::path::Path;
|
|
4
4
|
use std::sync::{Arc, RwLock, OnceLock};
|
|
5
|
-
use c2pa::{create_signer, Builder, BuilderIntent, Context, Reader, SigningAlg};
|
|
5
|
+
use c2pa::{create_signer, format_from_path, Builder, BuilderIntent, Context, Reader, SigningAlg};
|
|
6
6
|
use magnus::{function, prelude::*, Error, RString, Ruby};
|
|
7
7
|
|
|
8
8
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -19,11 +19,20 @@ use magnus::{function, prelude::*, Error, RString, Ruby};
|
|
|
19
19
|
//
|
|
20
20
|
// It currently carries defaults. Exposing settings to Ruby is a separate piece
|
|
21
21
|
// of work; this is the seam that makes it possible.
|
|
22
|
-
// The
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
|
|
22
|
+
// The two places this gem departs from c2pa-rs's defaults; Config::to_json
|
|
23
|
+
// always states both.
|
|
24
|
+
//
|
|
25
|
+
// Thumbnails: c2pa-rs generates them when the feature is compiled in, scaling
|
|
26
|
+
// to a 1024px long edge — and it upscales, so a 160x120 source gets a
|
|
27
|
+
// 1024x768 "thumbnail" ten times its size. Off unless asked for.
|
|
28
|
+
//
|
|
29
|
+
// verify_after_sign: since 0.91 c2pa-rs validates the manifest before writing
|
|
30
|
+
// and refuses an invalid one. The gem's own guard reads the finished output
|
|
31
|
+
// back, which also checks the hashes on the bytes written (upstream's check
|
|
32
|
+
// skips them by default), and honours verify: false for inspecting an invalid
|
|
33
|
+
// file. Running both would read every output twice, so upstream's is off.
|
|
34
|
+
const DEFAULT_SETTINGS: &str =
|
|
35
|
+
r#"{"builder":{"thumbnail":{"enabled":false}},"verify":{"verify_after_sign":false}}"#;
|
|
27
36
|
|
|
28
37
|
fn context_slot() -> &'static RwLock<Arc<Context>> {
|
|
29
38
|
static CONTEXT: OnceLock<RwLock<Arc<Context>>> = OnceLock::new();
|
|
@@ -189,6 +198,39 @@ fn do_sign_file(source_path: &str, dest_path: &str, request: &SigningRequest) ->
|
|
|
189
198
|
Ok(())
|
|
190
199
|
}
|
|
191
200
|
|
|
201
|
+
// Sign without embedding: the asset is hashed as it is, the output carries no
|
|
202
|
+
// manifest, and the manifest store goes to its own file. Hashing the asset
|
|
203
|
+
// unmodified is what makes the pair valid; stripping an embedded manifest out
|
|
204
|
+
// afterwards does not work, because embedding moves offsets elsewhere in the
|
|
205
|
+
// file (chunk offsets in an MP4's moov, for one).
|
|
206
|
+
fn do_sign_file_detached(source_path: &str, dest_path: &str, sidecar_path: &str, request: &SigningRequest)
|
|
207
|
+
-> Result<(), BoxError> {
|
|
208
|
+
let signer = build_signer(request.cert_path, request.key_path, request.alg)?;
|
|
209
|
+
|
|
210
|
+
let title = Path::new(source_path)
|
|
211
|
+
.file_name()
|
|
212
|
+
.and_then(|n| n.to_str())
|
|
213
|
+
.unwrap_or("unknown");
|
|
214
|
+
let mut builder = build_builder(request, title)?;
|
|
215
|
+
builder.set_no_embed(true);
|
|
216
|
+
|
|
217
|
+
let format = format_from_path(source_path)
|
|
218
|
+
.ok_or_else(|| format!("Signing failed: cannot tell the format of '{}' from its extension", source_path))?;
|
|
219
|
+
let mut source = File::open(source_path)
|
|
220
|
+
.map_err(|e| format!("Signing failed: cannot open '{}': {}", source_path, e))?;
|
|
221
|
+
let mut dest = std::fs::OpenOptions::new().read(true).write(true).create_new(true).open(dest_path)
|
|
222
|
+
.map_err(|e| format!("Signing failed: cannot create '{}': {}", dest_path, e))?;
|
|
223
|
+
|
|
224
|
+
let manifest = builder
|
|
225
|
+
.sign(&*signer, &format, &mut source, &mut dest)
|
|
226
|
+
.map_err(|e| format!("Signing failed: {}", e))?;
|
|
227
|
+
|
|
228
|
+
std::fs::write(sidecar_path, manifest)
|
|
229
|
+
.map_err(|e| format!("Signing failed: cannot write '{}': {}", sidecar_path, e))?;
|
|
230
|
+
|
|
231
|
+
Ok(())
|
|
232
|
+
}
|
|
233
|
+
|
|
192
234
|
// Sign bytes held in memory. The source is read through a Cursor, and the
|
|
193
235
|
// destination has to be one too: c2pa-rs writes the asset and then seeks back
|
|
194
236
|
// to hash it and patch the manifest in, so a write-only sink will not do.
|
|
@@ -219,6 +261,27 @@ fn do_read_file(path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
|
|
219
261
|
Ok(reader.json())
|
|
220
262
|
}
|
|
221
263
|
|
|
264
|
+
// A manifest store kept apart from its asset, as a .c2pa sidecar or fetched
|
|
265
|
+
// from a remote URL. The store is validated against the asset's bytes; read
|
|
266
|
+
// on its own, a store's hard binding hashes the store itself and never
|
|
267
|
+
// matches.
|
|
268
|
+
fn do_read_detached(manifest: &[u8], format: &str, asset: impl std::io::Read + std::io::Seek + Send)
|
|
269
|
+
-> Result<String, BoxError> {
|
|
270
|
+
let reader = Reader::from_shared_context(&shared_context())
|
|
271
|
+
.with_manifest_data_and_stream(manifest, format, asset)
|
|
272
|
+
.map_err(|e| format!("Failed to read detached manifest: {}", e))?;
|
|
273
|
+
Ok(reader.json())
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
fn do_read_file_with_manifest(path: &str, manifest_path: &str) -> Result<String, BoxError> {
|
|
277
|
+
let manifest = std::fs::read(manifest_path)
|
|
278
|
+
.map_err(|e| format!("Failed to read manifest file '{}': {}", manifest_path, e))?;
|
|
279
|
+
let format = format_from_path(path)
|
|
280
|
+
.ok_or_else(|| format!("Failed to read '{}': cannot tell its format from the extension", path))?;
|
|
281
|
+
let asset = File::open(path).map_err(|e| format!("Failed to open '{}': {}", path, e))?;
|
|
282
|
+
do_read_detached(&manifest, &format, asset)
|
|
283
|
+
}
|
|
284
|
+
|
|
222
285
|
// ─── Running without the GVL ──────────────────────────────────────────────────
|
|
223
286
|
//
|
|
224
287
|
// Signing and reading are CPU-bound Rust with no need of the interpreter, so
|
|
@@ -290,6 +353,7 @@ fn sign_file(
|
|
|
290
353
|
manifest_json: Option<String>,
|
|
291
354
|
intent: Option<String>,
|
|
292
355
|
ingredient_files: Option<String>,
|
|
356
|
+
sidecar: Option<String>,
|
|
293
357
|
) -> Result<String, Error> {
|
|
294
358
|
let request = SigningRequest {
|
|
295
359
|
cert_path: &cert,
|
|
@@ -300,7 +364,11 @@ fn sign_file(
|
|
|
300
364
|
ingredient_files_json: ingredient_files.as_deref(),
|
|
301
365
|
};
|
|
302
366
|
|
|
303
|
-
without_gvl(||
|
|
367
|
+
without_gvl(|| match &sidecar {
|
|
368
|
+
Some(sidecar) => do_sign_file_detached(&source, &dest, sidecar, &request),
|
|
369
|
+
None => do_sign_file(&source, &dest, &request),
|
|
370
|
+
})
|
|
371
|
+
.map_err(runtime_error)?;
|
|
304
372
|
Ok(dest)
|
|
305
373
|
}
|
|
306
374
|
|
|
@@ -346,6 +414,18 @@ fn read_buffer(data: RString, format: Option<String>) -> Result<String, Error> {
|
|
|
346
414
|
without_gvl(|| do_read_buffer(&bytes, format)).map_err(runtime_error)
|
|
347
415
|
}
|
|
348
416
|
|
|
417
|
+
fn read_file_with_manifest(path: String, manifest_path: String) -> Result<String, Error> {
|
|
418
|
+
without_gvl(|| do_read_file_with_manifest(&path, &manifest_path)).map_err(runtime_error)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Unlike read_buffer, nothing is sniffed from the asset here, so the format
|
|
422
|
+
// is required.
|
|
423
|
+
fn read_buffer_with_manifest(data: RString, format: String, manifest: RString) -> Result<String, Error> {
|
|
424
|
+
let bytes = unsafe { data.as_slice() }.to_vec();
|
|
425
|
+
let manifest = unsafe { manifest.as_slice() }.to_vec();
|
|
426
|
+
without_gvl(|| do_read_detached(&manifest, &format, Cursor::new(bytes))).map_err(runtime_error)
|
|
427
|
+
}
|
|
428
|
+
|
|
349
429
|
fn configure(settings_json: String) -> Result<(), Error> {
|
|
350
430
|
do_configure(&settings_json).map_err(runtime_error)
|
|
351
431
|
}
|
|
@@ -361,10 +441,12 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
361
441
|
let c2pa = ruby.define_module("C2PA")?;
|
|
362
442
|
let native = c2pa.define_module("Native")?;
|
|
363
443
|
|
|
364
|
-
native.define_singleton_method("sign_file", function!(sign_file,
|
|
444
|
+
native.define_singleton_method("sign_file", function!(sign_file, 9))?;
|
|
365
445
|
native.define_singleton_method("sign_buffer", function!(sign_buffer, 8))?;
|
|
366
446
|
native.define_singleton_method("read_file", function!(read_file, 1))?;
|
|
367
447
|
native.define_singleton_method("read_buffer", function!(read_buffer, 2))?;
|
|
448
|
+
native.define_singleton_method("read_file_with_manifest", function!(read_file_with_manifest, 2))?;
|
|
449
|
+
native.define_singleton_method("read_buffer_with_manifest", function!(read_buffer_with_manifest, 3))?;
|
|
368
450
|
native.define_singleton_method("configure", function!(configure, 1))?;
|
|
369
451
|
native.define_singleton_method("sdk_version", function!(sdk_version, 0))?;
|
|
370
452
|
|
data/lib/c2pa/config.rb
CHANGED
|
@@ -38,6 +38,26 @@ module C2PA
|
|
|
38
38
|
# network requests.
|
|
39
39
|
attr_accessor :ocsp_fetch
|
|
40
40
|
|
|
41
|
+
# Whether network requests made while reading and validating (remote
|
|
42
|
+
# manifests, OCSP, timestamps) may follow HTTP redirects.
|
|
43
|
+
#
|
|
44
|
+
# c2pa-rs defaults this to true but refuses a redirect to an internal
|
|
45
|
+
# address, such as loopback, a private network or a cloud metadata
|
|
46
|
+
# endpoint, because the URL can come from untrusted content. false stops
|
|
47
|
+
# redirects altogether. Neither setting affects a URL that names an
|
|
48
|
+
# internal host directly.
|
|
49
|
+
attr_accessor :allow_redirects
|
|
50
|
+
|
|
51
|
+
# Whether signing an MP4, MOV or other BMFF file leaves its /free and /skip
|
|
52
|
+
# boxes out of the hash. c2pa-rs defaults this to true: those boxes are
|
|
53
|
+
# padding that tools commonly rewrite after signing, and the specification
|
|
54
|
+
# allows excluding them. Set false and any later change to them breaks the
|
|
55
|
+
# signature, like a change to the media itself.
|
|
56
|
+
#
|
|
57
|
+
# It affects signing only. The choice is recorded in the signed file, and
|
|
58
|
+
# readers follow the file whatever their own setting.
|
|
59
|
+
attr_accessor :exclude_free_and_skip_boxes
|
|
60
|
+
|
|
41
61
|
# Whether to embed a thumbnail of the asset in its manifest, and of each
|
|
42
62
|
# ingredient supplied as a file.
|
|
43
63
|
#
|
|
@@ -66,6 +86,8 @@ module C2PA
|
|
|
66
86
|
@verify_trust = nil
|
|
67
87
|
@remote_manifest_fetch = nil
|
|
68
88
|
@ocsp_fetch = nil
|
|
89
|
+
@allow_redirects = nil
|
|
90
|
+
@exclude_free_and_skip_boxes = nil
|
|
69
91
|
@thumbnails = false
|
|
70
92
|
@thumbnail_size = nil
|
|
71
93
|
@thumbnail_format = nil
|
|
@@ -84,7 +106,9 @@ module C2PA
|
|
|
84
106
|
trust["trust_anchors"] = read_pem(@trust_list) unless @trust_list.nil?
|
|
85
107
|
trust["allowed_list"] = read_pem(@allowed_certificates) unless @allowed_certificates.nil?
|
|
86
108
|
|
|
87
|
-
|
|
109
|
+
# verify_after_sign is always sent: C2PA.sign runs its own check on the
|
|
110
|
+
# finished output, so c2pa-rs's pre-write check stays off.
|
|
111
|
+
verify = { "verify_after_sign" => false }
|
|
88
112
|
verify["verify_trust"] = @verify_trust unless @verify_trust.nil?
|
|
89
113
|
verify["remote_manifest_fetch"] = @remote_manifest_fetch unless @remote_manifest_fetch.nil?
|
|
90
114
|
verify["ocsp_fetch"] = @ocsp_fetch unless @ocsp_fetch.nil?
|
|
@@ -96,9 +120,15 @@ module C2PA
|
|
|
96
120
|
thumbnail["format"] = @thumbnail_format.to_s.downcase unless @thumbnail_format.nil?
|
|
97
121
|
thumbnail["quality"] = @thumbnail_quality.to_s.downcase unless @thumbnail_quality.nil?
|
|
98
122
|
|
|
99
|
-
|
|
123
|
+
builder = { "thumbnail" => thumbnail }
|
|
124
|
+
unless @exclude_free_and_skip_boxes.nil?
|
|
125
|
+
builder["bmff_hash_exclude_free_and_skip_boxes"] = @exclude_free_and_skip_boxes
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
settings = { "builder" => builder }
|
|
129
|
+
settings["core"] = { "allow_redirects" => @allow_redirects } unless @allow_redirects.nil?
|
|
100
130
|
settings["trust"] = trust unless trust.empty?
|
|
101
|
-
settings["verify"] = verify
|
|
131
|
+
settings["verify"] = verify
|
|
102
132
|
|
|
103
133
|
JSON.generate(settings)
|
|
104
134
|
end
|
data/lib/c2pa/manifest.rb
CHANGED
|
@@ -75,6 +75,17 @@ module C2PA
|
|
|
75
75
|
"intent: :edit to C2PA::Manifest.new instead and the action will be added for you."
|
|
76
76
|
end
|
|
77
77
|
|
|
78
|
+
# Same limit as c2pa.opened: each entry is a hashed URI that only c2pa-rs
|
|
79
|
+
# can compute, and in 0.91 its builder does not compute them, only
|
|
80
|
+
# checks them when reading. A hand-written hash would pass that check
|
|
81
|
+
# today while claiming a binding nothing verified.
|
|
82
|
+
if parameters.is_a?(Hash) && (parameters.key?("relatedAssertions") || parameters.key?(:relatedAssertions))
|
|
83
|
+
raise InvalidManifestError,
|
|
84
|
+
"relatedAssertions cannot be set. Each entry must be a hashed URI computed over " \
|
|
85
|
+
"the assertion as c2pa-rs serialises it, and c2pa-rs #{C2PA.sdk_version} does not " \
|
|
86
|
+
"compute them when building a manifest, so Ruby cannot produce a genuine one."
|
|
87
|
+
end
|
|
88
|
+
|
|
78
89
|
# Required as of c2pa-rs 0.90. Earlier versions accepted its absence, so
|
|
79
90
|
# manifests signed by releases before 0.3.0 are rejected by current
|
|
80
91
|
# verifiers. No default is supplied: c2pa-rs accepts any string here, so
|
|
@@ -126,10 +137,9 @@ module C2PA
|
|
|
126
137
|
# at it, so provenance chains from the original through to this asset. A
|
|
127
138
|
# verifier can then follow and check the whole history.
|
|
128
139
|
#
|
|
129
|
-
# For a file with no credentials there is
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
# feature, which this gem does not enable.)
|
|
140
|
+
# For a file with no credentials there is no manifest to carry forward.
|
|
141
|
+
# The file still contributes a thumbnail when C2PA::Config#thumbnails is
|
|
142
|
+
# on.
|
|
133
143
|
#
|
|
134
144
|
# Without `file:`, only the description is recorded. Nothing binds it to
|
|
135
145
|
# any actual bytes. This form is kept for compatibility.
|
|
@@ -139,15 +149,23 @@ module C2PA
|
|
|
139
149
|
# @param instance_id [String] unique identifier for the ingredient instance
|
|
140
150
|
# @param relationship [String] relationship to this asset; defaults to "parentOf"
|
|
141
151
|
# @param file [String, nil] path to the ingredient file
|
|
152
|
+
# @param digital_source_type [String, nil] URI from the C2PA digitalSourceType
|
|
153
|
+
# vocabulary, for an ingredient with no content credentials of its own,
|
|
154
|
+
# such as a generated image composited into this asset
|
|
142
155
|
# @return [self]
|
|
143
|
-
# @raise [C2PA::InvalidManifestError] if `file` is given but cannot be read
|
|
144
|
-
|
|
156
|
+
# @raise [C2PA::InvalidManifestError] if `file` is given but cannot be read,
|
|
157
|
+
# or if `digital_source_type` is given for a file that carries content
|
|
158
|
+
# credentials
|
|
159
|
+
def add_ingredient(title:, format:, instance_id:, relationship: "parentOf", file: nil,
|
|
160
|
+
digital_source_type: nil)
|
|
145
161
|
description = {
|
|
146
162
|
"title" => title,
|
|
147
163
|
"format" => format,
|
|
148
164
|
"instance_id" => instance_id,
|
|
149
165
|
"relationship" => relationship
|
|
150
166
|
}
|
|
167
|
+
digital_source_type = to_s_or_nil(digital_source_type)
|
|
168
|
+
description["digital_source_type"] = digital_source_type if digital_source_type
|
|
151
169
|
|
|
152
170
|
if file.nil?
|
|
153
171
|
@ingredients << description
|
|
@@ -158,6 +176,16 @@ module C2PA
|
|
|
158
176
|
raise InvalidManifestError, "ingredient file not readable: #{file.inspect}"
|
|
159
177
|
end
|
|
160
178
|
|
|
179
|
+
# The specification forbids a source type alongside an embedded manifest:
|
|
180
|
+
# an ingredient with credentials already records its own origin. c2pa-rs
|
|
181
|
+
# refuses the combination only at signing, as "unable to encode assertion
|
|
182
|
+
# data", so it is caught here where the cause is clear.
|
|
183
|
+
if digital_source_type && content_credentials?(file)
|
|
184
|
+
raise InvalidManifestError,
|
|
185
|
+
"digital_source_type cannot be set for #{file.inspect}: it carries content " \
|
|
186
|
+
"credentials, which already record where it came from"
|
|
187
|
+
end
|
|
188
|
+
|
|
161
189
|
@ingredient_files << {
|
|
162
190
|
"json" => JSON.generate(description),
|
|
163
191
|
"format" => format,
|
|
@@ -198,6 +226,14 @@ module C2PA
|
|
|
198
226
|
|
|
199
227
|
private
|
|
200
228
|
|
|
229
|
+
# A file with no manifest, or one c2pa-rs cannot read, counts as having no
|
|
230
|
+
# credentials; if it does carry some, signing still fails upstream.
|
|
231
|
+
def content_credentials?(file)
|
|
232
|
+
!C2PA.read(file: file)["active_manifest"].nil?
|
|
233
|
+
rescue ReadError
|
|
234
|
+
false
|
|
235
|
+
end
|
|
236
|
+
|
|
201
237
|
# Parameters may be keyed with strings or symbols depending on the caller.
|
|
202
238
|
def param_present?(parameters, key)
|
|
203
239
|
return false unless parameters.is_a?(Hash)
|
data/lib/c2pa/version.rb
CHANGED
data/lib/c2pa.rb
CHANGED
|
@@ -58,6 +58,12 @@ module C2PA
|
|
|
58
58
|
# @param manifest [C2PA::Manifest] the manifest to embed
|
|
59
59
|
# @param verify [Boolean] read the signed file back and confirm it
|
|
60
60
|
# validates (default: true)
|
|
61
|
+
# @param sidecar [String, nil] write the manifest to this path as a
|
|
62
|
+
# .c2pa file instead of embedding it (must
|
|
63
|
+
# not already exist). The output is the
|
|
64
|
+
# asset as hashed, usually identical to
|
|
65
|
+
# the input, and is only valid alongside
|
|
66
|
+
# the sidecar.
|
|
61
67
|
# @return [String] the output path
|
|
62
68
|
# @raise [C2PA::SigningError] if signing fails, or if the signed file does not validate
|
|
63
69
|
#
|
|
@@ -72,13 +78,17 @@ module C2PA
|
|
|
72
78
|
# key: "key.pem",
|
|
73
79
|
# manifest: manifest
|
|
74
80
|
# )
|
|
75
|
-
def self.sign(file:, output:, certificate:, key:, algorithm: "es256", manifest:, verify: true)
|
|
81
|
+
def self.sign(file:, output:, certificate:, key:, algorithm: "es256", manifest:, verify: true, sidecar: nil)
|
|
76
82
|
manifest_json = manifest.to_json
|
|
77
83
|
|
|
78
84
|
raise SigningError, "Source file not found: '#{file}'" unless File.exist?(file)
|
|
79
85
|
raise SigningError, "Certificate file not found: '#{certificate}'" unless File.exist?(certificate)
|
|
80
86
|
raise SigningError, "Key file not found: '#{key}'" unless File.exist?(key)
|
|
81
87
|
raise SigningError, "Output file already exists: '#{output}'" if File.exist?(output)
|
|
88
|
+
raise SigningError, "Sidecar file already exists: '#{sidecar}'" if sidecar && File.exist?(sidecar)
|
|
89
|
+
if sidecar && File.expand_path(sidecar) == File.expand_path(output)
|
|
90
|
+
raise SigningError, "sidecar and output must be different paths"
|
|
91
|
+
end
|
|
82
92
|
|
|
83
93
|
begin
|
|
84
94
|
# to_json is the only thing genuinely required of a manifest, so an
|
|
@@ -87,12 +97,12 @@ module C2PA
|
|
|
87
97
|
files = manifest.respond_to?(:ingredient_files) ? manifest.ingredient_files : []
|
|
88
98
|
ingredient_files = files.empty? ? nil : JSON.generate(files)
|
|
89
99
|
Native.sign_file(file, output, certificate, key, algorithm, manifest_json,
|
|
90
|
-
intent, ingredient_files)
|
|
100
|
+
intent, ingredient_files, sidecar)
|
|
91
101
|
rescue RuntimeError => e
|
|
92
102
|
raise SigningError, e.message
|
|
93
103
|
end
|
|
94
104
|
|
|
95
|
-
verify_signed_output!(output) if verify
|
|
105
|
+
verify_signed_output!(output, sidecar) if verify
|
|
96
106
|
|
|
97
107
|
output
|
|
98
108
|
end
|
|
@@ -152,32 +162,54 @@ module C2PA
|
|
|
152
162
|
# Read the C2PA manifest embedded in bytes held in memory.
|
|
153
163
|
#
|
|
154
164
|
# c2pa-rs identifies most formats from the leading bytes and ignores the
|
|
155
|
-
# hint when the two disagree. The hint matters for formats
|
|
156
|
-
#
|
|
165
|
+
# hint when the two disagree. The hint matters for formats it cannot tell
|
|
166
|
+
# apart by their bytes: SVG, and the ZIP-based documents (EPUB, DOCX, ODT,
|
|
167
|
+
# OpenXPS), which all begin with the same ZIP header.
|
|
168
|
+
#
|
|
169
|
+
# With manifest_data, the manifest store is taken from there, as the
|
|
170
|
+
# contents of a .c2pa sidecar, and validated against data. The format is
|
|
171
|
+
# then required, since nothing is sniffed.
|
|
157
172
|
#
|
|
158
|
-
# @param data
|
|
159
|
-
# @param format
|
|
160
|
-
# @
|
|
161
|
-
# @
|
|
162
|
-
|
|
173
|
+
# @param data [String] the asset, as a binary string
|
|
174
|
+
# @param format [String, nil] MIME type or extension, e.g. "image/svg+xml"
|
|
175
|
+
# @param manifest_data [String, nil] a detached manifest store, as a binary string
|
|
176
|
+
# @return [Hash] parsed manifest JSON
|
|
177
|
+
# @raise [C2PA::ReadError] if the data has no valid manifest
|
|
178
|
+
def self.read_buffer(data:, format: nil, manifest_data: nil)
|
|
163
179
|
data = binary!(data, "data")
|
|
164
|
-
JSON.parse(Native.read_buffer(data, format))
|
|
180
|
+
return JSON.parse(Native.read_buffer(data, format)) if manifest_data.nil?
|
|
181
|
+
|
|
182
|
+
manifest_data = binary!(manifest_data, "manifest_data")
|
|
183
|
+
raise ArgumentError, "format is required with manifest_data" if format.nil?
|
|
184
|
+
|
|
185
|
+
JSON.parse(Native.read_buffer_with_manifest(data, format, manifest_data))
|
|
165
186
|
rescue RuntimeError => e
|
|
166
187
|
raise ReadError, e.message
|
|
167
188
|
end
|
|
168
189
|
|
|
169
190
|
# Read the C2PA manifest embedded in a signed file.
|
|
170
191
|
#
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
#
|
|
192
|
+
# A file with no embedded manifest is read against a sidecar beside it with
|
|
193
|
+
# the same name and a .c2pa extension, if there is one: photo.c2pa for
|
|
194
|
+
# photo.jpg. Pass manifest_file for a sidecar kept anywhere else.
|
|
195
|
+
#
|
|
196
|
+
# Reading a .c2pa file on its own returns the manifest, but with nothing to
|
|
197
|
+
# check it against its hash never matches and the state is Invalid.
|
|
198
|
+
#
|
|
199
|
+
# @param file [String] path to the signed file
|
|
200
|
+
# @param manifest_file [String, nil] path to a detached manifest (.c2pa)
|
|
201
|
+
# to validate the file against
|
|
202
|
+
# @return [Hash] parsed manifest JSON
|
|
203
|
+
# @raise [C2PA::ReadError] if the file has no valid manifest
|
|
174
204
|
#
|
|
175
205
|
# @example
|
|
176
206
|
# manifest = C2PA.read(file: "photo_signed.jpg")
|
|
177
207
|
# active = manifest["manifests"][manifest["active_manifest"]]
|
|
178
208
|
# puts active["title"]
|
|
179
|
-
def self.read(file:)
|
|
180
|
-
JSON.parse(Native.read_file(file))
|
|
209
|
+
def self.read(file:, manifest_file: nil)
|
|
210
|
+
return JSON.parse(Native.read_file(file)) if manifest_file.nil?
|
|
211
|
+
|
|
212
|
+
JSON.parse(Native.read_file_with_manifest(file, manifest_file))
|
|
181
213
|
rescue RuntimeError => e
|
|
182
214
|
raise ReadError, e.message
|
|
183
215
|
end
|
|
@@ -201,12 +233,13 @@ module C2PA
|
|
|
201
233
|
#
|
|
202
234
|
# @param output [String] path to the signed file
|
|
203
235
|
# @raise [C2PA::SigningError] if the file does not validate
|
|
204
|
-
def self.verify_signed_output!(output)
|
|
236
|
+
def self.verify_signed_output!(output, sidecar = nil)
|
|
205
237
|
result =
|
|
206
238
|
begin
|
|
207
|
-
read(file: output)
|
|
239
|
+
read(file: output, manifest_file: sidecar)
|
|
208
240
|
rescue ReadError => e
|
|
209
241
|
discard(output)
|
|
242
|
+
discard(sidecar) if sidecar
|
|
210
243
|
raise SigningError, "signed file failed verification: could not read it back: #{e.message}"
|
|
211
244
|
end
|
|
212
245
|
|
|
@@ -218,6 +251,7 @@ module C2PA
|
|
|
218
251
|
detail = failures.empty? ? "no failure detail reported" : failures.uniq.join(", ")
|
|
219
252
|
|
|
220
253
|
discard(output)
|
|
254
|
+
discard(sidecar) if sidecar
|
|
221
255
|
raise SigningError,
|
|
222
256
|
"signed file failed verification: validation_state=#{state.inspect}, #{detail}. " \
|
|
223
257
|
"The output file has been removed. Pass verify: false to keep it for inspection."
|