confium 0.4.1 → 0.5.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 +37 -0
- data/lib/confium/audit/otlp_sink.rb +103 -0
- data/lib/confium/audit.rb +2 -0
- data/lib/confium/composite.rb +39 -1
- data/lib/confium/tc/coordinator.rb +23 -10
- data/lib/confium/tc/network_coordinator.rb +208 -0
- data/lib/confium/tc.rb +8 -1
- data/lib/confium/version.rb +1 -1
- data/lib/confium.rb +10 -5
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d09c11dbd19d2df0b80cac44a48013395a7d96a1430506be1b1dc42b0b3831a3
|
|
4
|
+
data.tar.gz: c15b846b399f57a4a36fe808a8e98e3e224ec3873690a81e43f7c281134b5918
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 568abf025728f288d453c8f315930665f3fabb77abc1484e743c7539fd36c45813c721c970202f40a90e4e34969313a28adca521b5a38cd62a01984743b92a74
|
|
7
|
+
data.tar.gz: 59877e7dc1b834e49e7f9c5b5a25e1a87bcd888f50a15bdc3917f6024685c168bc87feb4d30bc3f5f9cb37b232a3b141652d9e07b0501f9c2e110205486e7b3c
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.0] — 2026-08-24
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Ruby 4.0 support** — Windows ships an exact `4.0` ABI window;
|
|
8
|
+
every platform install-checks on Ruby 4.0; the test matrix gains
|
|
9
|
+
4.0 legs. (There is no Ruby 3.5 — 4.0 is the successor line.)
|
|
10
|
+
- **Multi-host threshold signing**: `Confium::TC::NetworkCoordinator`
|
|
11
|
+
(TCP service, NDJSON + hex wire protocol) + `Confium::TC::SignerClient`
|
|
12
|
+
— signers on separate machines submit commitments and shares;
|
|
13
|
+
aggregation runs the real CMP20/GG18 combine and returns an
|
|
14
|
+
OpenSSL-verifiable signature. Plain TCP for loopback/private
|
|
15
|
+
networks; the upstream noise transport replaces it later.
|
|
16
|
+
- **OpenTelemetry export**: `Confium::Audit::OtlpSink` ships every
|
|
17
|
+
audit record to an OTLP collector over OTLP/HTTP JSON (logs
|
|
18
|
+
signal, stdlib-only, failures drop-and-report).
|
|
19
|
+
- `Confium::Composite::Signature#to_json` — instances remember their
|
|
20
|
+
source (components or a canonical JSON document) and re-serialize
|
|
21
|
+
losslessly, completing the JSON transport symmetry.
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- `Confium::TC::Coordinator` now aggregates REAL threshold-ECDSA
|
|
26
|
+
signatures: once the threshold is met, it runs the CMP20 or GG18
|
|
27
|
+
combine (selected per session via `scheme:`) and returns a 64-byte
|
|
28
|
+
signature verifiable under the quorum public key. Previously
|
|
29
|
+
`aggregate` concatenated the share bytes — a placeholder that
|
|
30
|
+
returned garbage. Unknown sessions below-threshold now raise
|
|
31
|
+
`Confium::ThresholdError`.
|
|
32
|
+
- `Confium::TC::Coordinator` and `ShareFile` were unreachable after
|
|
33
|
+
`require "confium"`: the TC namespace file that registers them
|
|
34
|
+
never loaded because the native extension pre-defines
|
|
35
|
+
`Confium::TC` (the same orphaned-module trap PKI had). The
|
|
36
|
+
namespace file is now eager-required; `TC::Session` stays lazy —
|
|
37
|
+
it wraps the engine via FFI and needs the external libconfium
|
|
38
|
+
dylib.
|
|
39
|
+
|
|
3
40
|
## [0.4.1] — 2026-08-24
|
|
4
41
|
|
|
5
42
|
### Fixed
|
|
@@ -0,0 +1,103 @@
|
|
|
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; failures are reported on
|
|
20
|
+
# $stderr and the record is dropped (the audit core's policy — a
|
|
21
|
+
# telemetry outage must never break signing). stdlib only.
|
|
22
|
+
class OtlpSink < Sink
|
|
23
|
+
DEFAULT_ENDPOINT = 'http://localhost:4318/v1/logs'
|
|
24
|
+
SEVERITY = { 'success' => 9, 'failure' => 17, 'error' => 17 }.freeze # INFO / ERROR
|
|
25
|
+
|
|
26
|
+
def initialize(endpoint: DEFAULT_ENDPOINT, headers: {}, service_name: 'confium', timeout: 5)
|
|
27
|
+
super()
|
|
28
|
+
@uri = URI.parse(endpoint)
|
|
29
|
+
@headers = headers
|
|
30
|
+
@service_name = service_name
|
|
31
|
+
@timeout = timeout
|
|
32
|
+
@dropped = 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Records dropped after failed delivery (diagnostic only).
|
|
36
|
+
attr_reader :dropped
|
|
37
|
+
|
|
38
|
+
def write(record)
|
|
39
|
+
payload = envelope(record)
|
|
40
|
+
response = Net::HTTP.post(@uri, JSON.generate(payload), headers.merge('Content-Type' => 'application/json'))
|
|
41
|
+
raise "collector responded #{response.code}" unless response.is_a?(Net::HTTPSuccess)
|
|
42
|
+
rescue StandardError => e
|
|
43
|
+
@dropped += 1
|
|
44
|
+
warn "confium: OTLP delivery failed, audit record dropped (##{@dropped}): #{e.class}: #{e.message}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def close; end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def headers
|
|
52
|
+
{ 'User-Agent' => "confium-otlp-sink #{Confium::VERSION}" }.merge(@headers)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# OTLP/JSON logs request: one resource, one scope, one record.
|
|
56
|
+
def envelope(record)
|
|
57
|
+
{
|
|
58
|
+
'resourceLogs' => [
|
|
59
|
+
{
|
|
60
|
+
'resource' => {
|
|
61
|
+
'attributes' => [
|
|
62
|
+
{ 'key' => 'service.name', 'value' => { 'stringValue' => @service_name } },
|
|
63
|
+
{ 'key' => 'telemetry.sdk.name', 'value' => { 'stringValue' => 'confium' } },
|
|
64
|
+
{ 'key' => 'telemetry.sdk.version', 'value' => { 'stringValue' => Confium::VERSION } }
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
'scopeLogs' => [
|
|
68
|
+
{
|
|
69
|
+
'scope' => { 'name' => 'confium.audit' },
|
|
70
|
+
'logRecords' => [log_record(record)]
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
]
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def log_record(record)
|
|
79
|
+
{
|
|
80
|
+
'timeUnixNano' => (parse_time(record['timestamp']) * 1_000_000_000).to_i.to_s,
|
|
81
|
+
'severityNumber' => SEVERITY.fetch(record['result'], 9),
|
|
82
|
+
'severityText' => record['result'] == 'success' ? 'INFO' : 'ERROR',
|
|
83
|
+
'body' => { 'stringValue' => "#{record['operation']} #{record['result']}".strip },
|
|
84
|
+
'attributes' => attributes_for(record)
|
|
85
|
+
}
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def attributes_for(record)
|
|
89
|
+
record.except('timestamp').map do |key, value|
|
|
90
|
+
{ 'key' => key, 'value' => { 'stringValue' => value.to_s } }
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def parse_time(timestamp)
|
|
95
|
+
return Time.now.to_f unless timestamp.is_a?(String)
|
|
96
|
+
|
|
97
|
+
Time.parse(timestamp).to_f
|
|
98
|
+
rescue ArgumentError
|
|
99
|
+
Time.now.to_f
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
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)
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Confium::TC::Coordinator
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
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 — the same in-process
|
|
7
|
+
# drivers behind Confium::TC::Cmp20 / Confium::TC::Gg18) and returns
|
|
8
|
+
# a 64-byte (r, s) signature verifiable under the quorum public key.
|
|
7
9
|
#
|
|
8
10
|
# Usage:
|
|
9
11
|
# coordinator = Confium::TC::Coordinator.new(quorum_id: "biml-root")
|
|
10
12
|
# session_id = coordinator.create_session(message: data,
|
|
11
|
-
# threshold:
|
|
12
|
-
#
|
|
13
|
+
# threshold: 3,
|
|
14
|
+
# scheme: "CMP20-ECDSA-P256")
|
|
13
15
|
# coordinator.submit_commitment(session_id, signer_id, commitment_bytes)
|
|
14
16
|
# # ... wait for T commitments ...
|
|
15
17
|
# coordinator.submit_share(session_id, signer_id, share_bytes)
|
|
@@ -25,12 +27,20 @@ module Confium
|
|
|
25
27
|
@sessions = {}
|
|
26
28
|
end
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
SCHEMES = {
|
|
31
|
+
'CMP20-ECDSA-P256' => ->(shares, threshold, message) { Cmp20.sign(shares, threshold, message) },
|
|
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
|
+
|
|
29
38
|
session_id = "session-#{@sessions.length + 1}"
|
|
30
39
|
@sessions[session_id] = {
|
|
31
40
|
message: message,
|
|
32
41
|
threshold: threshold,
|
|
33
42
|
unlock_window: unlock_window,
|
|
43
|
+
scheme: scheme,
|
|
34
44
|
state: :pending,
|
|
35
45
|
commitments: [],
|
|
36
46
|
shares: []
|
|
@@ -57,11 +67,14 @@ module Confium
|
|
|
57
67
|
|
|
58
68
|
def aggregate(session_id)
|
|
59
69
|
session = @sessions[session_id] or raise "Unknown session: #{session_id}"
|
|
60
|
-
raise 'Threshold not met' if session[:shares].length < session[:threshold]
|
|
70
|
+
raise ThresholdError, 'Threshold not met' if session[:shares].length < session[:threshold]
|
|
61
71
|
|
|
62
72
|
session[:state] = :completed
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
SCHEMES.fetch(session[:scheme]).call(
|
|
74
|
+
session[:shares].map { |s| s[:bytes] },
|
|
75
|
+
session[:threshold],
|
|
76
|
+
session[:message]
|
|
77
|
+
)
|
|
65
78
|
end
|
|
66
79
|
end
|
|
67
80
|
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
require 'socket'
|
|
5
|
+
require_relative 'coordinator'
|
|
6
|
+
|
|
7
|
+
module Confium
|
|
8
|
+
module TC
|
|
9
|
+
# Networked threshold-signing coordinator service.
|
|
10
|
+
#
|
|
11
|
+
# Wraps Confium::TC::Coordinator (real CMP20/GG18 combine) behind
|
|
12
|
+
# a TCP socket so signers on separate machines submit commitments
|
|
13
|
+
# and shares from their own processes:
|
|
14
|
+
#
|
|
15
|
+
# service = Confium::TC::NetworkCoordinator.new(quorum_id: 'root')
|
|
16
|
+
# service.start # binds 127.0.0.1:<ephemeral>
|
|
17
|
+
# client = Confium::TC::SignerClient.new(port: service.port)
|
|
18
|
+
# sid = client.create_session(message: data, threshold: 3)
|
|
19
|
+
# client.submit_share(sid, 'signer-1', share_blob)
|
|
20
|
+
# client.aggregate(sid) # => 64-byte signature
|
|
21
|
+
#
|
|
22
|
+
# Protocol: one JSON object per line (NDJSON); binary fields are
|
|
23
|
+
# hex-encoded. Unknown operations and Coordinator errors come
|
|
24
|
+
# back as {"error": ..., "message": ...} lines.
|
|
25
|
+
#
|
|
26
|
+
# Transport security: none yet — plain TCP intended for loopback
|
|
27
|
+
# or private networks. The upstream noise-transport session
|
|
28
|
+
# protocol replaces this wholesale (see
|
|
29
|
+
# TODO.full/01-multi-host-threshold-signing.md).
|
|
30
|
+
class NetworkCoordinator
|
|
31
|
+
HANDLERS = {
|
|
32
|
+
'create' => lambda do |coord, req|
|
|
33
|
+
session_id = coord.create_session(
|
|
34
|
+
message: [req.fetch('message_hex')].pack('H*'),
|
|
35
|
+
threshold: req.fetch('threshold'),
|
|
36
|
+
scheme: req['scheme'] || 'CMP20-ECDSA-P256'
|
|
37
|
+
)
|
|
38
|
+
{ 'session_id' => session_id }
|
|
39
|
+
end,
|
|
40
|
+
'commitment' => lambda do |coord, req|
|
|
41
|
+
session_id = req.fetch('session_id')
|
|
42
|
+
coord.submit_commitment(
|
|
43
|
+
session_id, req.fetch('signer_id'),
|
|
44
|
+
[req.fetch('bytes_hex')].pack('H*')
|
|
45
|
+
)
|
|
46
|
+
{ 'ok' => true, 'state' => coord.session_state(session_id).to_s }
|
|
47
|
+
end,
|
|
48
|
+
'share' => lambda do |coord, req|
|
|
49
|
+
coord.submit_share(
|
|
50
|
+
req.fetch('session_id'), req.fetch('signer_id'),
|
|
51
|
+
[req.fetch('bytes_hex')].pack('H*')
|
|
52
|
+
)
|
|
53
|
+
{ 'ok' => true }
|
|
54
|
+
end,
|
|
55
|
+
'aggregate' => lambda do |coord, req|
|
|
56
|
+
signature = coord.aggregate(req.fetch('session_id'))
|
|
57
|
+
{ 'signature_hex' => signature.unpack1('H*') }
|
|
58
|
+
end
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
class RequestError < StandardError; end
|
|
62
|
+
|
|
63
|
+
attr_reader :quorum_id, :port
|
|
64
|
+
|
|
65
|
+
def initialize(quorum_id:, host: '127.0.0.1', port: 0, coordinator: nil)
|
|
66
|
+
@quorum_id = quorum_id
|
|
67
|
+
@host = host
|
|
68
|
+
@port = port
|
|
69
|
+
@coordinator = coordinator || Coordinator.new(quorum_id: quorum_id)
|
|
70
|
+
@server = nil
|
|
71
|
+
@accept_thread = nil
|
|
72
|
+
@mutex = Mutex.new
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def start
|
|
76
|
+
raise 'already started' if @server
|
|
77
|
+
|
|
78
|
+
# RBS socket lib lacks the (host, port) overload for new
|
|
79
|
+
@server = TCPServer.new(@host.to_s, @port) # steep:ignore
|
|
80
|
+
@port = @server.addr[1]
|
|
81
|
+
@accept_thread = Thread.new { accept_loop }
|
|
82
|
+
self
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def stop
|
|
86
|
+
return unless @server
|
|
87
|
+
|
|
88
|
+
server = @server
|
|
89
|
+
@server = nil
|
|
90
|
+
server.close
|
|
91
|
+
@accept_thread&.join(5)
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def running?
|
|
96
|
+
!@server.nil?
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
def accept_loop
|
|
102
|
+
loop do
|
|
103
|
+
server = @server
|
|
104
|
+
break unless server
|
|
105
|
+
|
|
106
|
+
begin
|
|
107
|
+
socket = server.accept
|
|
108
|
+
rescue IOError, Errno::EBADF, Errno::EINVAL
|
|
109
|
+
break # listener closed by #stop
|
|
110
|
+
end
|
|
111
|
+
Thread.new(socket) { |conn| serve(conn) }
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def serve(socket)
|
|
116
|
+
socket.each_line do |line|
|
|
117
|
+
request = JSON.parse(line)
|
|
118
|
+
response = @mutex.synchronize { dispatch(request) }
|
|
119
|
+
socket.write("#{JSON.generate(response)}\n")
|
|
120
|
+
end
|
|
121
|
+
rescue JSON::ParserError => e
|
|
122
|
+
write_error(socket, 'RequestError', "malformed JSON: #{e.message}")
|
|
123
|
+
rescue StandardError => e
|
|
124
|
+
write_error(socket, e.class.name, e.message)
|
|
125
|
+
ensure
|
|
126
|
+
socket.close
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def dispatch(request)
|
|
130
|
+
op = request['op'] or raise RequestError, 'missing "op"'
|
|
131
|
+
handler = HANDLERS[op] or raise RequestError, "unknown op: #{op}"
|
|
132
|
+
|
|
133
|
+
handler.call(@coordinator, request)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def write_error(socket, klass, message)
|
|
137
|
+
socket.write("#{JSON.generate({ 'error' => klass, 'message' => message })}\n")
|
|
138
|
+
rescue IOError
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Client side for NetworkCoordinator. One instance per signer
|
|
144
|
+
# process; each call opens its own connection, so signers never
|
|
145
|
+
# share state.
|
|
146
|
+
class SignerClient
|
|
147
|
+
class RemoteError < StandardError
|
|
148
|
+
attr_reader :remote_class
|
|
149
|
+
|
|
150
|
+
def initialize(remote_class, message)
|
|
151
|
+
@remote_class = remote_class
|
|
152
|
+
super("#{remote_class}: #{message}")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def initialize(port:, host: '127.0.0.1')
|
|
157
|
+
@host = host
|
|
158
|
+
@port = port
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def create_session(message:, threshold:, scheme: 'CMP20-ECDSA-P256')
|
|
162
|
+
response = call(
|
|
163
|
+
'op' => 'create',
|
|
164
|
+
'message_hex' => message.to_s.unpack1('H*'),
|
|
165
|
+
'threshold' => threshold,
|
|
166
|
+
'scheme' => scheme
|
|
167
|
+
)
|
|
168
|
+
response.fetch('session_id')
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def submit_commitment(session_id, signer_id, commitment_bytes)
|
|
172
|
+
call(
|
|
173
|
+
'op' => 'commitment',
|
|
174
|
+
'session_id' => session_id,
|
|
175
|
+
'signer_id' => signer_id,
|
|
176
|
+
'bytes_hex' => commitment_bytes.unpack1('H*')
|
|
177
|
+
).fetch('ok')
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def submit_share(session_id, signer_id, share_bytes)
|
|
181
|
+
call(
|
|
182
|
+
'op' => 'share',
|
|
183
|
+
'session_id' => session_id,
|
|
184
|
+
'signer_id' => signer_id,
|
|
185
|
+
'bytes_hex' => share_bytes.unpack1('H*')
|
|
186
|
+
).fetch('ok')
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def aggregate(session_id)
|
|
190
|
+
response = call('op' => 'aggregate', 'session_id' => session_id)
|
|
191
|
+
[response.fetch('signature_hex')].pack('H*')
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
private
|
|
195
|
+
|
|
196
|
+
def call(request)
|
|
197
|
+
TCPSocket.open(@host, @port) do |socket|
|
|
198
|
+
socket.write("#{JSON.generate(request)}\n")
|
|
199
|
+
line = socket.readline
|
|
200
|
+
response = JSON.parse(line)
|
|
201
|
+
raise RemoteError.new(response['error'], response['message']) if response['error']
|
|
202
|
+
|
|
203
|
+
response
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
data/lib/confium/tc.rb
CHANGED
|
@@ -11,7 +11,14 @@
|
|
|
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 is
|
|
15
|
+
# eager-required from confium.rb, so autoloads registered here do
|
|
16
|
+
# fire. Coordinator and ShareFile are pure Ruby and load with the
|
|
17
|
+
# namespace; Session wraps the engine via FFI and needs the
|
|
18
|
+
# external libconfium dylib, so it stays lazy.
|
|
14
19
|
autoload :Session, 'confium/tc/session'
|
|
15
|
-
|
|
20
|
+
require_relative 'tc/coordinator'
|
|
21
|
+
require_relative 'tc/network_coordinator'
|
|
22
|
+
require_relative 'tc/share_file'
|
|
16
23
|
end
|
|
17
24
|
end
|
data/lib/confium/version.rb
CHANGED
data/lib/confium.rb
CHANGED
|
@@ -20,7 +20,11 @@ begin
|
|
|
20
20
|
# directory.
|
|
21
21
|
minor = RUBY_VERSION[/\A\d+\.\d+/]
|
|
22
22
|
dlext = RbConfig::CONFIG['DLEXT'] || 'so'
|
|
23
|
-
|
|
23
|
+
major = RUBY_VERSION[/\A\d+/].to_i
|
|
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]
|
|
24
28
|
# Windows gems carry an exact-minor window per Ruby (a PE import
|
|
25
29
|
# names the version-specific ruby DLL); other platforms share the
|
|
26
30
|
# 3.3 window for 3.3+. Prefer the exact minor when present.
|
|
@@ -92,7 +96,8 @@ require_relative 'confium/transparency'
|
|
|
92
96
|
# of that module.
|
|
93
97
|
require_relative 'confium/audit'
|
|
94
98
|
|
|
95
|
-
# Eager-load the TC
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
99
|
+
# Eager-load the TC namespace file: `Confium::TC` is native-defined,
|
|
100
|
+
# so an autoload here would never fire (the PKI pattern). It
|
|
101
|
+
# registers the pure-Ruby Session/Coordinator companions and the
|
|
102
|
+
# ShareFile class for filesystem-backed share persistence.
|
|
103
|
+
require_relative 'confium/tc'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: confium
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Open
|
|
@@ -128,6 +128,7 @@ files:
|
|
|
128
128
|
- ext/confium_native/src/util.rs
|
|
129
129
|
- lib/confium.rb
|
|
130
130
|
- lib/confium/audit.rb
|
|
131
|
+
- lib/confium/audit/otlp_sink.rb
|
|
131
132
|
- lib/confium/cfm.rb
|
|
132
133
|
- lib/confium/composite.rb
|
|
133
134
|
- lib/confium/crypto.rb
|
|
@@ -155,6 +156,7 @@ files:
|
|
|
155
156
|
- lib/confium/secure_bytes.rb
|
|
156
157
|
- lib/confium/tc.rb
|
|
157
158
|
- lib/confium/tc/coordinator.rb
|
|
159
|
+
- lib/confium/tc/network_coordinator.rb
|
|
158
160
|
- lib/confium/tc/session.rb
|
|
159
161
|
- lib/confium/tc/session_stub.rb
|
|
160
162
|
- lib/confium/tc/share_file.rb
|