confium 0.6.2 → 0.7.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 +43 -0
- data/Cargo.lock +519 -80
- data/README.adoc +72 -4
- data/confium.gemspec +2 -0
- data/ext/confium_native/Cargo.toml +12 -4
- data/ext/confium_native/src/lib.rs +24 -0
- data/ext/confium_native/src/net.rs +125 -0
- data/ext/confium_native/src/tc_session.rs +268 -0
- data/ext/confium_native/src/winsock.rs +784 -0
- data/lib/confium/version.rb +1 -1
- metadata +5 -2
data/README.adoc
CHANGED
|
@@ -18,7 +18,7 @@ Add to your Gemfile:
|
|
|
18
18
|
|
|
19
19
|
[source,ruby]
|
|
20
20
|
----
|
|
21
|
-
gem "confium", "~> 0.
|
|
21
|
+
gem "confium", "~> 0.7"
|
|
22
22
|
----
|
|
23
23
|
|
|
24
24
|
Or install directly:
|
|
@@ -78,18 +78,38 @@ kp = Confium::TC::FrostP256.generate_keypair
|
|
|
78
78
|
shares = Confium::TC::FrostP256.split_secret(kp["private_key"], 3, 5)
|
|
79
79
|
recovered = Confium::TC::FrostP256.recover_secret(shares.first(3).map { |s| { "x" => s.x, "y" => s.y_bytes } })
|
|
80
80
|
recovered == kp["private_key"] # => true
|
|
81
|
+
|
|
82
|
+
# Distributed key generation + 2-of-3 FROST-ed25519 signing (each party
|
|
83
|
+
# holds its own Confium::TC::Session and only exchanges round messages)
|
|
84
|
+
parties = %w[alice bob carol]
|
|
85
|
+
sessions = parties.each_with_index.map do |id, i|
|
|
86
|
+
Confium::TC::Session.new("FROST-ed25519-dkg", parties: parties, threshold: 2, this_party_idx: i)
|
|
87
|
+
end
|
|
88
|
+
# run rounds: session.round_step(messages_in) -> {"outgoing" => [...], "complete" => bool}
|
|
89
|
+
# broadcast "outgoing" payloads between sessions until every session is complete
|
|
90
|
+
dkg_share = sessions.first.result # this party's share (embeds the group public key)
|
|
91
|
+
|
|
92
|
+
signers = sessions.first(2).map.with_index do |s, i|
|
|
93
|
+
Confium::TC::Session.new("FROST-ed25519",
|
|
94
|
+
parties: parties.first(2), threshold: 2, this_party_idx: i,
|
|
95
|
+
local_share: dkg_share, message: "authentic message")
|
|
96
|
+
end
|
|
97
|
+
# run rounds as above; the completed session's #result is an RFC 8032
|
|
98
|
+
# Ed25519 signature verifiable under the group public key
|
|
81
99
|
----
|
|
82
100
|
|
|
83
|
-
== API surface (v0.
|
|
101
|
+
== API surface (v0.7)
|
|
84
102
|
|
|
85
103
|
=== `Confium::Transparency`
|
|
86
104
|
- `MerkleTree.new` / `#append(artifact_hash)` / `#root` / `#length` / `#empty?` / `#inclusion_proof(seq)`
|
|
87
105
|
- `InclusionProof#sequence` / `#steps` / `#verify(root)`
|
|
106
|
+
- `Ots.stamp(hash)` / `Ots.verify(receipt, hash)` — OpenTimestamps anchoring interface (pure-Ruby stub; calendar-server wiring lands with the Rust client)
|
|
88
107
|
|
|
89
108
|
=== `Confium::Composite` — PQ migration
|
|
90
109
|
- `.generate_ed25519_keypair` → `{ private_key:, public_key: }`
|
|
91
110
|
- `.sign_ed25519(private_key, message)` → component Hash
|
|
92
|
-
- `Signature.new(components)` / `#verify(message)` / `#component_count` / `#algorithms`
|
|
111
|
+
- `Signature.new(components)` / `#verify(message)` / `#component_count` / `#algorithms` / `#to_json`
|
|
112
|
+
- `.from_json(json)` / `.components_to_json(components)` / `.canonical_json(json, data)`
|
|
93
113
|
- `VerificationResult#all_verified?` / `#per_component`
|
|
94
114
|
|
|
95
115
|
=== `Confium::Attributes` — threshold policy DSL
|
|
@@ -99,11 +119,14 @@ recovered == kp["private_key"] # => true
|
|
|
99
119
|
- DSL: `min_count("attr", n)`, `min_distinct("attr", n)`, `any("attr")`, `all("attr")`, `none("attr")`, `and(...)`, `or(...)`, `not(p)`
|
|
100
120
|
|
|
101
121
|
=== `Confium::PKI`
|
|
102
|
-
- `Certificate.from_der(bytes)` / `.from_pem(str)` / `#to_der` / `#to_pem` / `#fingerprint_sha256` / `#serial_hex` / `#not_before` / `#not_after` / `#valid_at?(iso8601)` / `#public_key_bytes`
|
|
122
|
+
- `Certificate.from_der(bytes)` / `.from_pem(str)` / `#to_der` / `#to_pem` / `#fingerprint_sha256` / `#serial_hex` / `#not_before` / `#not_after` / `#valid_at?(iso8601)` / `#public_key_bytes`; DER parse failures carry a byte `offset` in the error details
|
|
123
|
+
- `CertificateBuilder` / `CMS::SignedDataBuilder` — build certificates and signed data in Ruby
|
|
124
|
+
- `PathValidator.validate(leaf, intermediates, root, now_iso8601 = nil)` → `PathValidationResult` (chain validation leaf → root at a point in time)
|
|
103
125
|
- `CSR.from_der` / `.from_pem` / `#to_der` / `#to_pem`
|
|
104
126
|
- `CMS::SignedData.from_json` / `#to_json` / `#signer_count` / `#content_type` / `#content` / `#certificate_count` / `#certificate_at(i)`
|
|
105
127
|
- `CMS::Content#bytes` / `#length`
|
|
106
128
|
- `XMLDSig.canonicalize(xml)` / `.canonicalize_exclusive(xml)` (Canonical XML RFC 3076 + Exclusive C14N)
|
|
129
|
+
- `Cnml` — institutional certificate workflow helpers
|
|
107
130
|
|
|
108
131
|
=== `Confium::Identity` — actor roles
|
|
109
132
|
- `.actor_types` → `["manufacturer", "testing_lab", "issuing_authority_officer", "biml_director", "quorum_coordinator", "verifier"]`
|
|
@@ -123,6 +146,51 @@ recovered == kp["private_key"] # => true
|
|
|
123
146
|
- `.partial_decrypt(party_index, share_bytes, ciphertext)` → `{ party_index:, bytes: }`
|
|
124
147
|
- `.aggregate_partials(partials, threshold, ciphertext)` → shared_secret bytes
|
|
125
148
|
|
|
149
|
+
=== `Confium::TC::Cmp20` / `Confium::TC::Gg18` — threshold ECDSA protocols
|
|
150
|
+
- `.keygen(threshold, parties)` → `{ public_key, shares }`
|
|
151
|
+
- `.sign(shares, public_key, message)` → DER ECDSA signature
|
|
152
|
+
|
|
153
|
+
=== `Confium::TC::Session` — per-party threshold sessions (real FROST)
|
|
154
|
+
- `Session.new(scheme, parties:, threshold:, this_party_idx:, local_share: nil, message: nil)` — `"FROST-ed25519-dkg"` for distributed key generation, `"FROST-ed25519"` for signing (pass the DKG share and the message)
|
|
155
|
+
- `#scheme_name` / `#threshold` / `#party_count` / `#this_party_idx` / `#round` / `#complete?`
|
|
156
|
+
- `#round_step(messages)` → `{ "outgoing" => [{from, to, round, payload}...], "complete" => bool }` — feed each round's incoming messages, broadcast the outgoing ones
|
|
157
|
+
- `#result` — after DKG: share blob (group public key + party share); after signing: RFC 8032 Ed25519 signature
|
|
158
|
+
|
|
159
|
+
=== `Confium::TC::Coordinator` — quorum signing orchestration
|
|
160
|
+
- `Coordinator.new(quorum_id:)` / `#create_session(message:, threshold:, scheme: "CMP20-ECDSA-P256")` / `#submit_commitment` / `#submit_share` / `#aggregate` / `#session_state`
|
|
161
|
+
- `SigningSession#add_commitment` / `#add_share` / `#threshold_met?` / `#aggregate` — the seam every transport adapts to
|
|
162
|
+
- `NetworkCoordinator` — TCP/NDJSON server (`#start` / `#stop` / `#running?`); `SignerClient` — remote submit + aggregate with typed `RemoteError`
|
|
163
|
+
- `ShareFile` — encrypted share-file persistence
|
|
164
|
+
|
|
165
|
+
=== `Confium::Store::Keystore` — key handles and remote signing
|
|
166
|
+
- `Keystore.new(backend, **options)` / `.backends`
|
|
167
|
+
- `#sign(key_id, algorithm, message)` → signature bytes (sign-with-handle contract; cloud KMS backends `aws-kms` / `gcp-kms` / `azure-keyvault` via cargo features)
|
|
168
|
+
|
|
169
|
+
=== `Confium::Audit` — structured audit trail
|
|
170
|
+
- `Audit.sink =` / `Audit.sink` / `Audit.enabled?` / `Audit.record(...)`
|
|
171
|
+
- `Sink` (base) / `MemorySink` / `StderrSink` / `FileSink`
|
|
172
|
+
- `OtlpSink.new(endpoint:, headers:, service_name:, ...)` — OTLP/HTTP export with retry and `#dropped` counter
|
|
173
|
+
|
|
174
|
+
=== `Confium::OpenPGP`
|
|
175
|
+
- `.armor(data, type)` / `.dearmor(data)` — RFC 9580 §6 ASCII armor with CRC-24 (pure Ruby)
|
|
176
|
+
- `.verify_detached(message, signature, keys)` / `.verify(...)` — real signature verification when the extension is built with the `pgp` feature; `PGP_AVAILABLE` reports it
|
|
177
|
+
|
|
178
|
+
=== `Confium::Transport` — coordinator clients over any transport
|
|
179
|
+
- `SignerClient.new(url)` — connect over any registry transport: `tcp://host:port` (local/trusted) or `noise://host:port?key=<hex>&pinned=<hex>` (Noise_XX encrypted, stable identity + peer pinning)
|
|
180
|
+
- `#register(signer_id, quorum_id)` / `#create_session(quorum_id, scheme, message, threshold, num_parties)` → session id
|
|
181
|
+
- `#submit_commitment(session_id, signer_id, bytes)` / `#submit_share(session_id, signer_id, bytes)`
|
|
182
|
+
- `CoordinatorServer.new(url)` — serve coordinator sessions over any linked scheme (test/in-process use)
|
|
183
|
+
|
|
184
|
+
=== `Confium::ERS` — evidence records
|
|
185
|
+
- `EvidenceRecord.build_initial(...)` / `#renew(...)` / `#renewal_count` (RFC 4998 / RFC 6283 archival)
|
|
186
|
+
|
|
187
|
+
=== `Confium::Policy` / `Confium::SecureBytes`
|
|
188
|
+
- `Policy.jurisdiction = :eu` — jurisdictional algorithm policy (EU/US profiles; SM2/SM3/SM4 for CN planned)
|
|
189
|
+
- `SecureBytes.wrap(raw)` — zeroize-on-clear wrapper for private keys, shares, and secrets (`#bytes` non-destructive read, `#clear`)
|
|
190
|
+
|
|
191
|
+
=== Errors
|
|
192
|
+
- Typed error hierarchy (`ParseError`, `ThresholdError`, `VerificationError`, `PolicyViolationError`, ...) — native failures surface positional `details` hashes (including DER byte offsets for parse errors)
|
|
193
|
+
|
|
126
194
|
== Development
|
|
127
195
|
|
|
128
196
|
After checking out the repo:
|
data/confium.gemspec
CHANGED
|
@@ -40,6 +40,8 @@ Gem::Specification.new do |spec|
|
|
|
40
40
|
]
|
|
41
41
|
spec.files.reject! { |f| File.directory?(f) }
|
|
42
42
|
spec.files.reject! { |f| f =~ /\.(dll|so|dylib|lib|bundle)\Z/ }
|
|
43
|
+
# Windows socket diagnostic crate — development-only, never shipped.
|
|
44
|
+
spec.files.reject! { |f| f.start_with?('ext/socket-smoke/') }
|
|
43
45
|
spec.require_paths = ['lib']
|
|
44
46
|
|
|
45
47
|
spec.required_ruby_version = '>= 3.1.0'
|
|
@@ -25,6 +25,9 @@ build = "build.rs"
|
|
|
25
25
|
pgp = ["rnp"]
|
|
26
26
|
|
|
27
27
|
[dependencies]
|
|
28
|
+
confium-coordinator = "0.8"
|
|
29
|
+
confium-net-noise = "0.8"
|
|
30
|
+
confium-net-tcp = "0.8"
|
|
28
31
|
# rb-sys for Ruby C API access.
|
|
29
32
|
rb-sys = { version = "0.9.124", features = ["global-allocator"] }
|
|
30
33
|
|
|
@@ -40,10 +43,15 @@ confium-pki = "0.5.6"
|
|
|
40
43
|
confium-store = "0.5.8"
|
|
41
44
|
confium-deployment = "0.3"
|
|
42
45
|
confium-tc = "0.3.1"
|
|
43
|
-
confium-tc-frost-p256 = "0.
|
|
44
|
-
confium-tc-elgamal-p256 = "0.
|
|
45
|
-
confium-tc-cmp20 = "0.
|
|
46
|
-
confium-tc-gg18 = "0.
|
|
46
|
+
confium-tc-frost-p256 = "0.4"
|
|
47
|
+
confium-tc-elgamal-p256 = "0.4"
|
|
48
|
+
confium-tc-cmp20 = "0.4"
|
|
49
|
+
confium-tc-gg18 = "0.4"
|
|
50
|
+
|
|
51
|
+
# Per-party session protocol: the tc-core 0.4 registry line. The 0.3-era
|
|
52
|
+
# in-process drivers above keep their own registry harmlessly.
|
|
53
|
+
confium-tc-session = { package = "confium-tc-core", version = "0.4.7" }
|
|
54
|
+
confium-tc-frost-ed25519 = "0.4.7"
|
|
47
55
|
|
|
48
56
|
# Newly published shared crypto crates (confium product restructuring).
|
|
49
57
|
confium-tc-core = "0.3"
|
|
@@ -14,9 +14,14 @@ mod path;
|
|
|
14
14
|
mod pki;
|
|
15
15
|
mod store;
|
|
16
16
|
mod tc;
|
|
17
|
+
mod net;
|
|
18
|
+
mod tc_session;
|
|
17
19
|
mod transparency;
|
|
18
20
|
mod util;
|
|
19
21
|
|
|
22
|
+
#[cfg(windows)]
|
|
23
|
+
mod winsock;
|
|
24
|
+
|
|
20
25
|
use magnus::{function, Error, Module, Ruby};
|
|
21
26
|
|
|
22
27
|
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
|
@@ -29,6 +34,19 @@ fn native_loaded() -> bool {
|
|
|
29
34
|
true
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
/// `Confium::Native.winsock_probe(tag)` — run the winsock diagnostic
|
|
38
|
+
/// sequence at an arbitrary point (Windows only; no-op elsewhere).
|
|
39
|
+
/// Timeline bisect for the listener bind failure: call it at ext
|
|
40
|
+
/// load, suite start, and just before a ceremony to see exactly when
|
|
41
|
+
/// each std net operation degrades.
|
|
42
|
+
fn winsock_probe_fn(tag: String) -> Result<(), Error> {
|
|
43
|
+
#[cfg(windows)]
|
|
44
|
+
winsock::probe_on_demand(&tag);
|
|
45
|
+
#[cfg(not(windows))]
|
|
46
|
+
let _ = tag;
|
|
47
|
+
Ok(())
|
|
48
|
+
}
|
|
49
|
+
|
|
32
50
|
fn core_version() -> &'static str {
|
|
33
51
|
// Set by build.rs at compile time from Cargo.lock. Always matches the
|
|
34
52
|
// confium-core crate version the extension was built against.
|
|
@@ -37,11 +55,15 @@ fn core_version() -> &'static str {
|
|
|
37
55
|
|
|
38
56
|
#[magnus::init]
|
|
39
57
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
58
|
+
#[cfg(windows)]
|
|
59
|
+
winsock::probe();
|
|
60
|
+
|
|
40
61
|
let confium = ruby.define_module("Confium")?;
|
|
41
62
|
let native = confium.define_module("Native")?;
|
|
42
63
|
native.define_module_function("version", function!(native_version, 0))?;
|
|
43
64
|
native.define_module_function("loaded?", function!(native_loaded, 0))?;
|
|
44
65
|
confium.define_module_function("core_version", function!(core_version, 0))?;
|
|
66
|
+
native.define_module_function("winsock_probe", function!(winsock_probe_fn, 1))?;
|
|
45
67
|
|
|
46
68
|
transparency::init(ruby, confium)?;
|
|
47
69
|
openpgp_verify::init(ruby, confium)?;
|
|
@@ -54,6 +76,8 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
54
76
|
path::init(ruby, confium)?;
|
|
55
77
|
deployment::init(ruby, confium)?;
|
|
56
78
|
tc::init(ruby, confium)?;
|
|
79
|
+
tc_session::init(ruby, confium)?;
|
|
80
|
+
net::init(ruby, confium)?;
|
|
57
81
|
audit::init(ruby, confium)?;
|
|
58
82
|
ers::init(ruby, confium)?;
|
|
59
83
|
Ok(())
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//! Confium::Transport — transport-level coordinator clients.
|
|
2
|
+
//!
|
|
3
|
+
//! Binds the registry-transport coordinator surface: a signer dials
|
|
4
|
+
//! the coordinator over any URL scheme the native library links
|
|
5
|
+
//! (tcp:// for local/trusted, noise:// for encrypted sessions with
|
|
6
|
+
//! key=/pinned= parameters), and a CoordinatorServer serves any
|
|
7
|
+
//! linked scheme. This is the transport half of multi-host signing;
|
|
8
|
+
//! the protocol half is Confium::TC::Session.
|
|
9
|
+
|
|
10
|
+
use magnus::{prelude::*, DataTypeFunctions, Error, Ruby, TypedData};
|
|
11
|
+
|
|
12
|
+
// Force the noise transport crate (and its `register_transport!`
|
|
13
|
+
// static) into the cdylib so the noise:// scheme resolves — the
|
|
14
|
+
// binding itself never names a symbol from it.
|
|
15
|
+
extern crate confium_net_noise as _noise_link;
|
|
16
|
+
extern crate confium_net_tcp as _tcp_link;
|
|
17
|
+
|
|
18
|
+
use confium_coordinator::coordinator::client::SignerClient as RustSignerClient;
|
|
19
|
+
use confium_coordinator::coordinator::net_server::CoordinatorServer as RustCoordinatorServer;
|
|
20
|
+
|
|
21
|
+
fn io_error(e: std::io::Error, operation: &str) -> Error {
|
|
22
|
+
magnus::Error::new(
|
|
23
|
+
magnus::exception::io_error(),
|
|
24
|
+
format!("{operation}: {e}"),
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Confium::Transport::SignerClient — a coordinator connection over a
|
|
29
|
+
/// registry transport URL.
|
|
30
|
+
#[derive(TypedData, DataTypeFunctions)]
|
|
31
|
+
#[magnus(class = "Confium::Transport::SignerClient", size)]
|
|
32
|
+
pub struct SignerClient {
|
|
33
|
+
inner: std::cell::RefCell<RustSignerClient>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
impl SignerClient {
|
|
37
|
+
fn initialize(ruby: &Ruby, url: String) -> Result<Self, Error> {
|
|
38
|
+
let _ = ruby;
|
|
39
|
+
let inner = RustSignerClient::connect_url(&url).map_err(|e| io_error(e, "SignerClient.new"))?;
|
|
40
|
+
Ok(Self { inner: std::cell::RefCell::new(inner) })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn register(&self, signer_id: String, quorum_id: String) -> Result<(), Error> {
|
|
44
|
+
self.inner
|
|
45
|
+
.borrow_mut()
|
|
46
|
+
.register(&signer_id, &quorum_id)
|
|
47
|
+
.map_err(|e| io_error(e, "register"))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn create_session(
|
|
51
|
+
&self,
|
|
52
|
+
quorum_id: String,
|
|
53
|
+
scheme: String,
|
|
54
|
+
message: String,
|
|
55
|
+
threshold: u32,
|
|
56
|
+
num_parties: u32,
|
|
57
|
+
) -> Result<String, Error> {
|
|
58
|
+
self.inner
|
|
59
|
+
.borrow_mut()
|
|
60
|
+
.create_session(&quorum_id, &scheme, message.as_bytes(), threshold, num_parties)
|
|
61
|
+
.map_err(|e| io_error(e, "create_session"))
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn submit_commitment(&self, args: &[magnus::Value]) -> Result<(), Error> {
|
|
65
|
+
let scanned = magnus::scan_args::scan_args::<(String, String, Vec<u8>), (), (), (), (), ()>(args)
|
|
66
|
+
.map_err(|e| magnus::Error::new(magnus::exception::arg_error(), e.to_string()))?;
|
|
67
|
+
let (session_id, signer_id, commitment) = scanned.required;
|
|
68
|
+
self.inner
|
|
69
|
+
.borrow_mut()
|
|
70
|
+
.submit_commitment(&session_id, &signer_id, &commitment)
|
|
71
|
+
.map_err(|e| io_error(e, "submit_commitment"))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
fn submit_share(&self, args: &[magnus::Value]) -> Result<Option<Vec<u8>>, Error> {
|
|
75
|
+
let scanned = magnus::scan_args::scan_args::<(String, String, Vec<u8>), (), (), (), (), ()>(args)
|
|
76
|
+
.map_err(|e| magnus::Error::new(magnus::exception::arg_error(), e.to_string()))?;
|
|
77
|
+
let (session_id, signer_id, share) = scanned.required;
|
|
78
|
+
self.inner
|
|
79
|
+
.borrow_mut()
|
|
80
|
+
.submit_share(&session_id, &signer_id, &share)
|
|
81
|
+
.map_err(|e| io_error(e, "submit_share"))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Confium::Transport::CoordinatorServer — serves coordinator sessions over
|
|
86
|
+
/// any linked transport scheme. Held in a Ruby object; the server
|
|
87
|
+
/// thread runs until the process exits.
|
|
88
|
+
#[derive(TypedData, DataTypeFunctions)]
|
|
89
|
+
#[magnus(class = "Confium::Transport::CoordinatorServer", size)]
|
|
90
|
+
pub struct CoordinatorServer {
|
|
91
|
+
_bound: String,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
impl CoordinatorServer {
|
|
95
|
+
fn initialize(ruby: &Ruby, url: String) -> Result<Self, Error> {
|
|
96
|
+
let _ = ruby;
|
|
97
|
+
let server = RustCoordinatorServer::new(&url);
|
|
98
|
+
let bound = server
|
|
99
|
+
.start_url(&url)
|
|
100
|
+
.map_err(|e| io_error(e, "CoordinatorServer.new"))?;
|
|
101
|
+
Ok(Self { _bound: bound })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
pub fn init(ruby: &Ruby, parent: magnus::RModule) -> Result<(), Error> {
|
|
106
|
+
let net = parent.define_module("Transport")?;
|
|
107
|
+
|
|
108
|
+
let client = net.define_class("SignerClient", ruby.class_object())?;
|
|
109
|
+
client.define_singleton_method("new", magnus::function!(SignerClient::initialize, 1))?;
|
|
110
|
+
client.define_method("register", magnus::method!(SignerClient::register, 2))?;
|
|
111
|
+
client.define_method(
|
|
112
|
+
"create_session",
|
|
113
|
+
magnus::method!(SignerClient::create_session, 5),
|
|
114
|
+
)?;
|
|
115
|
+
client.define_method(
|
|
116
|
+
"submit_commitment",
|
|
117
|
+
magnus::method!(SignerClient::submit_commitment, -1),
|
|
118
|
+
)?;
|
|
119
|
+
client.define_method("submit_share", magnus::method!(SignerClient::submit_share, -1))?;
|
|
120
|
+
|
|
121
|
+
let server = net.define_class("CoordinatorServer", ruby.class_object())?;
|
|
122
|
+
server.define_singleton_method("new", magnus::function!(CoordinatorServer::initialize, 1))?;
|
|
123
|
+
|
|
124
|
+
Ok(())
|
|
125
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
//! Confium::TC::Session — per-party threshold protocol sessions.
|
|
2
|
+
//!
|
|
3
|
+
//! Binds `confium_tc_core::Session`: each signer process runs ONE
|
|
4
|
+
//! session for its own party, feeding it the messages received from
|
|
5
|
+
//! the peers and sending on what `round_step` produces. The scheme
|
|
6
|
+
//! never leaves the process — this is the per-party half of the
|
|
7
|
+
//! multi-host signing story (the NetworkCoordinator is the transport
|
|
8
|
+
//! half).
|
|
9
|
+
//!
|
|
10
|
+
//! session = Confium::TC::Session.new("FROST-ed25519-dkg",
|
|
11
|
+
//! parties: ["p0", "p1", "p2"],
|
|
12
|
+
//! threshold: 2,
|
|
13
|
+
//! this_party_idx: 0)
|
|
14
|
+
//! loop do
|
|
15
|
+
//! result = session.round_step(incoming)
|
|
16
|
+
//! break if result["complete"]
|
|
17
|
+
//! incoming = broadcast(result["outgoing"])
|
|
18
|
+
//! end
|
|
19
|
+
//! session.result
|
|
20
|
+
//!
|
|
21
|
+
//! Messages are Hashes with string keys: `"from"`, `"to"` (nil for
|
|
22
|
+
//! broadcast), `"round"`, `"payload"` (binary String).
|
|
23
|
+
|
|
24
|
+
use magnus::{prelude::*, DataTypeFunctions, Error, Module, RArray, RHash, Ruby, TryConvert, TypedData, Value};
|
|
25
|
+
|
|
26
|
+
// Force the frost-ed25519 crate (and its `inventory::submit!` calls for
|
|
27
|
+
// FrostEd25519 + FrostEd25519Dkg) to be linked into the cdylib even
|
|
28
|
+
// though the binding itself never names a symbol from it — otherwise
|
|
29
|
+
// the linker drops the registration statics and `Session::create`
|
|
30
|
+
// reports the scheme as unknown.
|
|
31
|
+
extern crate confium_tc_frost_ed25519 as _frost_ed25519_link;
|
|
32
|
+
|
|
33
|
+
use confium_tc_session::{
|
|
34
|
+
session::{Session as RustSession, SessionParams}, share::Share, Error as TcError, Message,
|
|
35
|
+
Party, PartyList,
|
|
36
|
+
};
|
|
37
|
+
// (confium-tc-session is the renamed confium-tc-core 0.4.7 dependency.)
|
|
38
|
+
|
|
39
|
+
fn typed_session_error(e: TcError, operation: &str) -> Error {
|
|
40
|
+
let ruby = match Ruby::get() {
|
|
41
|
+
Ok(r) => r,
|
|
42
|
+
Err(_) => return Error::new(magnus::exception::runtime_error(), e.to_string()),
|
|
43
|
+
};
|
|
44
|
+
let details = crate::util::new_details(&ruby);
|
|
45
|
+
let _ = details.aset("operation", operation);
|
|
46
|
+
crate::util::confium_error(e.to_string(), "Error", details)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Confium::TC::Session — see the module docs.
|
|
50
|
+
#[derive(TypedData, DataTypeFunctions)]
|
|
51
|
+
#[magnus(class = "Confium::TC::Session", size)]
|
|
52
|
+
pub struct Session {
|
|
53
|
+
inner: std::cell::RefCell<RustSession>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fn bytes_arg(v: Value, name: &str) -> Result<Vec<u8>, Error> {
|
|
57
|
+
crate::util::bytes_from_value(v)
|
|
58
|
+
.map_err(|e| crate::util::arg_error(format!("Session: {name}: {e}")))
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
impl Session {
|
|
62
|
+
fn initialize(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
63
|
+
let scanned = magnus::scan_args::scan_args::<(String,), (Option<RHash>,), (), (), (), ()>(args)
|
|
64
|
+
.map_err(|e| crate::util::arg_error(e.to_string()))?;
|
|
65
|
+
let scheme = scanned.required.0;
|
|
66
|
+
let raw_opts = scanned
|
|
67
|
+
.optional
|
|
68
|
+
.0
|
|
69
|
+
.ok_or_else(|| crate::util::arg_error("Session.new: options Hash is required"))?;
|
|
70
|
+
// Ruby callers pass keywords (`parties: [...]`), which arrive as
|
|
71
|
+
// symbol keys; accept both symbol and string keys.
|
|
72
|
+
let opts = ruby.hash_new();
|
|
73
|
+
raw_opts.foreach(|k: Value, v: Value| {
|
|
74
|
+
let key: String = if k.is_kind_of(ruby.class_symbol()) {
|
|
75
|
+
k.funcall("to_s", ())?
|
|
76
|
+
} else {
|
|
77
|
+
<String as TryConvert>::try_convert(k)?
|
|
78
|
+
};
|
|
79
|
+
opts.aset(key, v)?;
|
|
80
|
+
Ok(magnus::r_hash::ForEach::Continue)
|
|
81
|
+
})?;
|
|
82
|
+
|
|
83
|
+
let required = |key: &str| -> Result<Value, Error> {
|
|
84
|
+
opts.fetch(key).map_err(|_| {
|
|
85
|
+
crate::util::arg_error(format!("Session.new: :{key} is required"))
|
|
86
|
+
})
|
|
87
|
+
};
|
|
88
|
+
let parties_val = required("parties")?;
|
|
89
|
+
let ids: Vec<String> = <Vec<String> as TryConvert>::try_convert(parties_val).map_err(|_| {
|
|
90
|
+
crate::util::arg_error("Session.new: :parties must be an Array of String ids")
|
|
91
|
+
})?;
|
|
92
|
+
if ids.len() < 2 {
|
|
93
|
+
return Err(crate::util::arg_error(
|
|
94
|
+
"Session.new: at least two parties are required",
|
|
95
|
+
));
|
|
96
|
+
}
|
|
97
|
+
let mut parties = PartyList::new();
|
|
98
|
+
for id in &ids {
|
|
99
|
+
parties.push(Party::new(id.clone(), None::<String>));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let threshold: u32 = <u32 as TryConvert>::try_convert(required("threshold")?)
|
|
103
|
+
.map_err(|_| crate::util::arg_error("Session.new: :threshold must be an Integer"))?;
|
|
104
|
+
let this_party_idx: usize = <usize as TryConvert>::try_convert(required("this_party_idx")?)
|
|
105
|
+
.map_err(|_| {
|
|
106
|
+
crate::util::arg_error("Session.new: :this_party_idx must be an Integer")
|
|
107
|
+
})?;
|
|
108
|
+
|
|
109
|
+
let local_share_v: Value = opts
|
|
110
|
+
.get("local_share")
|
|
111
|
+
.unwrap_or_else(|| ruby.qnil().as_value());
|
|
112
|
+
let local_share: Option<Share> = if local_share_v.is_nil() {
|
|
113
|
+
None
|
|
114
|
+
} else {
|
|
115
|
+
Some(Share::new(
|
|
116
|
+
scheme.clone(),
|
|
117
|
+
bytes_arg(local_share_v, "local_share")?,
|
|
118
|
+
))
|
|
119
|
+
};
|
|
120
|
+
let message_v: Value = opts
|
|
121
|
+
.get("message")
|
|
122
|
+
.unwrap_or_else(|| ruby.qnil().as_value());
|
|
123
|
+
let message: Option<Vec<u8>> = if message_v.is_nil() {
|
|
124
|
+
None
|
|
125
|
+
} else {
|
|
126
|
+
Some(bytes_arg(message_v, "message")?)
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
let params = SessionParams {
|
|
130
|
+
scheme: scheme.clone(),
|
|
131
|
+
parties,
|
|
132
|
+
threshold,
|
|
133
|
+
this_party_idx,
|
|
134
|
+
local_share,
|
|
135
|
+
message,
|
|
136
|
+
};
|
|
137
|
+
let inner = RustSession::create(¶ms)
|
|
138
|
+
.map_err(|e| typed_session_error(e, "Session.new"))?;
|
|
139
|
+
let _ = ruby;
|
|
140
|
+
Ok(Self {
|
|
141
|
+
inner: std::cell::RefCell::new(inner),
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fn scheme_name(&self) -> String {
|
|
146
|
+
self.inner.borrow().scheme_name().to_string()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
fn threshold(&self) -> u32 {
|
|
150
|
+
self.inner.borrow().threshold()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
fn party_count(&self) -> usize {
|
|
154
|
+
self.inner.borrow().party_count()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fn this_party_idx(&self) -> usize {
|
|
158
|
+
self.inner.borrow().this_party_idx()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
fn round(&self) -> u8 {
|
|
162
|
+
self.inner.borrow().round()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fn complete(&self) -> bool {
|
|
166
|
+
self.inner.borrow().is_complete()
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
fn round_step(&self, args: &[Value]) -> Result<RHash, Error> {
|
|
170
|
+
let ruby = Ruby::get().map_err(|e| crate::util::runtime(e.to_string()))?;
|
|
171
|
+
let scanned =
|
|
172
|
+
magnus::scan_args::scan_args::<(), (Option<Value>,), (), (), (), ()>(args)
|
|
173
|
+
.map_err(|e| crate::util::arg_error(e.to_string()))?;
|
|
174
|
+
let incoming = match scanned.optional.0 {
|
|
175
|
+
Some(v) if !v.is_nil() => parse_messages(&ruby, v)?,
|
|
176
|
+
_ => Vec::new(),
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
let result = self
|
|
180
|
+
.inner
|
|
181
|
+
.borrow_mut()
|
|
182
|
+
.round_step(&incoming)
|
|
183
|
+
.map_err(|e| typed_session_error(e, "Session#round_step"))?;
|
|
184
|
+
|
|
185
|
+
let out = ruby.hash_new();
|
|
186
|
+
let arr = ruby.ary_new();
|
|
187
|
+
for m in &result.outgoing {
|
|
188
|
+
let h = ruby.hash_new();
|
|
189
|
+
let _ = h.aset("from", m.from_party_id.as_str());
|
|
190
|
+
let to: Value = match &m.to_party_id {
|
|
191
|
+
Some(t) => ruby.str_new(t).as_value(),
|
|
192
|
+
None => ruby.qnil().as_value(),
|
|
193
|
+
};
|
|
194
|
+
let _ = h.aset("to", to);
|
|
195
|
+
let _ = h.aset("round", m.round as i64);
|
|
196
|
+
let _ = h.aset(
|
|
197
|
+
"payload",
|
|
198
|
+
crate::util::bytes_to_rstring(&ruby, &m.payload),
|
|
199
|
+
);
|
|
200
|
+
arr.push(h)?;
|
|
201
|
+
}
|
|
202
|
+
let _ = out.aset("outgoing", arr);
|
|
203
|
+
let _ = out.aset("complete", result.complete);
|
|
204
|
+
Ok(out)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
fn result(&self) -> Result<magnus::RString, Error> {
|
|
208
|
+
let ruby = Ruby::get().map_err(|e| crate::util::runtime(e.to_string()))?;
|
|
209
|
+
let bytes = self
|
|
210
|
+
.inner
|
|
211
|
+
.borrow()
|
|
212
|
+
.result()
|
|
213
|
+
.map_err(|e| typed_session_error(e, "Session#result"))?;
|
|
214
|
+
Ok(crate::util::bytes_to_rstring(&ruby, &bytes))
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
fn parse_messages(ruby: &Ruby, value: Value) -> Result<Vec<Message>, Error> {
|
|
219
|
+
let arr: RArray = <RArray as TryConvert>::try_convert(value)
|
|
220
|
+
.map_err(|_| crate::util::arg_error("Session#round_step: incoming must be an Array"))?;
|
|
221
|
+
let mut out = Vec::with_capacity(arr.len());
|
|
222
|
+
for item in arr.into_iter() {
|
|
223
|
+
let h: RHash = <RHash as TryConvert>::try_convert(item).map_err(|_| {
|
|
224
|
+
crate::util::arg_error("Session#round_step: each message must be a Hash")
|
|
225
|
+
})?;
|
|
226
|
+
let fetch = |key: &str| -> Result<Value, Error> {
|
|
227
|
+
h.fetch(key)
|
|
228
|
+
.map_err(|_| crate::util::arg_error(format!("message: '{key}' is required")))
|
|
229
|
+
};
|
|
230
|
+
let from: String = <String as TryConvert>::try_convert(fetch("from")?)
|
|
231
|
+
.map_err(|_| crate::util::arg_error("message: 'from' must be a String"))?;
|
|
232
|
+
let to_v: Value = fetch("to")?;
|
|
233
|
+
let to: Option<String> = if to_v.is_nil() {
|
|
234
|
+
None
|
|
235
|
+
} else {
|
|
236
|
+
Some(
|
|
237
|
+
<String as TryConvert>::try_convert(to_v)
|
|
238
|
+
.map_err(|_| crate::util::arg_error("message: 'to' must be a String"))?,
|
|
239
|
+
)
|
|
240
|
+
};
|
|
241
|
+
let round: u8 = <u8 as TryConvert>::try_convert(fetch("round")?)
|
|
242
|
+
.map_err(|_| crate::util::arg_error("message: 'round' must be an Integer"))?;
|
|
243
|
+
let payload: Value = fetch("payload")?;
|
|
244
|
+
let payload = crate::util::bytes_from_value(payload)?;
|
|
245
|
+
let msg = match to {
|
|
246
|
+
Some(t) => Message::directed(from, t, round, payload),
|
|
247
|
+
None => Message::broadcast(from, round, payload),
|
|
248
|
+
};
|
|
249
|
+
out.push(msg);
|
|
250
|
+
let _ = ruby;
|
|
251
|
+
}
|
|
252
|
+
Ok(out)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
pub fn init(ruby: &Ruby, parent: magnus::RModule) -> Result<(), Error> {
|
|
256
|
+
let tc = parent.define_module("TC")?;
|
|
257
|
+
let class = tc.define_class("Session", ruby.class_object())?;
|
|
258
|
+
class.define_singleton_method("new", magnus::function!(Session::initialize, -1))?;
|
|
259
|
+
class.define_method("scheme_name", magnus::method!(Session::scheme_name, 0))?;
|
|
260
|
+
class.define_method("threshold", magnus::method!(Session::threshold, 0))?;
|
|
261
|
+
class.define_method("party_count", magnus::method!(Session::party_count, 0))?;
|
|
262
|
+
class.define_method("this_party_idx", magnus::method!(Session::this_party_idx, 0))?;
|
|
263
|
+
class.define_method("round", magnus::method!(Session::round, 0))?;
|
|
264
|
+
class.define_method("complete?", magnus::method!(Session::complete, 0))?;
|
|
265
|
+
class.define_method("round_step", magnus::method!(Session::round_step, -1))?;
|
|
266
|
+
class.define_method("result", magnus::method!(Session::result, 0))?;
|
|
267
|
+
Ok(())
|
|
268
|
+
}
|