ruby-c2pa 0.2.1 → 0.3.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 +105 -0
- data/CONTRIBUTING.md +117 -0
- data/README.md +154 -8
- data/Rakefile +29 -1
- data/ext/c2pa_native/Cargo.lock +4128 -0
- data/ext/c2pa_native/Cargo.toml +1 -1
- data/ext/c2pa_native/src/lib.rs +25 -3
- data/lib/c2pa/digital_source_types.rb +72 -0
- data/lib/c2pa/manifest.rb +115 -4
- data/lib/c2pa/version.rb +1 -1
- data/lib/c2pa.rb +68 -4
- data/ruby-c2pa.gemspec +28 -3
- metadata +10 -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
data/ext/c2pa_native/Cargo.toml
CHANGED
data/ext/c2pa_native/src/lib.rs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
use std::path::Path;
|
|
2
|
-
use c2pa::{create_signer, Builder, Reader, SigningAlg};
|
|
2
|
+
use c2pa::{create_signer, Builder, BuilderIntent, Reader, SigningAlg};
|
|
3
3
|
use magnus::{function, prelude::*, Error, Ruby};
|
|
4
4
|
|
|
5
5
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
@@ -22,6 +22,22 @@ fn alg_from_str(alg: &str) -> Result<SigningAlg, String> {
|
|
|
22
22
|
|
|
23
23
|
// ─── Core logic ──────────────────────────────────────────────────────────────
|
|
24
24
|
|
|
25
|
+
// A c2pa.opened action has to reference its parent ingredient by hashed URI,
|
|
26
|
+
// and that hash is computed over the ingredient assertion as c2pa-rs
|
|
27
|
+
// serialises it. Ruby cannot construct one. Declaring the intent instead lets
|
|
28
|
+
// the SDK derive the parent ingredient from the source and wire the action to
|
|
29
|
+
// it, which is the only way the edit workflow can be expressed.
|
|
30
|
+
fn intent_from_str(intent: &str) -> Result<BuilderIntent, String> {
|
|
31
|
+
match intent.to_lowercase().as_str() {
|
|
32
|
+
"edit" => Ok(BuilderIntent::Edit),
|
|
33
|
+
"update" => Ok(BuilderIntent::Update),
|
|
34
|
+
_ => Err(format!(
|
|
35
|
+
"Unknown intent: '{}'. Valid options: edit, update",
|
|
36
|
+
intent
|
|
37
|
+
)),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
25
41
|
fn do_sign_file(
|
|
26
42
|
source_path: &str,
|
|
27
43
|
dest_path: &str,
|
|
@@ -29,6 +45,7 @@ fn do_sign_file(
|
|
|
29
45
|
key_path: &str,
|
|
30
46
|
alg_str: &str,
|
|
31
47
|
manifest_json: Option<&str>,
|
|
48
|
+
intent_str: Option<&str>,
|
|
32
49
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
33
50
|
let cert = std::fs::read(cert_path)
|
|
34
51
|
.map_err(|e| format!("Cannot read certificate '{}': {}", cert_path, e))?;
|
|
@@ -50,6 +67,10 @@ fn do_sign_file(
|
|
|
50
67
|
let mut builder = Builder::from_json(json)
|
|
51
68
|
.map_err(|e| format!("Invalid manifest JSON: {}", e))?;
|
|
52
69
|
|
|
70
|
+
if let Some(intent) = intent_str {
|
|
71
|
+
builder.set_intent(intent_from_str(intent)?);
|
|
72
|
+
}
|
|
73
|
+
|
|
53
74
|
builder.sign_file(&*signer, source_path, dest_path)
|
|
54
75
|
.map_err(|e| format!("Signing failed: {}", e))?;
|
|
55
76
|
|
|
@@ -71,10 +92,11 @@ fn sign_file(
|
|
|
71
92
|
key: String,
|
|
72
93
|
alg: Option<String>,
|
|
73
94
|
manifest_json: Option<String>,
|
|
95
|
+
intent: Option<String>,
|
|
74
96
|
) -> Result<String, Error> {
|
|
75
97
|
let alg_str = alg.as_deref().unwrap_or("es256");
|
|
76
98
|
|
|
77
|
-
do_sign_file(&source, &dest, &cert, &key, alg_str, manifest_json.as_deref())
|
|
99
|
+
do_sign_file(&source, &dest, &cert, &key, alg_str, manifest_json.as_deref(), intent.as_deref())
|
|
78
100
|
.map_err(|e| Error::new(Ruby::get().expect("called from Ruby thread").exception_runtime_error(), e.to_string()))?;
|
|
79
101
|
|
|
80
102
|
Ok(dest)
|
|
@@ -96,7 +118,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
96
118
|
let c2pa = ruby.define_module("C2PA")?;
|
|
97
119
|
let native = c2pa.define_module("Native")?;
|
|
98
120
|
|
|
99
|
-
native.define_singleton_method("sign_file", function!(sign_file,
|
|
121
|
+
native.define_singleton_method("sign_file", function!(sign_file, 7))?;
|
|
100
122
|
native.define_singleton_method("read_file", function!(read_file, 1))?;
|
|
101
123
|
native.define_singleton_method("sdk_version", function!(sdk_version, 0))?;
|
|
102
124
|
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module C2PA
|
|
2
|
+
# URIs from the IPTC Digital Source Type NewsCodes vocabulary.
|
|
3
|
+
#
|
|
4
|
+
# The C2PA specification requires every c2pa.created action to declare one of
|
|
5
|
+
# these, so consumers can tell how the asset came into being. Pick the one that
|
|
6
|
+
# honestly describes the asset — this is a provenance claim, and c2pa-rs will
|
|
7
|
+
# not second-guess the value you supply.
|
|
8
|
+
#
|
|
9
|
+
# @see https://cv.iptc.org/newscodes/digitalsourcetype/
|
|
10
|
+
module DigitalSourceTypes
|
|
11
|
+
BASE = "http://cv.iptc.org/newscodes/digitalsourcetype"
|
|
12
|
+
|
|
13
|
+
# Declining to claim a source type, rather than guessing at one.
|
|
14
|
+
#
|
|
15
|
+
# c2pa-rs requires c2pa.created to carry a digitalSourceType but accepts
|
|
16
|
+
# any string, so a wrong value validates silently. When the origin is not
|
|
17
|
+
# known — a generic signing service, say — this says so, instead of
|
|
18
|
+
# asserting something untrue. It is the value c2pa-rs uses throughout its
|
|
19
|
+
# own fixtures.
|
|
20
|
+
UNSPECIFIED = "http://c2pa.org/digitalsourcetype/empty"
|
|
21
|
+
|
|
22
|
+
# Captured from real life
|
|
23
|
+
DIGITAL_CAPTURE = "#{BASE}/digitalCapture"
|
|
24
|
+
COMPUTATIONAL_CAPTURE = "#{BASE}/computationalCapture"
|
|
25
|
+
SCREEN_CAPTURE = "#{BASE}/screenCapture"
|
|
26
|
+
VIRTUAL_RECORDING = "#{BASE}/virtualRecording"
|
|
27
|
+
|
|
28
|
+
# Digitised from a physical medium
|
|
29
|
+
NEGATIVE_FILM = "#{BASE}/negativeFilm"
|
|
30
|
+
POSITIVE_FILM = "#{BASE}/positiveFilm"
|
|
31
|
+
PRINT = "#{BASE}/print"
|
|
32
|
+
|
|
33
|
+
# Human- or software-authored
|
|
34
|
+
HUMAN_EDITS = "#{BASE}/humanEdits"
|
|
35
|
+
DIGITAL_CREATION = "#{BASE}/digitalCreation"
|
|
36
|
+
ALGORITHMICALLY_ENHANCED = "#{BASE}/algorithmicallyEnhanced"
|
|
37
|
+
DATA_DRIVEN_MEDIA = "#{BASE}/dataDrivenMedia"
|
|
38
|
+
ALGORITHMIC_MEDIA = "#{BASE}/algorithmicMedia"
|
|
39
|
+
|
|
40
|
+
# Generative AI
|
|
41
|
+
TRAINED_ALGORITHMIC_MEDIA = "#{BASE}/trainedAlgorithmicMedia"
|
|
42
|
+
COMPOSITE_WITH_TRAINED_ALGORITHMIC_MEDIA = "#{BASE}/compositeWithTrainedAlgorithmicMedia"
|
|
43
|
+
COMPOSITE_SYNTHETIC = "#{BASE}/compositeSynthetic"
|
|
44
|
+
|
|
45
|
+
# Composites
|
|
46
|
+
COMPOSITE = "#{BASE}/composite"
|
|
47
|
+
COMPOSITE_CAPTURE = "#{BASE}/compositeCapture"
|
|
48
|
+
|
|
49
|
+
# Every type defined above. Supplying a URI outside this list is allowed —
|
|
50
|
+
# the vocabulary is extensible — but the values here cover the standard set.
|
|
51
|
+
ALL = [
|
|
52
|
+
UNSPECIFIED,
|
|
53
|
+
DIGITAL_CAPTURE,
|
|
54
|
+
COMPUTATIONAL_CAPTURE,
|
|
55
|
+
SCREEN_CAPTURE,
|
|
56
|
+
VIRTUAL_RECORDING,
|
|
57
|
+
NEGATIVE_FILM,
|
|
58
|
+
POSITIVE_FILM,
|
|
59
|
+
PRINT,
|
|
60
|
+
HUMAN_EDITS,
|
|
61
|
+
DIGITAL_CREATION,
|
|
62
|
+
ALGORITHMICALLY_ENHANCED,
|
|
63
|
+
DATA_DRIVEN_MEDIA,
|
|
64
|
+
ALGORITHMIC_MEDIA,
|
|
65
|
+
TRAINED_ALGORITHMIC_MEDIA,
|
|
66
|
+
COMPOSITE_WITH_TRAINED_ALGORITHMIC_MEDIA,
|
|
67
|
+
COMPOSITE_SYNTHETIC,
|
|
68
|
+
COMPOSITE,
|
|
69
|
+
COMPOSITE_CAPTURE
|
|
70
|
+
].freeze
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/c2pa/manifest.rb
CHANGED
|
@@ -2,9 +2,44 @@ require "json"
|
|
|
2
2
|
|
|
3
3
|
module C2PA
|
|
4
4
|
class Manifest
|
|
5
|
-
#
|
|
6
|
-
|
|
5
|
+
# Intents this gem can express.
|
|
6
|
+
#
|
|
7
|
+
# :edit — this asset derives from a parent. c2pa-rs generates the parent
|
|
8
|
+
# ingredient from the source file and adds a c2pa.opened action
|
|
9
|
+
# wired to it by hashed URI.
|
|
10
|
+
#
|
|
11
|
+
# Omitting the intent produces a manifest for a newly created asset.
|
|
12
|
+
#
|
|
13
|
+
# c2pa-rs also has an :update intent, a restricted edit for non-editorial
|
|
14
|
+
# changes. It is not offered here because it requires an ingredient with
|
|
15
|
+
# real content, and add_ingredient records metadata only — signing with it
|
|
16
|
+
# fails with "ingredient file not found". Tracked separately.
|
|
17
|
+
INTENTS = %i[edit].freeze
|
|
18
|
+
|
|
19
|
+
# c2pa-rs records itself in a namespaced field alongside the generator
|
|
20
|
+
# name, so the gem does the same when an application supplies its own.
|
|
21
|
+
GEM_FIELD = "org.rubygems.ruby_c2pa".freeze
|
|
22
|
+
|
|
23
|
+
# @return [Symbol, nil] the builder intent, if any
|
|
24
|
+
attr_reader :intent
|
|
25
|
+
|
|
26
|
+
# @param title [String] human-readable title for this asset
|
|
27
|
+
# @param intent [Symbol, nil] :edit; omit for a new creation
|
|
28
|
+
# @param generator_name [String, nil] the application doing the signing.
|
|
29
|
+
# Defaults to this gem. Supplying it credits your application as the
|
|
30
|
+
# claim generator, with the gem recorded alongside.
|
|
31
|
+
# @param generator_version [String, nil] version of that application
|
|
32
|
+
# @raise [C2PA::InvalidManifestError] if the intent is not recognised
|
|
33
|
+
def initialize(title:, intent: nil, generator_name: nil, generator_version: nil)
|
|
34
|
+
unless intent.nil? || INTENTS.include?(intent)
|
|
35
|
+
raise InvalidManifestError,
|
|
36
|
+
"unknown intent #{intent.inspect}. Valid options: #{INTENTS.map(&:inspect).join(', ')}"
|
|
37
|
+
end
|
|
38
|
+
|
|
7
39
|
@title = title
|
|
40
|
+
@intent = intent
|
|
41
|
+
@generator_name = generator_name
|
|
42
|
+
@generator_version = generator_version
|
|
8
43
|
@actions = []
|
|
9
44
|
@assertions = []
|
|
10
45
|
@ingredients = []
|
|
@@ -26,6 +61,38 @@ module C2PA
|
|
|
26
61
|
digital_source_type: nil,
|
|
27
62
|
changed: nil,
|
|
28
63
|
parameters: nil)
|
|
64
|
+
if action == Actions::OPENED
|
|
65
|
+
raise InvalidManifestError,
|
|
66
|
+
"#{Actions::OPENED} cannot be added directly. The specification requires it to " \
|
|
67
|
+
"reference a parentOf ingredient by hashed URI, and that hash is computed over " \
|
|
68
|
+
"the ingredient as c2pa-rs serialises it, so Ruby cannot construct one. Pass " \
|
|
69
|
+
"intent: :edit to C2PA::Manifest.new instead and the action will be added for you."
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Required as of c2pa-rs 0.90. Earlier versions accepted its absence, so
|
|
73
|
+
# manifests signed by releases before 0.3.0 are rejected by current
|
|
74
|
+
# verifiers. No default is supplied: c2pa-rs accepts any string here, so
|
|
75
|
+
# a guess would validate while asserting something untrue about where the
|
|
76
|
+
# asset came from. Use DigitalSourceTypes::UNSPECIFIED to decline.
|
|
77
|
+
if action == Actions::CREATED && to_s_or_nil(digital_source_type).nil?
|
|
78
|
+
raise InvalidManifestError,
|
|
79
|
+
"#{Actions::CREATED} requires a digital_source_type. Choose the value that " \
|
|
80
|
+
"describes how the asset was produced — for example " \
|
|
81
|
+
"C2PA::DigitalSourceTypes::DIGITAL_CAPTURE for a camera original, or " \
|
|
82
|
+
"TRAINED_ALGORITHMIC_MEDIA for generative AI. If the origin is genuinely " \
|
|
83
|
+
"unknown, use C2PA::DigitalSourceTypes::UNSPECIFIED rather than guessing."
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Also new in c2pa-rs 0.90.
|
|
87
|
+
if action == Actions::TRANSLATED
|
|
88
|
+
missing = %w[sourceLanguage targetLanguage].reject { |key| param_present?(parameters, key) }
|
|
89
|
+
unless missing.empty?
|
|
90
|
+
raise InvalidManifestError,
|
|
91
|
+
"#{Actions::TRANSLATED} requires #{missing.join(' and ')} in parameters, " \
|
|
92
|
+
"as RFC 5646 language codes"
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
29
96
|
entry = { "action" => action }
|
|
30
97
|
entry["when"] = when_time if when_time
|
|
31
98
|
entry["softwareAgent"] = software_agent || "ruby-c2pa/#{VERSION}"
|
|
@@ -66,12 +133,14 @@ module C2PA
|
|
|
66
133
|
# Serialize to the JSON structure expected by c2pa-rs.
|
|
67
134
|
#
|
|
68
135
|
# @return [String]
|
|
69
|
-
# @raise [C2PA::InvalidManifestError] if no actions have been added
|
|
136
|
+
# @raise [C2PA::InvalidManifestError] if no actions have been added, or if
|
|
137
|
+
# any value cannot be represented as JSON
|
|
70
138
|
def to_json
|
|
71
139
|
raise InvalidManifestError, "at least one action is required" if @actions.empty?
|
|
72
140
|
|
|
73
141
|
manifest = {
|
|
74
142
|
"title" => @title,
|
|
143
|
+
"claim_generator_info" => [claim_generator_info],
|
|
75
144
|
"assertions" => [
|
|
76
145
|
{ "label" => "c2pa.actions.v2", "data" => { "actions" => @actions } },
|
|
77
146
|
*@assertions
|
|
@@ -79,7 +148,49 @@ module C2PA
|
|
|
79
148
|
}
|
|
80
149
|
manifest["ingredients"] = @ingredients unless @ingredients.empty?
|
|
81
150
|
|
|
82
|
-
|
|
151
|
+
begin
|
|
152
|
+
JSON.generate(manifest)
|
|
153
|
+
rescue JSON::GeneratorError => e
|
|
154
|
+
# Typically a string that is not valid UTF-8 — a filename or caption
|
|
155
|
+
# read in another encoding and passed through untouched. Without this
|
|
156
|
+
# the caller gets a JSON::GeneratorError, which is not a C2PA::Error
|
|
157
|
+
# and so escapes `rescue C2PA::Error`.
|
|
158
|
+
raise InvalidManifestError,
|
|
159
|
+
"manifest contains text that cannot be encoded as JSON: #{e.message}"
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
# Parameters may be keyed with strings or symbols depending on the caller.
|
|
166
|
+
def param_present?(parameters, key)
|
|
167
|
+
return false unless parameters.is_a?(Hash)
|
|
168
|
+
|
|
169
|
+
!to_s_or_nil(parameters[key] || parameters[key.to_sym]).nil?
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def to_s_or_nil(value)
|
|
173
|
+
return nil if value.nil?
|
|
174
|
+
|
|
175
|
+
string = value.to_s
|
|
176
|
+
string.empty? ? nil : string
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Who signed this. Without it c2pa-rs names itself, so every asset this gem
|
|
180
|
+
# produced credited "c2pa-rs" and nothing identified the gem or the
|
|
181
|
+
# application using it.
|
|
182
|
+
#
|
|
183
|
+
# c2pa-rs 0.78 permits exactly one entry — supplying two fails with "only 1
|
|
184
|
+
# claim_generator_info allowed" — so an application name replaces the gem
|
|
185
|
+
# rather than preceding it, and the gem moves into a namespaced field. That
|
|
186
|
+
# mirrors how c2pa-rs records itself as org.contentauth.c2pa_rs.
|
|
187
|
+
def claim_generator_info
|
|
188
|
+
return { "name" => "ruby-c2pa", "version" => VERSION } if @generator_name.nil?
|
|
189
|
+
|
|
190
|
+
info = { "name" => @generator_name }
|
|
191
|
+
info["version"] = @generator_version if @generator_version
|
|
192
|
+
info[GEM_FIELD] = VERSION
|
|
193
|
+
info
|
|
83
194
|
end
|
|
84
195
|
end
|
|
85
196
|
end
|
data/lib/c2pa/version.rb
CHANGED
data/lib/c2pa.rb
CHANGED
|
@@ -2,10 +2,18 @@ require "json"
|
|
|
2
2
|
require_relative "c2pa/version"
|
|
3
3
|
require_relative "c2pa/error"
|
|
4
4
|
require_relative "c2pa/actions"
|
|
5
|
+
require_relative "c2pa/digital_source_types"
|
|
5
6
|
require_relative "c2pa/manifest"
|
|
6
7
|
require "c2pa/c2pa_native"
|
|
7
8
|
|
|
8
9
|
module C2PA
|
|
10
|
+
# Validation states that mean a signed asset is good.
|
|
11
|
+
#
|
|
12
|
+
# "Trusted" is "Valid" plus a signing certificate that chains to a root in
|
|
13
|
+
# the active trust list. Accepting only "Valid" would reject exactly the
|
|
14
|
+
# production files that are most correct.
|
|
15
|
+
VALID_STATES = %w[Valid Trusted].freeze
|
|
16
|
+
|
|
9
17
|
# Sign a file with a C2PA manifest.
|
|
10
18
|
#
|
|
11
19
|
# @param file [String] path to the input file
|
|
@@ -14,7 +22,10 @@ module C2PA
|
|
|
14
22
|
# @param key [String] path to a PEM-encoded private key
|
|
15
23
|
# @param algorithm [String] signing algorithm (default: "es256")
|
|
16
24
|
# @param manifest [C2PA::Manifest] the manifest to embed
|
|
25
|
+
# @param verify [Boolean] read the signed file back and confirm it
|
|
26
|
+
# validates (default: true)
|
|
17
27
|
# @return [String] the output path
|
|
28
|
+
# @raise [C2PA::SigningError] if signing fails, or if the signed file does not validate
|
|
18
29
|
#
|
|
19
30
|
# @example
|
|
20
31
|
# manifest = C2PA::Manifest.new(title: "Sunset over the bay")
|
|
@@ -27,7 +38,7 @@ module C2PA
|
|
|
27
38
|
# key: "key.pem",
|
|
28
39
|
# manifest: manifest
|
|
29
40
|
# )
|
|
30
|
-
def self.sign(file:, output:, certificate:, key:, algorithm: "es256", manifest:)
|
|
41
|
+
def self.sign(file:, output:, certificate:, key:, algorithm: "es256", manifest:, verify: true)
|
|
31
42
|
manifest_json = manifest.to_json
|
|
32
43
|
|
|
33
44
|
raise SigningError, "Source file not found: '#{file}'" unless File.exist?(file)
|
|
@@ -35,9 +46,18 @@ module C2PA
|
|
|
35
46
|
raise SigningError, "Key file not found: '#{key}'" unless File.exist?(key)
|
|
36
47
|
raise SigningError, "Output file already exists: '#{output}'" if File.exist?(output)
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
begin
|
|
50
|
+
# to_json is the only thing genuinely required of a manifest, so an
|
|
51
|
+
# object that provides just that still signs — as a creation.
|
|
52
|
+
intent = manifest.respond_to?(:intent) ? manifest.intent&.to_s : nil
|
|
53
|
+
Native.sign_file(file, output, certificate, key, algorithm, manifest_json, intent)
|
|
54
|
+
rescue RuntimeError => e
|
|
55
|
+
raise SigningError, e.message
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
verify_signed_output!(output) if verify
|
|
59
|
+
|
|
60
|
+
output
|
|
41
61
|
end
|
|
42
62
|
|
|
43
63
|
# Read the C2PA manifest embedded in a signed file.
|
|
@@ -62,4 +82,48 @@ module C2PA
|
|
|
62
82
|
def self.sdk_version
|
|
63
83
|
Native.sdk_version
|
|
64
84
|
end
|
|
85
|
+
|
|
86
|
+
# Read a freshly signed file back and confirm it actually validates.
|
|
87
|
+
#
|
|
88
|
+
# Signing reports success for manifests that verifiers reject. c2pa-rs
|
|
89
|
+
# applies its rules when reading, not when writing, so without this the gem
|
|
90
|
+
# hands back a path to a file that fails everywhere else — which is what
|
|
91
|
+
# released versions did, for years, silently.
|
|
92
|
+
#
|
|
93
|
+
# The rejected file is deleted rather than left behind, so a failed sign
|
|
94
|
+
# cannot leave something shippable on disk.
|
|
95
|
+
#
|
|
96
|
+
# @param output [String] path to the signed file
|
|
97
|
+
# @raise [C2PA::SigningError] if the file does not validate
|
|
98
|
+
def self.verify_signed_output!(output)
|
|
99
|
+
result =
|
|
100
|
+
begin
|
|
101
|
+
read(file: output)
|
|
102
|
+
rescue ReadError => e
|
|
103
|
+
discard(output)
|
|
104
|
+
raise SigningError, "signed file failed verification: could not read it back: #{e.message}"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
state = result["validation_state"]
|
|
108
|
+
return if VALID_STATES.include?(state)
|
|
109
|
+
|
|
110
|
+
failures = Array(result.dig("validation_results", "activeManifest", "failure"))
|
|
111
|
+
.map { |failure| "#{failure["code"]} (#{failure["explanation"]})" }
|
|
112
|
+
detail = failures.empty? ? "no failure detail reported" : failures.uniq.join(", ")
|
|
113
|
+
|
|
114
|
+
discard(output)
|
|
115
|
+
raise SigningError,
|
|
116
|
+
"signed file failed verification: validation_state=#{state.inspect}, #{detail}. " \
|
|
117
|
+
"The output file has been removed. Pass verify: false to keep it for inspection."
|
|
118
|
+
end
|
|
119
|
+
private_class_method :verify_signed_output!
|
|
120
|
+
|
|
121
|
+
# Remove a file this library created and is about to reject.
|
|
122
|
+
def self.discard(path)
|
|
123
|
+
File.delete(path) if File.exist?(path)
|
|
124
|
+
rescue SystemCallError
|
|
125
|
+
# Leaving the file behind is better than masking the original failure.
|
|
126
|
+
nil
|
|
127
|
+
end
|
|
128
|
+
private_class_method :discard
|
|
65
129
|
end
|
data/ruby-c2pa.gemspec
CHANGED
|
@@ -8,10 +8,35 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.summary = "Ruby bindings for the c2pa content authenticity library"
|
|
9
9
|
spec.description = "Embed and verify C2PA content provenance and authenticity credentials in images, video, and audio files. Ruby bindings for the official Rust c2pa-rs library."
|
|
10
10
|
spec.license = "MIT"
|
|
11
|
-
spec.homepage = "https://github.com/
|
|
12
|
-
spec.metadata["source_code_uri"] = spec.homepage
|
|
11
|
+
spec.homepage = "https://github.com/eddorre/ruby-c2pa"
|
|
13
12
|
|
|
14
|
-
spec.
|
|
13
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
|
14
|
+
spec.metadata["bug_tracker_uri"] = "#{spec.homepage}/issues"
|
|
15
|
+
spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/main/CHANGELOG.md"
|
|
16
|
+
spec.metadata["documentation_uri"] = "#{spec.homepage}/blob/main/README.md"
|
|
17
|
+
|
|
18
|
+
# Listed explicitly rather than globbed. "ext/**/*.rs" swept in whatever
|
|
19
|
+
# happened to be under ext/c2pa_native/target, so the package contents
|
|
20
|
+
# depended on what had been compiled on the machine that ran `gem build` —
|
|
21
|
+
# 1.3 MB of dependency build-script output shipped in 0.2.1.
|
|
22
|
+
#
|
|
23
|
+
# Cargo.lock is included deliberately. The extension is compiled at install
|
|
24
|
+
# time, so without it every installer resolves the dependency tree afresh.
|
|
25
|
+
# That is how 0.2.1 shipped against a broken atree and aborted the Ruby
|
|
26
|
+
# process on TIFF input.
|
|
27
|
+
spec.files = [
|
|
28
|
+
"LICENSE",
|
|
29
|
+
"README.md",
|
|
30
|
+
"CHANGELOG.md",
|
|
31
|
+
"CONTRIBUTING.md",
|
|
32
|
+
"Rakefile",
|
|
33
|
+
"ruby-c2pa.gemspec",
|
|
34
|
+
"ext/c2pa_native/Cargo.toml",
|
|
35
|
+
"ext/c2pa_native/Cargo.lock",
|
|
36
|
+
"ext/c2pa_native/extconf.rb",
|
|
37
|
+
*Dir["ext/c2pa_native/src/**/*.rs"],
|
|
38
|
+
*Dir["lib/**/*.rb"]
|
|
39
|
+
].sort
|
|
15
40
|
spec.require_paths = ["lib"]
|
|
16
41
|
spec.extensions = ["ext/c2pa_native/extconf.rb"]
|
|
17
42
|
|
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.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Carlos Rodriguez
|
|
@@ -74,32 +74,30 @@ extensions:
|
|
|
74
74
|
- ext/c2pa_native/extconf.rb
|
|
75
75
|
extra_rdoc_files: []
|
|
76
76
|
files:
|
|
77
|
+
- CHANGELOG.md
|
|
78
|
+
- CONTRIBUTING.md
|
|
77
79
|
- LICENSE
|
|
78
80
|
- README.md
|
|
79
81
|
- Rakefile
|
|
82
|
+
- ext/c2pa_native/Cargo.lock
|
|
80
83
|
- ext/c2pa_native/Cargo.toml
|
|
81
84
|
- ext/c2pa_native/extconf.rb
|
|
82
85
|
- ext/c2pa_native/src/lib.rs
|
|
83
|
-
- ext/c2pa_native/target/release/build/oid-registry-c350c75504c969dc/out/oid_db.rs
|
|
84
|
-
- ext/c2pa_native/target/release/build/pix-f303ab89d734032b/out/gamma_lut.rs
|
|
85
|
-
- ext/c2pa_native/target/release/build/serde-2e9abb4d9d73cae4/out/private.rs
|
|
86
|
-
- ext/c2pa_native/target/release/build/serde-f471616b462b0caf/out/private.rs
|
|
87
|
-
- ext/c2pa_native/target/release/build/serde_core-2b94e9134dc44065/out/private.rs
|
|
88
|
-
- ext/c2pa_native/target/release/build/serde_core-3dc945fa0ab21dd5/out/private.rs
|
|
89
|
-
- ext/c2pa_native/target/release/build/thiserror-a1f4c63469c326b9/out/private.rs
|
|
90
|
-
- ext/c2pa_native/target/release/build/typenum-4eb7d34b9fb695ef/out/tests.rs
|
|
91
|
-
- ext/c2pa_native/target/release/build/typenum-b8de96984639a942/out/tests.rs
|
|
92
86
|
- lib/c2pa.rb
|
|
93
87
|
- lib/c2pa/actions.rb
|
|
88
|
+
- lib/c2pa/digital_source_types.rb
|
|
94
89
|
- lib/c2pa/error.rb
|
|
95
90
|
- lib/c2pa/manifest.rb
|
|
96
91
|
- lib/c2pa/version.rb
|
|
97
92
|
- ruby-c2pa.gemspec
|
|
98
|
-
homepage: https://github.com/
|
|
93
|
+
homepage: https://github.com/eddorre/ruby-c2pa
|
|
99
94
|
licenses:
|
|
100
95
|
- MIT
|
|
101
96
|
metadata:
|
|
102
|
-
source_code_uri: https://github.com/
|
|
97
|
+
source_code_uri: https://github.com/eddorre/ruby-c2pa
|
|
98
|
+
bug_tracker_uri: https://github.com/eddorre/ruby-c2pa/issues
|
|
99
|
+
changelog_uri: https://github.com/eddorre/ruby-c2pa/blob/main/CHANGELOG.md
|
|
100
|
+
documentation_uri: https://github.com/eddorre/ruby-c2pa/blob/main/README.md
|
|
103
101
|
rdoc_options: []
|
|
104
102
|
require_paths:
|
|
105
103
|
- lib
|