confium 0.4.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +84 -0
- data/Cargo.lock +432 -12
- data/ext/confium_native/Cargo.toml +16 -0
- data/ext/confium_native/extconf.rb +5 -0
- data/ext/confium_native/src/lib.rs +4 -0
- data/ext/confium_native/src/openpgp_verify.rs +189 -0
- data/lib/confium/audit/otlp_sink.rb +120 -0
- data/lib/confium/audit.rb +2 -0
- data/lib/confium/composite.rb +39 -1
- data/lib/confium/native_windows.rb +31 -0
- data/lib/confium/tc/coordinator.rb +28 -27
- data/lib/confium/tc/network_coordinator.rb +208 -0
- data/lib/confium/tc/signing_session.rb +93 -0
- data/lib/confium/tc.rb +7 -2
- data/lib/confium/version.rb +1 -1
- data/lib/confium.rb +10 -16
- metadata +7 -9
- data/lib/confium/cfm.rb +0 -23
- data/lib/confium/crypto.rb +0 -51
- data/lib/confium/digest.rb +0 -62
- data/lib/confium/ffi.rb +0 -23
- data/lib/confium/lib.rb +0 -33
- data/lib/confium/tc/session.rb +0 -51
- data/lib/confium/tc/session_stub.rb +0 -43
|
@@ -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
|
|
@@ -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,7 +11,12 @@
|
|
|
11
11
|
# for the full interface specification.
|
|
12
12
|
module Confium
|
|
13
13
|
module TC
|
|
14
|
-
|
|
15
|
-
|
|
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'
|
|
18
|
+
require_relative 'tc/coordinator'
|
|
19
|
+
require_relative 'tc/network_coordinator'
|
|
20
|
+
require_relative 'tc/share_file'
|
|
16
21
|
end
|
|
17
22
|
end
|
data/lib/confium/version.rb
CHANGED
data/lib/confium.rb
CHANGED
|
@@ -10,21 +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
|
-
# Windows gems carry an exact-minor window per Ruby (a PE import
|
|
25
|
-
# names the version-specific ruby DLL); other platforms share the
|
|
26
|
-
# 3.3 window for 3.3+. Prefer the exact minor when present.
|
|
27
|
-
windowed = candidates.filter_map do |w|
|
|
20
|
+
windowed = Confium::NativeWindows.candidates(RUBY_VERSION).filter_map do |w|
|
|
28
21
|
path = File.expand_path("confium_native/#{w}/confium_native.#{dlext}", __dir__ || '.')
|
|
29
22
|
w if File.exist?(path)
|
|
30
23
|
end
|
|
@@ -92,7 +85,8 @@ require_relative 'confium/transparency'
|
|
|
92
85
|
# of that module.
|
|
93
86
|
require_relative 'confium/audit'
|
|
94
87
|
|
|
95
|
-
# Eager-load the TC
|
|
96
|
-
#
|
|
97
|
-
#
|
|
98
|
-
|
|
88
|
+
# Eager-load the TC namespace file: `Confium::TC` is native-defined,
|
|
89
|
+
# so an autoload here would never fire (the PKI pattern). It
|
|
90
|
+
# registers the pure-Ruby Session/Coordinator companions and the
|
|
91
|
+
# ShareFile class for filesystem-backed share persistence.
|
|
92
|
+
require_relative 'confium/tc'
|
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
|
|
@@ -128,10 +129,8 @@ files:
|
|
|
128
129
|
- ext/confium_native/src/util.rs
|
|
129
130
|
- lib/confium.rb
|
|
130
131
|
- lib/confium/audit.rb
|
|
131
|
-
- lib/confium/
|
|
132
|
+
- lib/confium/audit/otlp_sink.rb
|
|
132
133
|
- lib/confium/composite.rb
|
|
133
|
-
- lib/confium/crypto.rb
|
|
134
|
-
- lib/confium/digest.rb
|
|
135
134
|
- lib/confium/errors.rb
|
|
136
135
|
- lib/confium/errors/coerce.rb
|
|
137
136
|
- lib/confium/errors/crypto_error.rb
|
|
@@ -143,8 +142,7 @@ files:
|
|
|
143
142
|
- lib/confium/errors/unresolved_signer_error.rb
|
|
144
143
|
- lib/confium/errors/validation_error.rb
|
|
145
144
|
- lib/confium/errors/verification_error.rb
|
|
146
|
-
- lib/confium/
|
|
147
|
-
- lib/confium/lib.rb
|
|
145
|
+
- lib/confium/native_windows.rb
|
|
148
146
|
- lib/confium/openpgp.rb
|
|
149
147
|
- lib/confium/pki.rb
|
|
150
148
|
- lib/confium/pki/certificate_builder.rb
|
|
@@ -155,9 +153,9 @@ files:
|
|
|
155
153
|
- lib/confium/secure_bytes.rb
|
|
156
154
|
- lib/confium/tc.rb
|
|
157
155
|
- lib/confium/tc/coordinator.rb
|
|
158
|
-
- lib/confium/tc/
|
|
159
|
-
- lib/confium/tc/session_stub.rb
|
|
156
|
+
- lib/confium/tc/network_coordinator.rb
|
|
160
157
|
- lib/confium/tc/share_file.rb
|
|
158
|
+
- lib/confium/tc/signing_session.rb
|
|
161
159
|
- lib/confium/transparency.rb
|
|
162
160
|
- lib/confium/transparency/ots.rb
|
|
163
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
|
data/lib/confium/crypto.rb
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# {Confium::Crypto} is the registry namespace for Confium's cryptographic
|
|
4
|
-
# interfaces.
|
|
5
|
-
#
|
|
6
|
-
# Mirroring the Rust plugin-interface registry (TODO #02), each Ruby
|
|
7
|
-
# interface module (Digest, and future Cipher, AEAD, KDF, RNG, Signature,
|
|
8
|
-
# KEM, ...) registers itself here on load rather than being enumerated in
|
|
9
|
-
# a central case statement. This keeps the registry open for extension
|
|
10
|
-
# (OCP) and makes the set of available interfaces a single source of
|
|
11
|
-
# truth.
|
|
12
|
-
#
|
|
13
|
-
# Example:
|
|
14
|
-
#
|
|
15
|
-
# module Confium
|
|
16
|
-
# module Digest
|
|
17
|
-
# Confium::Crypto.register(:hash, self)
|
|
18
|
-
# end
|
|
19
|
-
# end
|
|
20
|
-
#
|
|
21
|
-
# Confium::Crypto.lookup(:hash) # => Confium::Digest
|
|
22
|
-
module Confium
|
|
23
|
-
module Crypto
|
|
24
|
-
# @type ivar @interfaces: Hash[Symbol, untyped]
|
|
25
|
-
@interfaces = {}
|
|
26
|
-
|
|
27
|
-
class << self
|
|
28
|
-
# Register an interface +klass+ under the symbolic +name+. Adding a
|
|
29
|
-
# new interface is a one-line registration; no central switch needs
|
|
30
|
-
# editing.
|
|
31
|
-
def register(name, klass)
|
|
32
|
-
@interfaces[name] = klass
|
|
33
|
-
klass
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Resolve a registered interface by +name+. Raises ArgumentError if
|
|
37
|
-
# nothing has been registered under that name.
|
|
38
|
-
def lookup(name)
|
|
39
|
-
@interfaces.fetch(name) do
|
|
40
|
-
raise ArgumentError, "unknown interface #{name.inspect}"
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Enumerate the names of every registered interface. Primarily for
|
|
45
|
-
# introspection and tooling.
|
|
46
|
-
def interfaces
|
|
47
|
-
@interfaces.keys.dup
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
data/lib/confium/digest.rb
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'ffi'
|
|
4
|
-
require 'digest'
|
|
5
|
-
|
|
6
|
-
module Confium
|
|
7
|
-
class Digest < ::Digest::Class
|
|
8
|
-
attr_reader :name, :ptr
|
|
9
|
-
|
|
10
|
-
def initialize(cfm, name)
|
|
11
|
-
@name = name
|
|
12
|
-
pptr = ::FFI::MemoryPointer.new(:pointer)
|
|
13
|
-
Confium.call_ffi(:cfm_hash_create, cfm.ptr, pptr, name, nil, nil, nil)
|
|
14
|
-
ptr = pptr.read_pointer
|
|
15
|
-
raise if ptr.null?
|
|
16
|
-
|
|
17
|
-
@ptr = ::FFI::AutoPointer.new(ptr, self.class.method(:destroy))
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def initialize_copy(source)
|
|
21
|
-
@name = source.name
|
|
22
|
-
pptr = ::FFI::MemoryPointer.new(:pointer)
|
|
23
|
-
Confium.call_ffi(:cfm_hash_clone, source.ptr, pptr)
|
|
24
|
-
ptr = pptr.read_pointer
|
|
25
|
-
@ptr = ::FFI::AutoPointer.new(ptr, self.class.method(:destroy))
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def self.destroy(ptr)
|
|
29
|
-
Confium::Lib.cfm_hash_destroy(ptr)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
def block_length
|
|
33
|
-
plength = ::FFI::MemoryPointer.new(:uint32)
|
|
34
|
-
Confium.call_ffi(:cfm_hash_block_size, @ptr, plength)
|
|
35
|
-
plength.read(:uint32)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def digest_length
|
|
39
|
-
plength = ::FFI::MemoryPointer.new(:uint32)
|
|
40
|
-
Confium.call_ffi(:cfm_hash_output_size, @ptr, plength)
|
|
41
|
-
plength.read(:uint32)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def update(data)
|
|
45
|
-
Confium.call_ffi(:cfm_hash_update, @ptr, data, data.bytesize)
|
|
46
|
-
self
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def reset
|
|
50
|
-
Confium.call_ffi(:cfm_hash_reset, @ptr)
|
|
51
|
-
self
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def finish
|
|
55
|
-
buf = ::FFI::MemoryPointer.new(:uint8, digest_length)
|
|
56
|
-
Confium.call_ffi(:cfm_hash_finalize, @ptr, buf, buf.size)
|
|
57
|
-
buf.read_bytes(buf.size)
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
alias << update
|
|
61
|
-
end
|
|
62
|
-
end
|
data/lib/confium/ffi.rb
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'ffi'
|
|
4
|
-
|
|
5
|
-
# {Confium::FFI} is the namespace for everything that touches the native
|
|
6
|
-
# Confium shared library through Ruby-FFI.
|
|
7
|
-
#
|
|
8
|
-
# This file registers autoload entries for the planned FFI helper modules
|
|
9
|
-
# (Library, Error, Options). They are listed in the TODO #14 architecture
|
|
10
|
-
# (see `TODO.finalize/14-ruby-bindings-architecture.md`) and will be added
|
|
11
|
-
# as the bindings grow; until then the autoloads are inert — they only
|
|
12
|
-
# trigger a load when the corresponding constant is first referenced.
|
|
13
|
-
#
|
|
14
|
-
# Note: the legacy FFI library wrapper still lives at `Confium::Lib`
|
|
15
|
-
# (file `confium/lib.rb`) and is autoloaded from `confium.rb`. It will be
|
|
16
|
-
# migrated into `Confium::FFI::Library` in a follow-up.
|
|
17
|
-
module Confium
|
|
18
|
-
module FFI
|
|
19
|
-
autoload :Library, 'confium/ffi/library'
|
|
20
|
-
autoload :Error, 'confium/ffi/error'
|
|
21
|
-
autoload :Options, 'confium/ffi/options'
|
|
22
|
-
end
|
|
23
|
-
end
|
data/lib/confium/lib.rb
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'ffi'
|
|
4
|
-
|
|
5
|
-
module Confium
|
|
6
|
-
module Lib
|
|
7
|
-
extend ::FFI::Library
|
|
8
|
-
|
|
9
|
-
FFI_LAYOUT = {
|
|
10
|
-
cfm_create: [%i[pointer], :uint32],
|
|
11
|
-
cfm_destroy: [%i[pointer], :uint32],
|
|
12
|
-
cfm_plugin_load: [%i[pointer string string pointer pointer], :uint32],
|
|
13
|
-
cfm_hash_create: [%i[pointer pointer pointer pointer pointer pointer], :uint32],
|
|
14
|
-
cfm_hash_output_size: [%i[pointer pointer], :uint32],
|
|
15
|
-
cfm_hash_block_size: [%i[pointer pointer], :uint32],
|
|
16
|
-
cfm_hash_update: [%i[pointer pointer uint32], :uint32],
|
|
17
|
-
cfm_hash_reset: [%i[pointer], :uint32],
|
|
18
|
-
cfm_hash_clone: [%i[pointer pointer], :uint32],
|
|
19
|
-
cfm_hash_finalize: [%i[pointer pointer uint32], :uint32],
|
|
20
|
-
cfm_hash_destroy: [%i[pointer], :void]
|
|
21
|
-
}.freeze
|
|
22
|
-
|
|
23
|
-
ffi_lib([ENV.fetch('CONFIUM_LIB', nil), 'confium', 'libconfium'].compact)
|
|
24
|
-
|
|
25
|
-
FFI_LAYOUT.each do |func, ary|
|
|
26
|
-
class_eval do
|
|
27
|
-
attach_function(func, ary.first, ary.last)
|
|
28
|
-
end
|
|
29
|
-
rescue ::FFI::NotFoundError
|
|
30
|
-
# that's okay
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
data/lib/confium/tc/session.rb
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'confium/lib'
|
|
4
|
-
|
|
5
|
-
# Confium::TC::Session wraps a threshold cryptography session.
|
|
6
|
-
#
|
|
7
|
-
# A session represents one signing or decryption operation involving
|
|
8
|
-
# T-of-N parties. The session goes through states: pending →
|
|
9
|
-
# commitments_collected → shares_collected → completed.
|
|
10
|
-
#
|
|
11
|
-
# Usage:
|
|
12
|
-
# session = Confium::TC::Session.new(scheme: "FROST-ed25519",
|
|
13
|
-
# threshold: 3,
|
|
14
|
-
# num_parties: 5,
|
|
15
|
-
# party_index: 0)
|
|
16
|
-
# session.set_local_share(share_bytes)
|
|
17
|
-
# session.round(incoming_messages) # returns outgoing messages
|
|
18
|
-
# result = session.result if session.complete?
|
|
19
|
-
module Confium
|
|
20
|
-
module TC
|
|
21
|
-
class Session
|
|
22
|
-
attr_reader :scheme, :threshold, :num_parties, :party_index, :state
|
|
23
|
-
|
|
24
|
-
def initialize(scheme:, threshold:, num_parties:, party_index:)
|
|
25
|
-
@scheme = scheme
|
|
26
|
-
@threshold = threshold
|
|
27
|
-
@num_parties = num_parties
|
|
28
|
-
@party_index = party_index
|
|
29
|
-
@state = :pending
|
|
30
|
-
@local_share = nil
|
|
31
|
-
@result = nil
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def set_local_share(share_bytes)
|
|
35
|
-
raise ArgumentError, 'share must be a String' unless share_bytes.is_a?(String)
|
|
36
|
-
|
|
37
|
-
@local_share = share_bytes
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def complete?
|
|
41
|
-
@state == :completed
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def result
|
|
45
|
-
raise 'Session not complete' unless complete?
|
|
46
|
-
|
|
47
|
-
@result
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|