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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +87 -1
- data/CONTRIBUTING.md +19 -2
- data/README.md +236 -28
- data/ext/c2pa_native/Cargo.lock +898 -666
- data/ext/c2pa_native/Cargo.toml +4 -1
- data/ext/c2pa_native/src/lib.rs +276 -30
- data/lib/c2pa/config.rb +122 -0
- data/lib/c2pa/error.rb +3 -0
- data/lib/c2pa/manifest.rb +50 -14
- data/lib/c2pa/version.rb +1 -1
- data/lib/c2pa.rb +147 -1
- metadata +2 -1
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
|
-
|
|
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.
|
|
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
|