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.
@@ -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 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}}}"#;
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(|| do_sign_file(&source, &dest, &request)).map_err(runtime_error)?;
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, 8))?;
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
- verify = {}
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
- settings = { "builder" => { "thumbnail" => thumbnail } }
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 unless verify.empty?
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 nothing to carry forward, and
130
- # the result is the same as the description alone. (Thumbnails would be
131
- # the other contribution, but they need c2pa-rs's `add_thumbnails`
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
- def add_ingredient(title:, format:, instance_id:, relationship: "parentOf", file: nil)
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
@@ -1,3 +1,3 @@
1
1
  module C2PA
2
- VERSION = "0.5.0"
2
+ VERSION = "0.6.0"
3
3
  end
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 with no signature
156
- # to sniff, such as SVG, which cannot be read without it.
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 [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)
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
- # @param file [String] path to the signed file
172
- # @return [Hash] parsed manifest JSON
173
- # @raise [C2PA::ReadError] if the file has no valid manifest
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."
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-c2pa
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Rodriguez