confium 0.4.1 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +84 -0
- data/Cargo.lock +432 -12
- data/ext/confium_native/Cargo.toml +16 -0
- data/ext/confium_native/extconf.rb +5 -0
- data/ext/confium_native/src/lib.rs +4 -0
- data/ext/confium_native/src/openpgp_verify.rs +189 -0
- data/lib/confium/audit/otlp_sink.rb +120 -0
- data/lib/confium/audit.rb +2 -0
- data/lib/confium/composite.rb +39 -1
- data/lib/confium/native_windows.rb +31 -0
- data/lib/confium/tc/coordinator.rb +28 -27
- data/lib/confium/tc/network_coordinator.rb +208 -0
- data/lib/confium/tc/signing_session.rb +93 -0
- data/lib/confium/tc.rb +7 -2
- data/lib/confium/version.rb +1 -1
- data/lib/confium.rb +10 -16
- metadata +7 -9
- data/lib/confium/cfm.rb +0 -23
- data/lib/confium/crypto.rb +0 -51
- data/lib/confium/digest.rb +0 -62
- data/lib/confium/ffi.rb +0 -23
- data/lib/confium/lib.rb +0 -33
- data/lib/confium/tc/session.rb +0 -51
- data/lib/confium/tc/session_stub.rb +0 -43
|
@@ -16,6 +16,14 @@ crate-type = ["cdylib"]
|
|
|
16
16
|
|
|
17
17
|
build = "build.rs"
|
|
18
18
|
|
|
19
|
+
[features]
|
|
20
|
+
# OpenPGP signature verification via a vendored librnp (librnp +
|
|
21
|
+
# Botan + json-c). OFF by default: the C/C++ build would restore the
|
|
22
|
+
# multi-minute compile times and heavy binaries 0.4.0 removed (armor
|
|
23
|
+
# itself is pure Ruby). Source builders opt in; pre-compiled platform
|
|
24
|
+
# gems always build default features.
|
|
25
|
+
pgp = ["rnp"]
|
|
26
|
+
|
|
19
27
|
[dependencies]
|
|
20
28
|
# rb-sys for Ruby C API access.
|
|
21
29
|
rb-sys = { version = "0.9.124", features = ["global-allocator"] }
|
|
@@ -57,3 +65,11 @@ serde_json = "1"
|
|
|
57
65
|
|
|
58
66
|
[profile.release]
|
|
59
67
|
lto = "off"
|
|
68
|
+
|
|
69
|
+
# OpenPGP verification (feature "pgp") — vendored librnp via rnp-rs.
|
|
70
|
+
# No system librnp needed; adds the Botan/json-c C/C++ build.
|
|
71
|
+
[dependencies.rnp]
|
|
72
|
+
package = "rnp-rs"
|
|
73
|
+
version = "0.1.10"
|
|
74
|
+
features = ["vendored"]
|
|
75
|
+
optional = true
|
|
@@ -7,4 +7,9 @@ create_rust_makefile('confium_native') do |r|
|
|
|
7
7
|
r.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :dev).to_sym
|
|
8
8
|
r.use_stable_api_compiled_fallback = true
|
|
9
9
|
r.force_install_rust_toolchain = false
|
|
10
|
+
# rb_sys only threads --features through to cargo when the builder
|
|
11
|
+
# has a non-empty feature list; seeding it from the env var makes
|
|
12
|
+
# RB_SYS_CARGO_FEATURES=pgp bundle exec rake compile work.
|
|
13
|
+
r.features = ENV.fetch('RB_SYS_CARGO_FEATURES', '')
|
|
14
|
+
.split(',').map(&:strip).reject(&:empty?)
|
|
10
15
|
end
|
|
@@ -9,6 +9,7 @@ mod attributes;
|
|
|
9
9
|
mod composite;
|
|
10
10
|
mod deployment;
|
|
11
11
|
mod ers;
|
|
12
|
+
mod openpgp_verify;
|
|
12
13
|
mod path;
|
|
13
14
|
mod pki;
|
|
14
15
|
mod tc;
|
|
@@ -42,6 +43,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
42
43
|
confium.define_module_function("core_version", function!(core_version, 0))?;
|
|
43
44
|
|
|
44
45
|
transparency::init(ruby, confium)?;
|
|
46
|
+
openpgp_verify::init(ruby, confium)?;
|
|
47
|
+
#[cfg(feature = "pgp")]
|
|
48
|
+
openpgp_verify::init_pgp(ruby, &confium)?;
|
|
45
49
|
composite::init(ruby, confium)?;
|
|
46
50
|
attributes::init(ruby, confium)?;
|
|
47
51
|
pki::init(ruby, confium)?;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
//! Confium::OpenPGP — OpenPGP (RFC 9580) signature verification via
|
|
2
|
+
//! the vendored librnp, behind the `pgp` cargo feature.
|
|
3
|
+
//!
|
|
4
|
+
//! Armor (encode/decode) is pure Ruby in lib/confium/openpgp.rb and
|
|
5
|
+
//! always available. This module adds verification only, and only
|
|
6
|
+
//! when the extension was built with `--features pgp`: the vendored
|
|
7
|
+
//! librnp brings a full Botan/json-c C/C++ build that default
|
|
8
|
+
//! (platform-gem) builds deliberately exclude.
|
|
9
|
+
|
|
10
|
+
use magnus::{Error, Module, RModule, Ruby, Value, function, prelude::*};
|
|
11
|
+
|
|
12
|
+
use crate::util::runtime;
|
|
13
|
+
|
|
14
|
+
fn stub_message(method: &str) -> String {
|
|
15
|
+
format!(
|
|
16
|
+
"Confium::OpenPGP.{} requires the pgp cargo feature — rebuild \
|
|
17
|
+
the extension with --features pgp (platform gems build \
|
|
18
|
+
default features)",
|
|
19
|
+
method
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Registered when the extension builds WITHOUT the pgp feature:
|
|
24
|
+
/// `Confium::OpenPGP::PGP_AVAILABLE` is false and the verify methods
|
|
25
|
+
/// raise with instructions instead of pretending.
|
|
26
|
+
pub fn init(_ruby: &Ruby, parent: RModule) -> Result<(), Error> {
|
|
27
|
+
let openpgp = parent.define_module("OpenPGP")?;
|
|
28
|
+
let _ = openpgp.const_set("PGP_AVAILABLE", false);
|
|
29
|
+
openpgp.define_singleton_method(
|
|
30
|
+
"verify_detached",
|
|
31
|
+
function!(
|
|
32
|
+
move |_args: &[Value]| -> Result<Value, Error> {
|
|
33
|
+
Err(runtime(stub_message("verify_detached")))
|
|
34
|
+
},
|
|
35
|
+
-1
|
|
36
|
+
),
|
|
37
|
+
)?;
|
|
38
|
+
openpgp.define_singleton_method(
|
|
39
|
+
"verify",
|
|
40
|
+
function!(
|
|
41
|
+
move |_args: &[Value]| -> Result<Value, Error> { Err(runtime(stub_message("verify"))) },
|
|
42
|
+
-1
|
|
43
|
+
),
|
|
44
|
+
)?;
|
|
45
|
+
Ok(())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// pgp feature implementation
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
#[cfg(feature = "pgp")]
|
|
52
|
+
mod pgp_impl {
|
|
53
|
+
use super::*;
|
|
54
|
+
use crate::util::{bytes_from_value, crypto_error, parse_error};
|
|
55
|
+
|
|
56
|
+
fn result_to_hash(ruby: &Ruby, result: &rnp::VerifyResult) -> Result<Value, Error> {
|
|
57
|
+
let hash = ruby.hash_new();
|
|
58
|
+
let _ = hash.aset("any_valid", result.any_valid().unwrap_or(false));
|
|
59
|
+
let _ = hash.aset(
|
|
60
|
+
"signature_count",
|
|
61
|
+
result.signature_count().unwrap_or(0) as i64,
|
|
62
|
+
);
|
|
63
|
+
let arr = ruby.ary_new();
|
|
64
|
+
for s in result.iter_signatures() {
|
|
65
|
+
let sh = ruby.hash_new();
|
|
66
|
+
let _ = sh.aset("valid", s.status_is_valid());
|
|
67
|
+
let _ = sh.aset("status", format!("{:?}", s.status()));
|
|
68
|
+
let _ = sh.aset("key_id", s.keyid().unwrap_or_else(|_| "?".to_string()));
|
|
69
|
+
let (created, expires) = s.times().unwrap_or((0, 0));
|
|
70
|
+
let _ = sh.aset("creation_time", created as i64);
|
|
71
|
+
let _ = sh.aset("expiration_time", expires as i64);
|
|
72
|
+
if let Ok(alg) = s.hash() {
|
|
73
|
+
let _ = sh.aset("hash", alg);
|
|
74
|
+
}
|
|
75
|
+
arr.push(sh)?;
|
|
76
|
+
}
|
|
77
|
+
let _ = hash.aset("signatures", arr);
|
|
78
|
+
Ok(hash.as_value())
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fn negative_result(ruby: &Ruby) -> Result<Value, Error> {
|
|
82
|
+
// librnp reports a failed signature check as an error from
|
|
83
|
+
// rnp_op_verify_execute; for a verifier API that is the
|
|
84
|
+
// answer, not an exception.
|
|
85
|
+
let hash = ruby.hash_new();
|
|
86
|
+
let _ = hash.aset("any_valid", false);
|
|
87
|
+
let _ = hash.aset("signature_count", 0 as i64);
|
|
88
|
+
let _ = hash.aset("signatures", ruby.ary_new());
|
|
89
|
+
Ok(hash.as_value())
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fn ctx() -> Result<rnp::Context, Error> {
|
|
93
|
+
rnp::Context::new().map_err(|e| crypto_error(e.to_string(), "OpenPGP", "openpgp"))
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
fn import_keys(context: &rnp::Context, keys: &[Value]) -> Result<(), Error> {
|
|
97
|
+
for k in keys {
|
|
98
|
+
let bytes = bytes_from_value(*k)?;
|
|
99
|
+
context
|
|
100
|
+
.import_keys(&bytes, rnp::LoadSaveFlags::PUBLIC)
|
|
101
|
+
.map_err(|e| {
|
|
102
|
+
parse_error(e.to_string(), "OpenPGP key import", Some("openpgp"), None)
|
|
103
|
+
})?;
|
|
104
|
+
}
|
|
105
|
+
Ok(())
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// `(message, signature, keys?)` → `(message, signature, keys)`
|
|
109
|
+
fn split_args(
|
|
110
|
+
args: &[Value],
|
|
111
|
+
min: usize,
|
|
112
|
+
method: &str,
|
|
113
|
+
) -> Result<(Value, Option<Value>, Vec<Value>), Error> {
|
|
114
|
+
if args.len() < min || args.len() > min + 1 {
|
|
115
|
+
return Err(crate::util::arg_error(format!(
|
|
116
|
+
"Confium::OpenPGP.{}: wrong number of arguments (given {}, expected {}..{})",
|
|
117
|
+
method,
|
|
118
|
+
args.len(),
|
|
119
|
+
min,
|
|
120
|
+
min + 1
|
|
121
|
+
)));
|
|
122
|
+
}
|
|
123
|
+
let keys = if args.len() == min + 1 {
|
|
124
|
+
match <magnus::RArray as magnus::TryConvert>::try_convert(args[min]) {
|
|
125
|
+
Ok(array) => array.into_iter().collect(),
|
|
126
|
+
Err(_) => vec![args[min]],
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
Vec::new()
|
|
130
|
+
};
|
|
131
|
+
Ok((args[0], args.get(1).copied(), keys))
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
pub fn verify_detached(args: &[Value]) -> Result<Value, Error> {
|
|
135
|
+
let ruby = Ruby::get().map_err(|e| runtime(e.to_string()))?;
|
|
136
|
+
let (message, signature, keys) = split_args(args, 2, "verify_detached")?;
|
|
137
|
+
let msg = bytes_from_value(message)?;
|
|
138
|
+
let sig = bytes_from_value(
|
|
139
|
+
signature.ok_or_else(|| crate::util::arg_error("missing signature"))?,
|
|
140
|
+
)?;
|
|
141
|
+
|
|
142
|
+
let context = ctx()?;
|
|
143
|
+
import_keys(&context, &keys)?;
|
|
144
|
+
let result = match rnp::verify_detached(&context, &msg, &sig) {
|
|
145
|
+
Ok(result) => result_to_hash(&ruby, &result)?,
|
|
146
|
+
Err(e) if e.kind() == rnp::ErrorKind::SignatureInvalid => negative_result(&ruby)?,
|
|
147
|
+
Err(e) => {
|
|
148
|
+
return Err(parse_error(
|
|
149
|
+
e.to_string(),
|
|
150
|
+
"OpenPGP.verify_detached",
|
|
151
|
+
Some("openpgp"),
|
|
152
|
+
None,
|
|
153
|
+
));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
Ok(result)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
pub fn verify(args: &[Value]) -> Result<Value, Error> {
|
|
160
|
+
let ruby = Ruby::get().map_err(|e| runtime(e.to_string()))?;
|
|
161
|
+
let (signed, _signature_ignored, keys) = split_args(args, 1, "verify")?;
|
|
162
|
+
let msg = bytes_from_value(signed)?;
|
|
163
|
+
|
|
164
|
+
let context = ctx()?;
|
|
165
|
+
import_keys(&context, &keys)?;
|
|
166
|
+
let result = match rnp::verify(&context, &msg) {
|
|
167
|
+
Ok(result) => result_to_hash(&ruby, &result)?,
|
|
168
|
+
Err(e) if e.kind() == rnp::ErrorKind::SignatureInvalid => negative_result(&ruby)?,
|
|
169
|
+
Err(e) => {
|
|
170
|
+
return Err(parse_error(
|
|
171
|
+
e.to_string(),
|
|
172
|
+
"OpenPGP.verify",
|
|
173
|
+
Some("openpgp"),
|
|
174
|
+
None,
|
|
175
|
+
));
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
Ok(result)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
#[cfg(feature = "pgp")]
|
|
183
|
+
pub fn init_pgp(_ruby: &Ruby, parent: &RModule) -> Result<(), Error> {
|
|
184
|
+
let openpgp = parent.define_module("OpenPGP")?;
|
|
185
|
+
let _ = openpgp.const_set("PGP_AVAILABLE", true);
|
|
186
|
+
openpgp.define_singleton_method("verify_detached", function!(pgp_impl::verify_detached, -1))?;
|
|
187
|
+
openpgp.define_singleton_method("verify", function!(pgp_impl::verify, -1))?;
|
|
188
|
+
Ok(())
|
|
189
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'time'
|
|
6
|
+
|
|
7
|
+
module Confium
|
|
8
|
+
module Audit
|
|
9
|
+
# Exports audit records to an OpenTelemetry Collector over
|
|
10
|
+
# OTLP/HTTP JSON (the logs signal — an audit event is a log
|
|
11
|
+
# record, not a span).
|
|
12
|
+
#
|
|
13
|
+
# Confium::Audit.sink = Confium::Audit::OtlpSink.new(
|
|
14
|
+
# endpoint: 'http://localhost:4318/v1/logs',
|
|
15
|
+
# headers: { 'Authorization' => "Bearer #{token}" },
|
|
16
|
+
# service_name: 'confium-issuer'
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# Delivery is synchronous per record, retried with exponential
|
|
20
|
+
# backoff (retries: attempts after the first, base doubled per
|
|
21
|
+
# attempt). After the final failure the record is dropped with a
|
|
22
|
+
# $stderr report — the audit core's policy: a telemetry outage
|
|
23
|
+
# must never break signing. stdlib only.
|
|
24
|
+
class OtlpSink < Sink
|
|
25
|
+
DEFAULT_ENDPOINT = 'http://localhost:4318/v1/logs'
|
|
26
|
+
SEVERITY = { 'success' => 9, 'failure' => 17, 'error' => 17 }.freeze # INFO / ERROR
|
|
27
|
+
|
|
28
|
+
def initialize(endpoint: DEFAULT_ENDPOINT, headers: {}, service_name: 'confium', timeout: 5,
|
|
29
|
+
retries: 2, retry_base: 0.1)
|
|
30
|
+
super()
|
|
31
|
+
@uri = URI.parse(endpoint)
|
|
32
|
+
@headers = headers
|
|
33
|
+
@service_name = service_name
|
|
34
|
+
@timeout = timeout
|
|
35
|
+
@retries = retries
|
|
36
|
+
@retry_base = retry_base
|
|
37
|
+
@dropped = 0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Records dropped after failed delivery (diagnostic only).
|
|
41
|
+
attr_reader :dropped
|
|
42
|
+
|
|
43
|
+
def write(record)
|
|
44
|
+
payload = envelope(record)
|
|
45
|
+
deliver(JSON.generate(payload))
|
|
46
|
+
rescue StandardError => e
|
|
47
|
+
@dropped += 1
|
|
48
|
+
warn "confium: OTLP delivery failed, audit record dropped (##{@dropped}): #{e.class}: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def close; end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Attempts grow the backoff: retry_base * 2**attempt. When
|
|
56
|
+
# retry_base is 0 (specs) retries run without delay.
|
|
57
|
+
def deliver(body)
|
|
58
|
+
(@retries + 1).times do |attempt|
|
|
59
|
+
sleep(@retry_base * (2**attempt)) if attempt.positive? && @retry_base.positive?
|
|
60
|
+
|
|
61
|
+
response = Net::HTTP.post(@uri, body, headers.merge('Content-Type' => 'application/json'))
|
|
62
|
+
return response if response.is_a?(Net::HTTPSuccess)
|
|
63
|
+
|
|
64
|
+
raise "collector responded #{response.code}" if attempt == @retries
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def headers
|
|
69
|
+
{ 'User-Agent' => "confium-otlp-sink #{Confium::VERSION}" }.merge(@headers)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# OTLP/JSON logs request: one resource, one scope, one record.
|
|
73
|
+
def envelope(record)
|
|
74
|
+
{
|
|
75
|
+
'resourceLogs' => [
|
|
76
|
+
{
|
|
77
|
+
'resource' => {
|
|
78
|
+
'attributes' => [
|
|
79
|
+
{ 'key' => 'service.name', 'value' => { 'stringValue' => @service_name } },
|
|
80
|
+
{ 'key' => 'telemetry.sdk.name', 'value' => { 'stringValue' => 'confium' } },
|
|
81
|
+
{ 'key' => 'telemetry.sdk.version', 'value' => { 'stringValue' => Confium::VERSION } }
|
|
82
|
+
]
|
|
83
|
+
},
|
|
84
|
+
'scopeLogs' => [
|
|
85
|
+
{
|
|
86
|
+
'scope' => { 'name' => 'confium.audit' },
|
|
87
|
+
'logRecords' => [log_record(record)]
|
|
88
|
+
}
|
|
89
|
+
]
|
|
90
|
+
}
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def log_record(record)
|
|
96
|
+
{
|
|
97
|
+
'timeUnixNano' => (parse_time(record['timestamp']) * 1_000_000_000).to_i.to_s,
|
|
98
|
+
'severityNumber' => SEVERITY.fetch(record['result'], 9),
|
|
99
|
+
'severityText' => record['result'] == 'success' ? 'INFO' : 'ERROR',
|
|
100
|
+
'body' => { 'stringValue' => "#{record['operation']} #{record['result']}".strip },
|
|
101
|
+
'attributes' => attributes_for(record)
|
|
102
|
+
}
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def attributes_for(record)
|
|
106
|
+
record.except('timestamp').map do |key, value|
|
|
107
|
+
{ 'key' => key, 'value' => { 'stringValue' => value.to_s } }
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def parse_time(timestamp)
|
|
112
|
+
return Time.now.to_f unless timestamp.is_a?(String)
|
|
113
|
+
|
|
114
|
+
Time.parse(timestamp).to_f
|
|
115
|
+
rescue ArgumentError
|
|
116
|
+
Time.now.to_f
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
data/lib/confium/audit.rb
CHANGED
data/lib/confium/composite.rb
CHANGED
|
@@ -16,6 +16,32 @@ module Confium
|
|
|
16
16
|
class Signature
|
|
17
17
|
BINARY_FIELDS = %w[public_key signature].freeze
|
|
18
18
|
|
|
19
|
+
# Serialize the components this instance was built from, in the
|
|
20
|
+
# canonical wire format. Instances remember their source: built
|
|
21
|
+
# from components, or from a JSON document (which is emitted
|
|
22
|
+
# verbatim when it was canonical).
|
|
23
|
+
def to_json(*_args)
|
|
24
|
+
cached = @confium_components_json
|
|
25
|
+
return cached if cached
|
|
26
|
+
|
|
27
|
+
components = @confium_components or
|
|
28
|
+
raise Confium::Error, 'Signature was not built with component data; use Signature.new or from_json'
|
|
29
|
+
Signature.components_to_json(components)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class << self
|
|
33
|
+
# magnus exposes the constructor as a class-level `new`;
|
|
34
|
+
# capture it before this reopen shadows it (plain `super`
|
|
35
|
+
# falls through to Class#new instead).
|
|
36
|
+
alias __native_new new
|
|
37
|
+
|
|
38
|
+
def new(*args)
|
|
39
|
+
sig = __native_new(*args)
|
|
40
|
+
sig.instance_variable_set(:@confium_components, args.first) if args.first.is_a?(Array)
|
|
41
|
+
sig
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
19
45
|
def self.components_to_json(components)
|
|
20
46
|
JSON.generate(components.map do |component|
|
|
21
47
|
component.transform_values do |value|
|
|
@@ -35,7 +61,19 @@ module Confium
|
|
|
35
61
|
raise ArgumentError, 'expected a non-empty "components" array'
|
|
36
62
|
end
|
|
37
63
|
|
|
38
|
-
new(components.map { |c| decode_binary_fields(c) })
|
|
64
|
+
sig = new(components.map { |c| decode_binary_fields(c) })
|
|
65
|
+
sig.instance_variable_set(:@confium_components, components)
|
|
66
|
+
sig.instance_variable_set(:@confium_components_json, canonical_json(json, data))
|
|
67
|
+
sig
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# The canonical wire form of whatever the caller handed us: a
|
|
71
|
+
# String passes through untouched; parsed structures are
|
|
72
|
+
# re-generated from their components array.
|
|
73
|
+
def self.canonical_json(json, data)
|
|
74
|
+
return json if json.is_a?(String)
|
|
75
|
+
|
|
76
|
+
JSON.generate(data.is_a?(Hash) ? data['components'] : data)
|
|
39
77
|
end
|
|
40
78
|
|
|
41
79
|
def self.decode_binary_fields(component)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Confium
|
|
4
|
+
# Pure resolution of which native-extension ABI windows to try, in
|
|
5
|
+
# order. Extracted from the loader so the decision — the piece with
|
|
6
|
+
# the release-incident history (3.1-window segfaults on 3.2, the
|
|
7
|
+
# cross-major 3.3→4.0 TypeError, windowless platform gems) — is
|
|
8
|
+
# testable without installing eight platform gems.
|
|
9
|
+
module NativeWindows
|
|
10
|
+
# Ordered candidate windows for +ruby_version+ (e.g. "3.4.8").
|
|
11
|
+
#
|
|
12
|
+
# Background: platform gems ship one extension per ABI window.
|
|
13
|
+
# Ruby 3.2 broke the 3.1 ABI (object shapes) and rb-sys references
|
|
14
|
+
# a VM pointer libruby stopped exporting in 3.3, so 3.1/3.2/4.0
|
|
15
|
+
# get exact-minor builds while a 3.3-window binary loads on both
|
|
16
|
+
# 3.3 and 3.4. The 3.3 fallback applies within the 3.x line
|
|
17
|
+
# alone: a cross-major load (4.x) fails TypedData class-identity
|
|
18
|
+
# checks, so 4.0 must not fall back to it.
|
|
19
|
+
def self.candidates(ruby_version)
|
|
20
|
+
minor = ruby_version[/\A\d+\.\d+/]
|
|
21
|
+
raise ArgumentError, "not a ruby version: #{ruby_version.inspect}" unless minor
|
|
22
|
+
|
|
23
|
+
major = ruby_version[/\A\d+/].to_i
|
|
24
|
+
if major == 3 && Gem::Version.new(ruby_version) >= Gem::Version.new('3.3')
|
|
25
|
+
[minor, '3.3'].uniq
|
|
26
|
+
else
|
|
27
|
+
[minor]
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Confium::TC::Coordinator
|
|
3
|
+
# Confium::TC::Coordinator coordinates an in-process threshold
|
|
4
|
+
# signing session: signers submit commitments and shares at their own
|
|
5
|
+
# pace; once the threshold is met, aggregation runs the real
|
|
6
|
+
# threshold-ECDSA combine (CMP20 or GG18) and returns a 64-byte
|
|
7
|
+
# (r, s) signature verifiable under the quorum public key.
|
|
4
8
|
#
|
|
5
|
-
# The
|
|
6
|
-
#
|
|
9
|
+
# The session semantics (state machine, duplicate-signer rejection,
|
|
10
|
+
# the combine) live in SigningSession; this class is the session
|
|
11
|
+
# registry and quorum-scoped entry point. NetworkCoordinator is the
|
|
12
|
+
# TCP/NDJSON adapter over the same sessions.
|
|
7
13
|
#
|
|
8
14
|
# Usage:
|
|
9
15
|
# coordinator = Confium::TC::Coordinator.new(quorum_id: "biml-root")
|
|
10
16
|
# session_id = coordinator.create_session(message: data,
|
|
11
|
-
# threshold:
|
|
12
|
-
#
|
|
17
|
+
# threshold: 3,
|
|
18
|
+
# scheme: "CMP20-ECDSA-P256")
|
|
13
19
|
# coordinator.submit_commitment(session_id, signer_id, commitment_bytes)
|
|
14
20
|
# # ... wait for T commitments ...
|
|
15
21
|
# coordinator.submit_share(session_id, signer_id, share_bytes)
|
|
@@ -25,43 +31,38 @@ module Confium
|
|
|
25
31
|
@sessions = {}
|
|
26
32
|
end
|
|
27
33
|
|
|
28
|
-
def create_session(message:, threshold:, unlock_window:
|
|
34
|
+
def create_session(message:, threshold:, unlock_window: SigningSession::DEFAULT_UNLOCK_WINDOW,
|
|
35
|
+
scheme: 'CMP20-ECDSA-P256')
|
|
29
36
|
session_id = "session-#{@sessions.length + 1}"
|
|
30
|
-
@sessions[session_id] =
|
|
31
|
-
message: message,
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
state: :pending,
|
|
35
|
-
commitments: [],
|
|
36
|
-
shares: []
|
|
37
|
-
}
|
|
37
|
+
@sessions[session_id] = SigningSession.new(
|
|
38
|
+
message: message, threshold: threshold,
|
|
39
|
+
unlock_window: unlock_window, scheme: scheme
|
|
40
|
+
)
|
|
38
41
|
session_id
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def session_state(session_id)
|
|
42
|
-
|
|
45
|
+
fetch(session_id).state
|
|
43
46
|
end
|
|
44
47
|
|
|
45
48
|
def submit_commitment(session_id, signer_id, commitment_bytes)
|
|
46
|
-
|
|
47
|
-
session[:commitments] << { signer_id: signer_id, bytes: commitment_bytes }
|
|
48
|
-
return unless session[:commitments].length >= session[:threshold]
|
|
49
|
-
|
|
50
|
-
session[:state] = :commitments_collected
|
|
49
|
+
fetch(session_id).add_commitment(signer_id, commitment_bytes)
|
|
51
50
|
end
|
|
52
51
|
|
|
53
52
|
def submit_share(session_id, signer_id, share_bytes)
|
|
54
|
-
|
|
55
|
-
session[:shares] << { signer_id: signer_id, bytes: share_bytes }
|
|
53
|
+
fetch(session_id).add_share(signer_id, share_bytes)
|
|
56
54
|
end
|
|
57
55
|
|
|
58
56
|
def aggregate(session_id)
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
fetch(session_id).aggregate
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
def fetch(session_id)
|
|
63
|
+
@sessions.fetch(session_id) do
|
|
64
|
+
raise NotFoundError.new("Unknown session: #{session_id}", kind: :session, identifier: session_id)
|
|
65
|
+
end
|
|
65
66
|
end
|
|
66
67
|
end
|
|
67
68
|
end
|