ruby-c2pa 0.3.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.
data/lib/c2pa.rb CHANGED
@@ -3,6 +3,7 @@ require_relative "c2pa/version"
3
3
  require_relative "c2pa/error"
4
4
  require_relative "c2pa/actions"
5
5
  require_relative "c2pa/digital_source_types"
6
+ require_relative "c2pa/config"
6
7
  require_relative "c2pa/manifest"
7
8
  require "c2pa/c2pa_native"
8
9
 
@@ -14,6 +15,39 @@ module C2PA
14
15
  # production files that are most correct.
15
16
  VALID_STATES = %w[Valid Trusted].freeze
16
17
 
18
+ # Configure how c2pa-rs validates.
19
+ #
20
+ # Settings are global and take effect for subsequent calls. Signing already
21
+ # in flight on another thread continues against the settings it started
22
+ # with, since the underlying context is replaced rather than mutated.
23
+ #
24
+ # @yieldparam config [C2PA::Config]
25
+ # @return [C2PA::Config] the configuration that was applied
26
+ # @raise [C2PA::InvalidSettingsError] if the settings are not usable
27
+ #
28
+ # @example Trusting a private CA
29
+ # C2PA.configure do |config|
30
+ # config.trust_anchors = "ca/root.pem"
31
+ # end
32
+ #
33
+ # @example An offline environment
34
+ # C2PA.configure do |config|
35
+ # config.remote_manifest_fetch = false
36
+ # config.ocsp_fetch = false
37
+ # end
38
+ def self.configure
39
+ config = Config.new
40
+ yield config if block_given?
41
+
42
+ begin
43
+ Native.configure(config.to_json)
44
+ rescue RuntimeError => e
45
+ raise InvalidSettingsError, e.message
46
+ end
47
+
48
+ config
49
+ end
50
+
17
51
  # Sign a file with a C2PA manifest.
18
52
  #
19
53
  # @param file [String] path to the input file
@@ -50,7 +84,10 @@ module C2PA
50
84
  # to_json is the only thing genuinely required of a manifest, so an
51
85
  # object that provides just that still signs — as a creation.
52
86
  intent = manifest.respond_to?(:intent) ? manifest.intent&.to_s : nil
53
- Native.sign_file(file, output, certificate, key, algorithm, manifest_json, intent)
87
+ files = manifest.respond_to?(:ingredient_files) ? manifest.ingredient_files : []
88
+ ingredient_files = files.empty? ? nil : JSON.generate(files)
89
+ Native.sign_file(file, output, certificate, key, algorithm, manifest_json,
90
+ intent, ingredient_files)
54
91
  rescue RuntimeError => e
55
92
  raise SigningError, e.message
56
93
  end
@@ -60,6 +97,75 @@ module C2PA
60
97
  output
61
98
  end
62
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
+
63
169
  # Read the C2PA manifest embedded in a signed file.
64
170
  #
65
171
  # @param file [String] path to the signed file
@@ -118,6 +224,46 @@ module C2PA
118
224
  end
119
225
  private_class_method :verify_signed_output!
120
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
+
121
267
  # Remove a file this library created and is about to reject.
122
268
  def self.discard(path)
123
269
  File.delete(path) if File.exist?(path)
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.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Carlos Rodriguez
@@ -85,6 +85,7 @@ files:
85
85
  - ext/c2pa_native/src/lib.rs
86
86
  - lib/c2pa.rb
87
87
  - lib/c2pa/actions.rb
88
+ - lib/c2pa/config.rb
88
89
  - lib/c2pa/digital_source_types.rb
89
90
  - lib/c2pa/error.rb
90
91
  - lib/c2pa/manifest.rb