confium 0.5.0 → 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 +47 -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 +23 -6
- data/lib/confium/native_windows.rb +31 -0
- data/lib/confium/tc/coordinator.rb +24 -36
- data/lib/confium/tc/signing_session.rb +93 -0
- data/lib/confium/tc.rb +4 -6
- data/lib/confium/version.rb +1 -1
- data/lib/confium.rb +5 -16
- metadata +5 -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
|
+
}
|
|
@@ -16,19 +16,24 @@ module Confium
|
|
|
16
16
|
# service_name: 'confium-issuer'
|
|
17
17
|
# )
|
|
18
18
|
#
|
|
19
|
-
# Delivery is synchronous per record
|
|
20
|
-
#
|
|
21
|
-
#
|
|
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.
|
|
22
24
|
class OtlpSink < Sink
|
|
23
25
|
DEFAULT_ENDPOINT = 'http://localhost:4318/v1/logs'
|
|
24
26
|
SEVERITY = { 'success' => 9, 'failure' => 17, 'error' => 17 }.freeze # INFO / ERROR
|
|
25
27
|
|
|
26
|
-
def initialize(endpoint: DEFAULT_ENDPOINT, headers: {}, service_name: 'confium', timeout: 5
|
|
28
|
+
def initialize(endpoint: DEFAULT_ENDPOINT, headers: {}, service_name: 'confium', timeout: 5,
|
|
29
|
+
retries: 2, retry_base: 0.1)
|
|
27
30
|
super()
|
|
28
31
|
@uri = URI.parse(endpoint)
|
|
29
32
|
@headers = headers
|
|
30
33
|
@service_name = service_name
|
|
31
34
|
@timeout = timeout
|
|
35
|
+
@retries = retries
|
|
36
|
+
@retry_base = retry_base
|
|
32
37
|
@dropped = 0
|
|
33
38
|
end
|
|
34
39
|
|
|
@@ -37,8 +42,7 @@ module Confium
|
|
|
37
42
|
|
|
38
43
|
def write(record)
|
|
39
44
|
payload = envelope(record)
|
|
40
|
-
|
|
41
|
-
raise "collector responded #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
45
|
+
deliver(JSON.generate(payload))
|
|
42
46
|
rescue StandardError => e
|
|
43
47
|
@dropped += 1
|
|
44
48
|
warn "confium: OTLP delivery failed, audit record dropped (##{@dropped}): #{e.class}: #{e.message}"
|
|
@@ -48,6 +52,19 @@ module Confium
|
|
|
48
52
|
|
|
49
53
|
private
|
|
50
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
|
+
|
|
51
68
|
def headers
|
|
52
69
|
{ 'User-Agent' => "confium-otlp-sink #{Confium::VERSION}" }.merge(@headers)
|
|
53
70
|
end
|
|
@@ -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
|
|
@@ -3,9 +3,13 @@
|
|
|
3
3
|
# Confium::TC::Coordinator coordinates an in-process threshold
|
|
4
4
|
# signing session: signers submit commitments and shares at their own
|
|
5
5
|
# pace; once the threshold is met, aggregation runs the real
|
|
6
|
-
# threshold-ECDSA combine (CMP20 or GG18
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# threshold-ECDSA combine (CMP20 or GG18) and returns a 64-byte
|
|
7
|
+
# (r, s) signature verifiable under the quorum public key.
|
|
8
|
+
#
|
|
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.
|
|
9
13
|
#
|
|
10
14
|
# Usage:
|
|
11
15
|
# coordinator = Confium::TC::Coordinator.new(quorum_id: "biml-root")
|
|
@@ -27,54 +31,38 @@ module Confium
|
|
|
27
31
|
@sessions = {}
|
|
28
32
|
end
|
|
29
33
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
'GG18-ECDSA-P256' => ->(shares, threshold, message) { Gg18.sign(shares, threshold, message) }
|
|
33
|
-
}.freeze
|
|
34
|
-
|
|
35
|
-
def create_session(message:, threshold:, unlock_window: 14_400, scheme: 'CMP20-ECDSA-P256')
|
|
36
|
-
raise ArgumentError, "unknown scheme: #{scheme} (known: #{SCHEMES.keys.join(', ')})" unless SCHEMES.key?(scheme)
|
|
37
|
-
|
|
34
|
+
def create_session(message:, threshold:, unlock_window: SigningSession::DEFAULT_UNLOCK_WINDOW,
|
|
35
|
+
scheme: 'CMP20-ECDSA-P256')
|
|
38
36
|
session_id = "session-#{@sessions.length + 1}"
|
|
39
|
-
@sessions[session_id] =
|
|
40
|
-
message: message,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
scheme: scheme,
|
|
44
|
-
state: :pending,
|
|
45
|
-
commitments: [],
|
|
46
|
-
shares: []
|
|
47
|
-
}
|
|
37
|
+
@sessions[session_id] = SigningSession.new(
|
|
38
|
+
message: message, threshold: threshold,
|
|
39
|
+
unlock_window: unlock_window, scheme: scheme
|
|
40
|
+
)
|
|
48
41
|
session_id
|
|
49
42
|
end
|
|
50
43
|
|
|
51
44
|
def session_state(session_id)
|
|
52
|
-
|
|
45
|
+
fetch(session_id).state
|
|
53
46
|
end
|
|
54
47
|
|
|
55
48
|
def submit_commitment(session_id, signer_id, commitment_bytes)
|
|
56
|
-
|
|
57
|
-
session[:commitments] << { signer_id: signer_id, bytes: commitment_bytes }
|
|
58
|
-
return unless session[:commitments].length >= session[:threshold]
|
|
59
|
-
|
|
60
|
-
session[:state] = :commitments_collected
|
|
49
|
+
fetch(session_id).add_commitment(signer_id, commitment_bytes)
|
|
61
50
|
end
|
|
62
51
|
|
|
63
52
|
def submit_share(session_id, signer_id, share_bytes)
|
|
64
|
-
|
|
65
|
-
session[:shares] << { signer_id: signer_id, bytes: share_bytes }
|
|
53
|
+
fetch(session_id).add_share(signer_id, share_bytes)
|
|
66
54
|
end
|
|
67
55
|
|
|
68
56
|
def aggregate(session_id)
|
|
69
|
-
|
|
70
|
-
|
|
57
|
+
fetch(session_id).aggregate
|
|
58
|
+
end
|
|
71
59
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
session
|
|
77
|
-
|
|
60
|
+
private
|
|
61
|
+
|
|
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
|
|
78
66
|
end
|
|
79
67
|
end
|
|
80
68
|
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Confium
|
|
4
|
+
module TC
|
|
5
|
+
# One threshold-signing session: the state machine and the combine
|
|
6
|
+
# behind every coordinator surface. Coordinator (in-process) and
|
|
7
|
+
# NetworkCoordinator (TCP/NDJSON) are adapters over this class —
|
|
8
|
+
# they own registries and framing; the session owns the semantics:
|
|
9
|
+
#
|
|
10
|
+
# pending → commitments_collected (T distinct commitments)
|
|
11
|
+
# → completed (aggregate runs the scheme combine)
|
|
12
|
+
#
|
|
13
|
+
# Aggregate may be called again after completion — the combine
|
|
14
|
+
# randomizes its nonce, so each call yields a fresh (verifiable)
|
|
15
|
+
# signature over the same shares, which is what a network client
|
|
16
|
+
# retrying after a dropped response needs.
|
|
17
|
+
#
|
|
18
|
+
# Signer identity is load-bearing: one signer submitting two
|
|
19
|
+
# shares must never count twice toward the threshold, so
|
|
20
|
+
# commitments and shares are keyed by signer_id and duplicates
|
|
21
|
+
# raise.
|
|
22
|
+
class SigningSession
|
|
23
|
+
DEFAULT_UNLOCK_WINDOW = 14_400
|
|
24
|
+
|
|
25
|
+
SCHEMES = {
|
|
26
|
+
'CMP20-ECDSA-P256' => ->(shares, threshold, message) { Cmp20.sign(shares, threshold, message) },
|
|
27
|
+
'GG18-ECDSA-P256' => ->(shares, threshold, message) { Gg18.sign(shares, threshold, message) }
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
attr_reader :message, :threshold, :unlock_window, :scheme, :state
|
|
31
|
+
|
|
32
|
+
def initialize(message:, threshold:, unlock_window: DEFAULT_UNLOCK_WINDOW, scheme: 'CMP20-ECDSA-P256')
|
|
33
|
+
raise ArgumentError, "unknown scheme: #{scheme} (known: #{SCHEMES.keys.join(', ')})" unless SCHEMES.key?(scheme)
|
|
34
|
+
|
|
35
|
+
@message = message
|
|
36
|
+
@threshold = threshold
|
|
37
|
+
@unlock_window = unlock_window
|
|
38
|
+
@scheme = scheme
|
|
39
|
+
@state = :pending
|
|
40
|
+
# @type ivar @commitments: Hash[String, String]
|
|
41
|
+
@commitments = {}
|
|
42
|
+
# @type ivar @shares: Hash[String, String]
|
|
43
|
+
@shares = {}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def add_commitment(signer_id, commitment_bytes)
|
|
47
|
+
record(@commitments, signer_id, commitment_bytes)
|
|
48
|
+
@state = :commitments_collected if @commitments.length >= @threshold
|
|
49
|
+
self
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def add_share(signer_id, share_bytes)
|
|
53
|
+
record(@shares, signer_id, share_bytes)
|
|
54
|
+
self
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def commitment_count
|
|
58
|
+
@commitments.length
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def share_count
|
|
62
|
+
@shares.length
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def threshold_met?
|
|
66
|
+
@shares.length >= @threshold
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def aggregate
|
|
70
|
+
unless threshold_met?
|
|
71
|
+
raise ThresholdError.new('Threshold not met', have_count: @shares.length, need_count: @threshold)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
signature = SCHEMES.fetch(@scheme).call(@shares.values, @threshold, @message)
|
|
75
|
+
@state = :completed
|
|
76
|
+
signature
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def record(store, signer_id, bytes)
|
|
82
|
+
if store.key?(signer_id)
|
|
83
|
+
raise ValidationError.new(
|
|
84
|
+
"duplicate submission from signer #{signer_id}",
|
|
85
|
+
param: :signer_id, expected: 'one submission per signer', actual: signer_id
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
store[signer_id] = bytes
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/confium/tc.rb
CHANGED
|
@@ -11,12 +11,10 @@
|
|
|
11
11
|
# for the full interface specification.
|
|
12
12
|
module Confium
|
|
13
13
|
module TC
|
|
14
|
-
# The native extension defines Confium::TC; this namespace file
|
|
15
|
-
# eager-required from confium.rb, so autoloads
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
# external libconfium dylib, so it stays lazy.
|
|
19
|
-
autoload :Session, 'confium/tc/session'
|
|
14
|
+
# The native extension defines Confium::TC; this namespace file
|
|
15
|
+
# is eager-required from confium.rb, so autoloads never fire —
|
|
16
|
+
# the pure-Ruby companions load with the namespace directly.
|
|
17
|
+
require_relative 'tc/signing_session'
|
|
20
18
|
require_relative 'tc/coordinator'
|
|
21
19
|
require_relative 'tc/network_coordinator'
|
|
22
20
|
require_relative 'tc/share_file'
|
data/lib/confium/version.rb
CHANGED
data/lib/confium.rb
CHANGED
|
@@ -10,25 +10,14 @@
|
|
|
10
10
|
# are loaded lazily via autoload — see lib/confium/<name>.rb.
|
|
11
11
|
|
|
12
12
|
require_relative 'confium/version'
|
|
13
|
+
require_relative 'confium/native_windows'
|
|
13
14
|
|
|
14
15
|
begin
|
|
15
|
-
# Pre-built platform gems ship one extension per
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
# 3.1 and 3.2 get exact-minor builds while 3.3 covers 3.3+).
|
|
19
|
-
# Source builds install the extension flat, without a version
|
|
20
|
-
# directory.
|
|
21
|
-
minor = RUBY_VERSION[/\A\d+\.\d+/]
|
|
16
|
+
# Pre-built platform gems ship one extension per ABI window (the
|
|
17
|
+
# window rules live in Confium::NativeWindows). Source builds
|
|
18
|
+
# install the extension flat, without a version directory.
|
|
22
19
|
dlext = RbConfig::CONFIG['DLEXT'] || 'so'
|
|
23
|
-
|
|
24
|
-
# 3.3-window binaries load on 3.3/3.4 only; a cross-major load
|
|
25
|
-
# (4.x) fails TypedData class checks, so the fallback applies
|
|
26
|
-
# within the 3.x line alone.
|
|
27
|
-
candidates = major == 3 && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3') ? [minor, '3.3'].uniq : [minor]
|
|
28
|
-
# Windows gems carry an exact-minor window per Ruby (a PE import
|
|
29
|
-
# names the version-specific ruby DLL); other platforms share the
|
|
30
|
-
# 3.3 window for 3.3+. Prefer the exact minor when present.
|
|
31
|
-
windowed = candidates.filter_map do |w|
|
|
20
|
+
windowed = Confium::NativeWindows.candidates(RUBY_VERSION).filter_map do |w|
|
|
32
21
|
path = File.expand_path("confium_native/#{w}/confium_native.#{dlext}", __dir__ || '.')
|
|
33
22
|
w if File.exist?(path)
|
|
34
23
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: confium
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.6.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Open
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-25 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rb_sys
|
|
@@ -121,6 +121,7 @@ files:
|
|
|
121
121
|
- ext/confium_native/src/deployment.rs
|
|
122
122
|
- ext/confium_native/src/ers.rs
|
|
123
123
|
- ext/confium_native/src/lib.rs
|
|
124
|
+
- ext/confium_native/src/openpgp_verify.rs
|
|
124
125
|
- ext/confium_native/src/path.rs
|
|
125
126
|
- ext/confium_native/src/pki.rs
|
|
126
127
|
- ext/confium_native/src/tc.rs
|
|
@@ -129,10 +130,7 @@ files:
|
|
|
129
130
|
- lib/confium.rb
|
|
130
131
|
- lib/confium/audit.rb
|
|
131
132
|
- lib/confium/audit/otlp_sink.rb
|
|
132
|
-
- lib/confium/cfm.rb
|
|
133
133
|
- lib/confium/composite.rb
|
|
134
|
-
- lib/confium/crypto.rb
|
|
135
|
-
- lib/confium/digest.rb
|
|
136
134
|
- lib/confium/errors.rb
|
|
137
135
|
- lib/confium/errors/coerce.rb
|
|
138
136
|
- lib/confium/errors/crypto_error.rb
|
|
@@ -144,8 +142,7 @@ files:
|
|
|
144
142
|
- lib/confium/errors/unresolved_signer_error.rb
|
|
145
143
|
- lib/confium/errors/validation_error.rb
|
|
146
144
|
- lib/confium/errors/verification_error.rb
|
|
147
|
-
- lib/confium/
|
|
148
|
-
- lib/confium/lib.rb
|
|
145
|
+
- lib/confium/native_windows.rb
|
|
149
146
|
- lib/confium/openpgp.rb
|
|
150
147
|
- lib/confium/pki.rb
|
|
151
148
|
- lib/confium/pki/certificate_builder.rb
|
|
@@ -157,9 +154,8 @@ files:
|
|
|
157
154
|
- lib/confium/tc.rb
|
|
158
155
|
- lib/confium/tc/coordinator.rb
|
|
159
156
|
- lib/confium/tc/network_coordinator.rb
|
|
160
|
-
- lib/confium/tc/session.rb
|
|
161
|
-
- lib/confium/tc/session_stub.rb
|
|
162
157
|
- lib/confium/tc/share_file.rb
|
|
158
|
+
- lib/confium/tc/signing_session.rb
|
|
163
159
|
- lib/confium/transparency.rb
|
|
164
160
|
- lib/confium/transparency/ots.rb
|
|
165
161
|
- lib/confium/version.rb
|
data/lib/confium/cfm.rb
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'ffi'
|
|
4
|
-
|
|
5
|
-
module Confium
|
|
6
|
-
class CFM
|
|
7
|
-
attr_reader :ptr
|
|
8
|
-
|
|
9
|
-
def initialize
|
|
10
|
-
pptr = ::FFI::MemoryPointer.new(:pointer)
|
|
11
|
-
Confium.call_ffi(:cfm_create, pptr)
|
|
12
|
-
@ptr = ::FFI::AutoPointer.new(pptr.read_pointer, self.class.method(:destroy))
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.destroy(ptr)
|
|
16
|
-
Confium::Lib.cfm_destroy(ptr)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def load_plugin(name, path)
|
|
20
|
-
Confium.call_ffi(:cfm_plugin_load, @ptr, name, path, nil, nil)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|