ruby-c2pa 0.3.0 → 0.4.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 +49 -1
- data/README.md +128 -20
- data/ext/c2pa_native/Cargo.lock +754 -667
- data/ext/c2pa_native/Cargo.toml +1 -1
- data/ext/c2pa_native/src/lib.rs +102 -5
- data/lib/c2pa/config.rb +90 -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 +38 -1
- metadata +2 -1
data/ext/c2pa_native/Cargo.toml
CHANGED
data/ext/c2pa_native/src/lib.rs
CHANGED
|
@@ -1,9 +1,52 @@
|
|
|
1
|
+
use std::fs::File;
|
|
1
2
|
use std::path::Path;
|
|
2
|
-
use
|
|
3
|
+
use std::sync::{Arc, RwLock, OnceLock};
|
|
4
|
+
use c2pa::{create_signer, Builder, BuilderIntent, Context, Reader, SigningAlg};
|
|
3
5
|
use magnus::{function, prelude::*, Error, Ruby};
|
|
4
6
|
|
|
5
7
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
6
8
|
|
|
9
|
+
// A Context carries the settings c2pa-rs validates against — trust anchors,
|
|
10
|
+
// what to verify, whether to fetch remote manifests. The older entry points
|
|
11
|
+
// read those from thread-local state, which is why they are deprecated: a
|
|
12
|
+
// threaded Ruby application could see different settings depending on which
|
|
13
|
+
// thread it signed from.
|
|
14
|
+
//
|
|
15
|
+
// One shared Context is built lazily and reused. It is Send + Sync, so an Arc
|
|
16
|
+
// is all that sharing requires, and reusing it avoids re-reading configuration
|
|
17
|
+
// on every call.
|
|
18
|
+
//
|
|
19
|
+
// It currently carries defaults. Exposing settings to Ruby is a separate piece
|
|
20
|
+
// of work; this is the seam that makes it possible.
|
|
21
|
+
fn context_slot() -> &'static RwLock<Arc<Context>> {
|
|
22
|
+
static CONTEXT: OnceLock<RwLock<Arc<Context>>> = OnceLock::new();
|
|
23
|
+
CONTEXT.get_or_init(|| RwLock::new(Context::new().into_shared()))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
fn shared_context() -> Arc<Context> {
|
|
27
|
+
context_slot()
|
|
28
|
+
.read()
|
|
29
|
+
.expect("context lock poisoned")
|
|
30
|
+
.clone()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Replace the shared Context with one built from the supplied settings.
|
|
34
|
+
//
|
|
35
|
+
// c2pa-rs takes settings as JSON, so the Ruby side assembles the document and
|
|
36
|
+
// this only has to validate and install it. Rebuilding rather than mutating
|
|
37
|
+
// keeps the Context immutable once shared: signing already in flight on another
|
|
38
|
+
// thread continues against the settings it started with.
|
|
39
|
+
fn do_configure(settings_json: &str) -> Result<(), Box<dyn std::error::Error>> {
|
|
40
|
+
let context = Context::new()
|
|
41
|
+
.with_settings(settings_json)
|
|
42
|
+
.map_err(|e| format!("Invalid settings: {}", e))?
|
|
43
|
+
.into_shared();
|
|
44
|
+
|
|
45
|
+
*context_slot().write().map_err(|_| "context lock poisoned")? = context;
|
|
46
|
+
|
|
47
|
+
Ok(())
|
|
48
|
+
}
|
|
49
|
+
|
|
7
50
|
fn alg_from_str(alg: &str) -> Result<SigningAlg, String> {
|
|
8
51
|
match alg.to_lowercase().as_str() {
|
|
9
52
|
"ps256" => Ok(SigningAlg::Ps256),
|
|
@@ -38,6 +81,41 @@ fn intent_from_str(intent: &str) -> Result<BuilderIntent, String> {
|
|
|
38
81
|
}
|
|
39
82
|
}
|
|
40
83
|
|
|
84
|
+
// Ingredients supplied as files rather than as descriptions. c2pa-rs reads the
|
|
85
|
+
// bytes to hash them, generate a thumbnail, and carry forward any manifest the
|
|
86
|
+
// file already holds; the JSON description takes precedence over anything
|
|
87
|
+
// derived from the stream.
|
|
88
|
+
//
|
|
89
|
+
// Expects a JSON array of {"json": "<ingredient JSON>", "format": "<mime>",
|
|
90
|
+
// "path": "<file>"}.
|
|
91
|
+
fn add_ingredient_files(
|
|
92
|
+
builder: &mut Builder,
|
|
93
|
+
ingredient_files_json: &str,
|
|
94
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
95
|
+
let entries: serde_json::Value = serde_json::from_str(ingredient_files_json)
|
|
96
|
+
.map_err(|e| format!("Invalid ingredient list: {}", e))?;
|
|
97
|
+
let entries = entries
|
|
98
|
+
.as_array()
|
|
99
|
+
.ok_or("Invalid ingredient list: expected an array")?;
|
|
100
|
+
|
|
101
|
+
for entry in entries {
|
|
102
|
+
let field = |name: &str| -> Result<&str, String> {
|
|
103
|
+
entry[name]
|
|
104
|
+
.as_str()
|
|
105
|
+
.ok_or_else(|| format!("Invalid ingredient list: missing '{}'", name))
|
|
106
|
+
};
|
|
107
|
+
let (json, format, path) = (field("json")?, field("format")?, field("path")?);
|
|
108
|
+
|
|
109
|
+
let mut stream = File::open(path)
|
|
110
|
+
.map_err(|e| format!("Cannot read ingredient '{}': {}", path, e))?;
|
|
111
|
+
builder
|
|
112
|
+
.add_ingredient_from_stream(json, format, &mut stream)
|
|
113
|
+
.map_err(|e| format!("Cannot add ingredient '{}': {}", path, e))?;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
Ok(())
|
|
117
|
+
}
|
|
118
|
+
|
|
41
119
|
fn do_sign_file(
|
|
42
120
|
source_path: &str,
|
|
43
121
|
dest_path: &str,
|
|
@@ -46,6 +124,7 @@ fn do_sign_file(
|
|
|
46
124
|
alg_str: &str,
|
|
47
125
|
manifest_json: Option<&str>,
|
|
48
126
|
intent_str: Option<&str>,
|
|
127
|
+
ingredient_files_json: Option<&str>,
|
|
49
128
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
50
129
|
let cert = std::fs::read(cert_path)
|
|
51
130
|
.map_err(|e| format!("Cannot read certificate '{}': {}", cert_path, e))?;
|
|
@@ -64,13 +143,18 @@ fn do_sign_file(
|
|
|
64
143
|
let default_json = format!(r#"{{"title": "{}"}}"#, title);
|
|
65
144
|
let json = manifest_json.unwrap_or(&default_json);
|
|
66
145
|
|
|
67
|
-
let mut builder = Builder::
|
|
146
|
+
let mut builder = Builder::from_shared_context(&shared_context())
|
|
147
|
+
.with_definition(json)
|
|
68
148
|
.map_err(|e| format!("Invalid manifest JSON: {}", e))?;
|
|
69
149
|
|
|
70
150
|
if let Some(intent) = intent_str {
|
|
71
151
|
builder.set_intent(intent_from_str(intent)?);
|
|
72
152
|
}
|
|
73
153
|
|
|
154
|
+
if let Some(files) = ingredient_files_json {
|
|
155
|
+
add_ingredient_files(&mut builder, files)?;
|
|
156
|
+
}
|
|
157
|
+
|
|
74
158
|
builder.sign_file(&*signer, source_path, dest_path)
|
|
75
159
|
.map_err(|e| format!("Signing failed: {}", e))?;
|
|
76
160
|
|
|
@@ -78,7 +162,8 @@ fn do_sign_file(
|
|
|
78
162
|
}
|
|
79
163
|
|
|
80
164
|
fn do_read_file(path: &str) -> Result<String, Box<dyn std::error::Error>> {
|
|
81
|
-
let reader = Reader::
|
|
165
|
+
let reader = Reader::from_shared_context(&shared_context())
|
|
166
|
+
.with_file(path)
|
|
82
167
|
.map_err(|e| format!("Failed to read manifest from '{}': {}", path, e))?;
|
|
83
168
|
Ok(reader.json())
|
|
84
169
|
}
|
|
@@ -93,10 +178,12 @@ fn sign_file(
|
|
|
93
178
|
alg: Option<String>,
|
|
94
179
|
manifest_json: Option<String>,
|
|
95
180
|
intent: Option<String>,
|
|
181
|
+
ingredient_files: Option<String>,
|
|
96
182
|
) -> Result<String, Error> {
|
|
97
183
|
let alg_str = alg.as_deref().unwrap_or("es256");
|
|
98
184
|
|
|
99
|
-
do_sign_file(&source, &dest, &cert, &key, alg_str, manifest_json.as_deref(),
|
|
185
|
+
do_sign_file(&source, &dest, &cert, &key, alg_str, manifest_json.as_deref(),
|
|
186
|
+
intent.as_deref(), ingredient_files.as_deref())
|
|
100
187
|
.map_err(|e| Error::new(Ruby::get().expect("called from Ruby thread").exception_runtime_error(), e.to_string()))?;
|
|
101
188
|
|
|
102
189
|
Ok(dest)
|
|
@@ -107,6 +194,15 @@ fn read_file(path: String) -> Result<String, Error> {
|
|
|
107
194
|
.map_err(|e| Error::new(Ruby::get().expect("called from Ruby thread").exception_runtime_error(), e.to_string()))
|
|
108
195
|
}
|
|
109
196
|
|
|
197
|
+
fn configure(settings_json: String) -> Result<(), Error> {
|
|
198
|
+
do_configure(&settings_json).map_err(|e| {
|
|
199
|
+
Error::new(
|
|
200
|
+
Ruby::get().expect("called from Ruby thread").exception_runtime_error(),
|
|
201
|
+
e.to_string(),
|
|
202
|
+
)
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
110
206
|
fn sdk_version() -> String {
|
|
111
207
|
c2pa::VERSION.to_string()
|
|
112
208
|
}
|
|
@@ -118,8 +214,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
118
214
|
let c2pa = ruby.define_module("C2PA")?;
|
|
119
215
|
let native = c2pa.define_module("Native")?;
|
|
120
216
|
|
|
121
|
-
native.define_singleton_method("sign_file", function!(sign_file,
|
|
217
|
+
native.define_singleton_method("sign_file", function!(sign_file, 8))?;
|
|
122
218
|
native.define_singleton_method("read_file", function!(read_file, 1))?;
|
|
219
|
+
native.define_singleton_method("configure", function!(configure, 1))?;
|
|
123
220
|
native.define_singleton_method("sdk_version", function!(sdk_version, 0))?;
|
|
124
221
|
|
|
125
222
|
Ok(())
|
data/lib/c2pa/config.rb
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module C2PA
|
|
4
|
+
# Settings that govern how c2pa-rs validates.
|
|
5
|
+
#
|
|
6
|
+
# These reach the SDK through its Context, which is rebuilt whenever they
|
|
7
|
+
# change. Signing already in flight on another thread continues against the
|
|
8
|
+
# settings it started with.
|
|
9
|
+
#
|
|
10
|
+
# Defaults are c2pa-rs's own, so a gem that never calls C2PA.configure
|
|
11
|
+
# behaves exactly as it did before this existed.
|
|
12
|
+
class Config
|
|
13
|
+
# Additional root certificates to trust, as a PEM bundle. Use this for a
|
|
14
|
+
# private or enterprise CA: a certificate chaining to one of these
|
|
15
|
+
# validates as "Trusted" rather than carrying signingCredential.untrusted.
|
|
16
|
+
attr_accessor :trust_anchors
|
|
17
|
+
|
|
18
|
+
# The trust list proper — normally the C2PA-recognised anchors. Setting
|
|
19
|
+
# this replaces that list rather than adding to it, so prefer
|
|
20
|
+
# trust_anchors unless you mean to substitute the whole thing.
|
|
21
|
+
attr_accessor :trust_list
|
|
22
|
+
|
|
23
|
+
# Explicitly allowed certificates, as a PEM bundle.
|
|
24
|
+
attr_accessor :allowed_certificates
|
|
25
|
+
|
|
26
|
+
# Whether to check certificates against the trust list at all.
|
|
27
|
+
#
|
|
28
|
+
# Turning this off means nothing is ever reported as untrusted, which in a
|
|
29
|
+
# library for establishing provenance is rarely what you want. It exists
|
|
30
|
+
# for offline and air-gapped environments.
|
|
31
|
+
attr_accessor :verify_trust
|
|
32
|
+
|
|
33
|
+
# Whether reading an asset may fetch a manifest over the network.
|
|
34
|
+
# c2pa-rs defaults this to true, so reading can make an outbound request.
|
|
35
|
+
attr_accessor :remote_manifest_fetch
|
|
36
|
+
|
|
37
|
+
# Whether to check certificate revocation over OCSP, which also makes
|
|
38
|
+
# network requests.
|
|
39
|
+
attr_accessor :ocsp_fetch
|
|
40
|
+
|
|
41
|
+
def initialize
|
|
42
|
+
@trust_anchors = nil
|
|
43
|
+
@trust_list = nil
|
|
44
|
+
@allowed_certificates = nil
|
|
45
|
+
@verify_trust = nil
|
|
46
|
+
@remote_manifest_fetch = nil
|
|
47
|
+
@ocsp_fetch = nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The settings document c2pa-rs expects.
|
|
51
|
+
#
|
|
52
|
+
# Only values that were actually set are included, so anything left alone
|
|
53
|
+
# keeps the SDK's default rather than being pinned to ours.
|
|
54
|
+
#
|
|
55
|
+
# @return [String] JSON
|
|
56
|
+
def to_json
|
|
57
|
+
trust = {}
|
|
58
|
+
trust["user_anchors"] = read_pem(@trust_anchors) unless @trust_anchors.nil?
|
|
59
|
+
trust["trust_anchors"] = read_pem(@trust_list) unless @trust_list.nil?
|
|
60
|
+
trust["allowed_list"] = read_pem(@allowed_certificates) unless @allowed_certificates.nil?
|
|
61
|
+
|
|
62
|
+
verify = {}
|
|
63
|
+
verify["verify_trust"] = @verify_trust unless @verify_trust.nil?
|
|
64
|
+
verify["remote_manifest_fetch"] = @remote_manifest_fetch unless @remote_manifest_fetch.nil?
|
|
65
|
+
verify["ocsp_fetch"] = @ocsp_fetch unless @ocsp_fetch.nil?
|
|
66
|
+
|
|
67
|
+
settings = {}
|
|
68
|
+
settings["trust"] = trust unless trust.empty?
|
|
69
|
+
settings["verify"] = verify unless verify.empty?
|
|
70
|
+
|
|
71
|
+
JSON.generate(settings)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
# Accept either PEM text or a path to a file containing it, since callers
|
|
77
|
+
# naturally have one or the other.
|
|
78
|
+
def read_pem(value)
|
|
79
|
+
string = value.to_s
|
|
80
|
+
return string if string.include?("BEGIN CERTIFICATE")
|
|
81
|
+
|
|
82
|
+
unless File.exist?(string)
|
|
83
|
+
raise InvalidSettingsError,
|
|
84
|
+
"expected PEM text or a readable file, got #{string.inspect}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
File.read(string)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
data/lib/c2pa/error.rb
CHANGED
data/lib/c2pa/manifest.rb
CHANGED
|
@@ -4,17 +4,15 @@ module C2PA
|
|
|
4
4
|
class Manifest
|
|
5
5
|
# Intents this gem can express.
|
|
6
6
|
#
|
|
7
|
-
# :edit
|
|
8
|
-
#
|
|
9
|
-
#
|
|
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
|
+
# :update — a restricted edit for non-editorial changes, such as fixing
|
|
11
|
+
# metadata. The parent is the source file itself; an explicit
|
|
12
|
+
# ingredient, if given, must be that same file.
|
|
10
13
|
#
|
|
11
14
|
# 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
|
|
15
|
+
INTENTS = %i[edit update].freeze
|
|
18
16
|
|
|
19
17
|
# c2pa-rs records itself in a namespaced field alongside the generator
|
|
20
18
|
# name, so the gem does the same when an application supplies its own.
|
|
@@ -43,8 +41,16 @@ module C2PA
|
|
|
43
41
|
@actions = []
|
|
44
42
|
@assertions = []
|
|
45
43
|
@ingredients = []
|
|
44
|
+
@ingredient_files = []
|
|
46
45
|
end
|
|
47
46
|
|
|
47
|
+
# Ingredients supplied as files, for the signing layer to hand to c2pa-rs.
|
|
48
|
+
# Each is a Hash with the ingredient's JSON description, its format, and
|
|
49
|
+
# the path to read.
|
|
50
|
+
#
|
|
51
|
+
# @return [Array<Hash>]
|
|
52
|
+
attr_reader :ingredient_files
|
|
53
|
+
|
|
48
54
|
# Add a C2PA action to this manifest.
|
|
49
55
|
#
|
|
50
56
|
# @param action [String] one of the C2PA::Actions constants
|
|
@@ -115,18 +121,48 @@ module C2PA
|
|
|
115
121
|
|
|
116
122
|
# Add an ingredient (source asset) to this manifest.
|
|
117
123
|
#
|
|
118
|
-
#
|
|
119
|
-
#
|
|
120
|
-
#
|
|
124
|
+
# With `file:`, c2pa-rs reads the ingredient itself. If that file carries
|
|
125
|
+
# content credentials, its manifest is embedded and the ingredient points
|
|
126
|
+
# at it, so provenance chains from the original through to this asset. A
|
|
127
|
+
# verifier can then follow and check the whole history.
|
|
128
|
+
#
|
|
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.)
|
|
133
|
+
#
|
|
134
|
+
# Without `file:`, only the description is recorded. Nothing binds it to
|
|
135
|
+
# any actual bytes. This form is kept for compatibility.
|
|
136
|
+
#
|
|
137
|
+
# @param title [String] human-readable title of the ingredient
|
|
138
|
+
# @param format [String] MIME type of the ingredient, e.g. "image/jpeg"
|
|
139
|
+
# @param instance_id [String] unique identifier for the ingredient instance
|
|
121
140
|
# @param relationship [String] relationship to this asset; defaults to "parentOf"
|
|
141
|
+
# @param file [String, nil] path to the ingredient file
|
|
122
142
|
# @return [self]
|
|
123
|
-
|
|
124
|
-
|
|
143
|
+
# @raise [C2PA::InvalidManifestError] if `file` is given but cannot be read
|
|
144
|
+
def add_ingredient(title:, format:, instance_id:, relationship: "parentOf", file: nil)
|
|
145
|
+
description = {
|
|
125
146
|
"title" => title,
|
|
126
147
|
"format" => format,
|
|
127
148
|
"instance_id" => instance_id,
|
|
128
149
|
"relationship" => relationship
|
|
129
150
|
}
|
|
151
|
+
|
|
152
|
+
if file.nil?
|
|
153
|
+
@ingredients << description
|
|
154
|
+
return self
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
unless File.file?(file) && File.readable?(file)
|
|
158
|
+
raise InvalidManifestError, "ingredient file not readable: #{file.inspect}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
@ingredient_files << {
|
|
162
|
+
"json" => JSON.generate(description),
|
|
163
|
+
"format" => format,
|
|
164
|
+
"path" => File.expand_path(file)
|
|
165
|
+
}
|
|
130
166
|
self
|
|
131
167
|
end
|
|
132
168
|
|
data/lib/c2pa/version.rb
CHANGED
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
|
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.4.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
|