confium 0.3.4-aarch64-linux → 0.5.0-aarch64-linux

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.
data/README.adoc CHANGED
@@ -10,7 +10,7 @@ Confium supports three deployment modes:
10
10
  * **Mode 2 — TC PKI replacement**: drop-in for existing PKI consumers (PKCS#11 server, OpenSSL 3.0 provider, JCE)
11
11
  * **Mode 3 — TC Certificate PKI**: institutional deployments with custom certificate formats (OIML CNML, BIPM, pharma, accreditation)
12
12
 
13
- This gem wraps the high-value Confium subsystems via a Rust native extension. Pre-compiled platform gems are published for Linux and macOS, so `gem install` needs no Rust toolchain there; other platforms build from source at install time with `rb_sys` + `magnus`. No separate C ABI library to install; everything is statically linked into the extension.
13
+ This gem wraps the high-value Confium subsystems via a pure-Rust native extension (`rb_sys` + `magnus`, no C dependencies). Pre-compiled platform gems are published for Linux (glibc + musl), macOS, and Windows, so `gem install` needs no Rust toolchain there; other platforms build from source at install time.
14
14
 
15
15
  == Installation
16
16
 
@@ -32,17 +32,13 @@ $ gem install confium
32
32
 
33
33
  * Ruby ≥ 3.1
34
34
  * Nothing else on the pre-compiled platforms: `x86_64-linux`,
35
- `aarch64-linux`, `x86_64-darwin`, `arm64-darwin` (each gem carries
36
- one extension per Ruby C-ABI window)
35
+ `aarch64-linux`, `x86_64-linux-musl`, `aarch64-linux-musl`,
36
+ `x86_64-darwin`, `arm64-darwin`, `x64-mingw-ucrt` (each gem
37
+ carries one extension per Ruby C-ABI window)
37
38
 
38
39
  Source builds (other platforms, or installing from the repo) also
39
- need:
40
-
41
- * Rust stable toolchain (`rustup default stable`)
42
- * C toolchain (clang/gcc/Xcode CLT)
43
-
44
- There is no separate `libconfium` to install — the extension
45
- statically links everything.
40
+ need the Rust stable toolchain (`rustup default stable`) — and
41
+ nothing else: the extension has no C dependencies to satisfy.
46
42
 
47
43
  == Quick start
48
44
 
@@ -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
@@ -124,3 +124,5 @@ module Confium
124
124
  end
125
125
  end
126
126
  end
127
+
128
+ require_relative 'audit/otlp_sink'
@@ -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,33 +1,155 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Confium::OpenPGP — OpenPGP (RFC 9580) via bundled rnp-rs.
4
- #
5
- # The native extension provides _native_armor / _native_dearmor.
6
- # This file adds the idiomatic Ruby wrappers with default args.
7
- #
8
- # Architecture: RNP is HARD-BUNDLED in the native extension. No
9
- # external gem dependency. Users get OpenPGP armor encode/decode
10
- # out of the box.
11
-
12
3
  module Confium
4
+ # OpenPGP ASCII armor (RFC 9580 §6) — Radix-64 framing with a
5
+ # CRC-24 checksum, implemented in pure Ruby.
6
+ #
7
+ # This replaced the earlier native wrapper around librnp, which
8
+ # pulled a full vendored Botan/json-c C/C++ build into every
9
+ # install just to do armor framing. The wire format is unchanged;
10
+ # spec/fixtures/openpgp_armor_vectors.json holds differential
11
+ # vectors captured from the native implementation.
13
12
  module OpenPGP
13
+ MESSAGE = 'message'
14
+ PUBLIC_KEY = 'public key'
15
+ SECRET_KEY = 'secret key'
16
+ SIGNATURE = 'signature'
17
+ CLEARTEXT = 'cleartext signed message'
18
+
19
+ LABELS = {
20
+ nil => 'MESSAGE',
21
+ MESSAGE => 'MESSAGE',
22
+ 'public key' => 'PUBLIC KEY BLOCK',
23
+ SECRET_KEY => 'PRIVATE KEY BLOCK',
24
+ 'private key' => 'PRIVATE KEY BLOCK',
25
+ SIGNATURE => 'SIGNATURE',
26
+ # Raw bytes cannot form a cleartext-signed message (that
27
+ # requires a signature packet); armor them as a plain message.
28
+ CLEARTEXT => 'MESSAGE',
29
+ 'cleartext' => 'MESSAGE'
30
+ }.freeze
31
+
32
+ CRC_POLY = 0x1864CFB
33
+ CRC_INIT = 0xB704CE
34
+ B64_CHARS = [('A'..'Z').to_a, ('a'..'z').to_a, ('0'..'9').to_a, %w[+ /]].flatten.freeze
35
+ CRC_TABLE = (0..255).map do |i|
36
+ c = i << 16
37
+ 8.times do
38
+ c = c.nobits?(0x800_000) ? c << 1 : ((c << 1) ^ CRC_POLY)
39
+ c &= 0xFFFFFF
40
+ end
41
+ c
42
+ end.freeze
43
+
44
+ private_constant :LABELS, :CRC_POLY, :CRC_INIT, :B64_CHARS, :CRC_TABLE
45
+
14
46
  class << self
15
- # ASCII-armor encode raw bytes.
47
+ # ASCII-armor encode raw bytes. Output uses CRLF line endings
48
+ # and 76-character data lines, byte-for-byte matching the
49
+ # earlier native (rnp) implementation.
16
50
  #
17
51
  # @param data [String] Binary data to encode.
18
- # @param type [String] Armor type — one of MESSAGE, PUBLIC_KEY,
19
- # SECRET_KEY, SIGNATURE, CLEARTEXT. Defaults to MESSAGE.
52
+ # @param type [String] One of MESSAGE, PUBLIC_KEY, SECRET_KEY,
53
+ # SIGNATURE, CLEARTEXT (armored as a plain message). Defaults
54
+ # to MESSAGE.
20
55
  # @return [String] Armored ASCII string.
56
+ # @raise [ArgumentError] if +type+ is not a known armor type.
21
57
  def armor(data, type = MESSAGE)
22
- _native_armor(data, type)
58
+ label = LABELS[type]
59
+ raise ArgumentError, "unknown armor type: #{type.inspect}" unless label
60
+
61
+ bytes = data.to_s.b
62
+ b64 = [bytes].pack('m0')
63
+ lines = b64.scan(/.{1,76}/)
64
+ <<~ARMOR.gsub("\n", "\r\n")
65
+ -----BEGIN PGP #{label}-----
66
+
67
+ #{lines.join("\n")}
68
+ =#{crc24_armor(bytes)}
69
+ -----END PGP #{label}-----
70
+ ARMOR
23
71
  end
24
72
 
25
- # Decode ASCII-armored data to raw bytes.
73
+ # Decode ASCII-armored data to raw bytes. Accepts LF or CRLF
74
+ # line endings, arbitrary line widths, and Armor Headers
75
+ # (Comment:, Version:, ...) between the BEGIN line and the
76
+ # blank line. The CRC-24 checksum line is verified when
77
+ # present.
26
78
  #
27
79
  # @param data [String] Armored ASCII string.
28
- # @return [String] Raw binary data.
80
+ # @return [String] Raw binary data (ASCII-8BIT).
81
+ # @raise [Confium::ParseError] on missing delimiters, non-base64
82
+ # content, or a CRC mismatch.
29
83
  def dearmor(data)
30
- _native_dearmor(data)
84
+ b64, crc_line = extract_body(data.to_s)
85
+ # @type var bytes: String
86
+ bytes = b64.unpack1('m0')
87
+ return ''.b if crc_line == crc24_armor(''.b) && b64.empty?
88
+
89
+ raise ParseError, 'armor CRC-24 checksum mismatch' if crc_line && crc24_armor(bytes) != crc_line.to_s
90
+
91
+ bytes
92
+ end
93
+
94
+ private
95
+
96
+ # Locate the armored block, skip the BEGIN line and any Armor
97
+ # Headers, and split the remaining lines into the joined
98
+ # Radix-64 data and the checksum line (if present).
99
+ def extract_body(text)
100
+ lines = block_lines(text)
101
+ b64, crc_line = collect_data(lines)
102
+ raise ParseError, 'armored block has no data' if b64.empty? && crc_line.nil?
103
+
104
+ [b64, crc_line]
105
+ end
106
+
107
+ # The lines of the armored block between BEGIN and END, with
108
+ # the BEGIN line and any Armor Headers removed.
109
+ def block_lines(text)
110
+ slice = block_slice(text.gsub("\r\n", "\n"))
111
+ lines = slice.split("\n")
112
+ lines.shift # BEGIN
113
+ lines.shift while lines.first&.match?(/^\s|^[A-Za-z0-9-]+: /)
114
+ lines.shift if lines.first == ''
115
+ lines
116
+ end
117
+
118
+ # The text between the BEGIN and END delimiter lines.
119
+ def block_slice(normalized)
120
+ begin_line = normalized.index(/^-----BEGIN PGP [A-Z ]+-----$/)
121
+ raise ParseError, 'not an ASCII-armored block (no BEGIN line)' unless begin_line
122
+
123
+ end_match = normalized.match(/^-----END PGP [A-Z ]+-----$/)
124
+ raise ParseError, 'not an ASCII-armored block (no END line)' unless end_match
125
+
126
+ normalized[begin_line...end_match.begin(0)].to_s
127
+ end
128
+
129
+ def collect_data(lines)
130
+ b64 = +''
131
+ crc_line = nil
132
+ lines.each do |line|
133
+ next if line.empty?
134
+
135
+ if line.start_with?('=')
136
+ crc_line = line[1..]
137
+ elsif line.match?(%r{\A[A-Za-z0-9+/]+={0,2}\z})
138
+ b64 << line
139
+ else
140
+ raise ParseError, "invalid armor data line: #{line[0, 20].inspect}"
141
+ end
142
+ end
143
+ [b64, crc_line]
144
+ end
145
+
146
+ # CRC-24 (RFC 9580 §6.1), encoded as the four Radix-64
147
+ # characters of the 24-bit value as three big-endian bytes.
148
+ def crc24_armor(bytes)
149
+ crc = CRC_INIT
150
+ bytes.each_byte { |b| crc = ((crc << 8) & 0xFFFFFF) ^ CRC_TABLE[((crc >> 16) ^ b) & 0xFF] }
151
+ three_bytes = [crc >> 16, (crc >> 8) & 0xFF, crc & 0xFF].pack('C3')
152
+ [three_bytes].pack('m0')
31
153
  end
32
154
  end
33
155
  end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Confium::TC::Coordinator wraps the async session coordinator service.
4
- #
5
- # The coordinator enables globally distributed threshold signers to
6
- # participate when convenient — no simultaneity required.
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: 5,
12
- # unlock_window: 14400)
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
- def create_session(message:, threshold:, unlock_window: 14_400)
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
- # In real implementation, calls FFI to aggregate shares
64
- session[:shares].map { |s| s[:bytes] }.join
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
- autoload :Coordinator, 'confium/tc/coordinator'
20
+ require_relative 'tc/coordinator'
21
+ require_relative 'tc/network_coordinator'
22
+ require_relative 'tc/share_file'
16
23
  end
17
24
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Confium
4
- VERSION = '0.3.4'
4
+ VERSION = '0.5.0'
5
5
  end