confium 0.2.0 → 0.3.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 +126 -0
- data/Cargo.lock +2534 -0
- data/Cargo.toml +9 -0
- data/README.adoc +114 -14
- data/Rakefile +8 -3
- data/confium.gemspec +42 -21
- data/ext/confium_native/Cargo.toml +62 -0
- data/ext/confium_native/build.rs +72 -0
- data/ext/confium_native/extconf.rb +10 -0
- data/ext/confium_native/src/attributes.rs +88 -0
- data/ext/confium_native/src/audit.rs +169 -0
- data/ext/confium_native/src/composite.rs +302 -0
- data/ext/confium_native/src/deployment.rs +176 -0
- data/ext/confium_native/src/ers.rs +93 -0
- data/ext/confium_native/src/lib.rs +56 -0
- data/ext/confium_native/src/openpgp.rs +55 -0
- data/ext/confium_native/src/path.rs +118 -0
- data/ext/confium_native/src/pki.rs +431 -0
- data/ext/confium_native/src/tc.rs +420 -0
- data/ext/confium_native/src/transparency.rs +341 -0
- data/ext/confium_native/src/util.rs +196 -0
- data/lib/confium/audit.rb +125 -0
- data/lib/confium/cfm.rb +2 -4
- data/lib/confium/crypto.rb +50 -0
- data/lib/confium/digest.rb +7 -7
- data/lib/confium/errors/coerce.rb +49 -0
- data/lib/confium/errors/crypto_error.rb +13 -0
- data/lib/confium/errors/index_error.rb +13 -0
- data/lib/confium/errors/not_found_error.rb +13 -0
- data/lib/confium/errors/parse_error.rb +13 -0
- data/lib/confium/errors/policy_violation_error.rb +13 -0
- data/lib/confium/errors/threshold_error.rb +14 -0
- data/lib/confium/errors/unresolved_signer_error.rb +12 -0
- data/lib/confium/errors/validation_error.rb +15 -0
- data/lib/confium/errors/verification_error.rb +13 -0
- data/lib/confium/errors.rb +26 -0
- data/lib/confium/ffi.rb +23 -0
- data/lib/confium/lib.rb +2 -39
- data/lib/confium/openpgp.rb +34 -0
- data/lib/confium/pki/certificate_builder.rb +60 -0
- data/lib/confium/pki/cms/signed_data_builder.rb +92 -0
- data/lib/confium/pki/cms.rb +15 -0
- data/lib/confium/pki/cnml.rb +80 -0
- data/lib/confium/pki.rb +13 -0
- data/lib/confium/policy.rb +138 -0
- data/lib/confium/secure_bytes.rb +124 -0
- data/lib/confium/tc/coordinator.rb +67 -0
- data/lib/confium/tc/session.rb +49 -0
- data/lib/confium/tc/session_stub.rb +43 -0
- data/lib/confium/tc/share_file.rb +87 -0
- data/lib/confium/tc.rb +17 -0
- data/lib/confium/transparency/ots.rb +63 -0
- data/lib/confium/version.rb +1 -1
- data/lib/confium.rb +50 -20
- metadata +142 -25
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -10
- data/sig/confium.rbs +0 -4
data/lib/confium/pki.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Confium::PKI namespace file.
|
|
4
|
+
#
|
|
5
|
+
# The PKI module itself is defined by the native Rust extension via
|
|
6
|
+
# magnus at require time (see ext/confium_native/src/pki.rs). This file
|
|
7
|
+
# registers pure-Ruby autoloads for the PKI submodules that wrap or
|
|
8
|
+
# extend the native surface.
|
|
9
|
+
module Confium
|
|
10
|
+
module PKI
|
|
11
|
+
autoload :CMS, "confium/pki/cms"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Jurisdictional policy enforcement for Confium.
|
|
4
|
+
#
|
|
5
|
+
# Different jurisdictions require different algorithms and key sizes:
|
|
6
|
+
# - EU: no RSA < 2048, ECDSA P-256+ accepted, Ed25519 accepted
|
|
7
|
+
# - US: SHA-1 legacy accepted, ECDSA P-256 accepted
|
|
8
|
+
# - China: SM2/SM3/SM4 required (not currently implemented)
|
|
9
|
+
#
|
|
10
|
+
# Set the active policy via:
|
|
11
|
+
# Confium::Policy.jurisdiction = :eu
|
|
12
|
+
#
|
|
13
|
+
# When a policy is active, cert verification and signing operations
|
|
14
|
+
# check the algorithm/key-size against the policy's allowed list.
|
|
15
|
+
# Violations raise Confium::PolicyViolationError.
|
|
16
|
+
|
|
17
|
+
module Confium
|
|
18
|
+
module Policy
|
|
19
|
+
@jurisdiction = nil
|
|
20
|
+
@fips_mode = false
|
|
21
|
+
|
|
22
|
+
# Built-in jurisdictional policies. Each is a Hash mapping
|
|
23
|
+
# algorithm name to minimum key bits.
|
|
24
|
+
JURISDICTIONS = {
|
|
25
|
+
# EU: eIDAS + GDPR alignment. RSA >= 2048, ECDSA P-256+.
|
|
26
|
+
eu: {
|
|
27
|
+
rsa: 2048,
|
|
28
|
+
ecdsa_p256: 256,
|
|
29
|
+
ecdsa_p384: 384,
|
|
30
|
+
ed25519: 256,
|
|
31
|
+
name: "European Union (eIDAS)",
|
|
32
|
+
},
|
|
33
|
+
# US: NIST SP 800-131A. RSA >= 2048, ECDSA P-256+, SHA-1 legacy.
|
|
34
|
+
us: {
|
|
35
|
+
rsa: 2048,
|
|
36
|
+
ecdsa_p256: 256,
|
|
37
|
+
ecdsa_p384: 384,
|
|
38
|
+
ed25519: 256,
|
|
39
|
+
sha1_legacy: true,
|
|
40
|
+
name: "United States (NIST SP 800-131A)",
|
|
41
|
+
},
|
|
42
|
+
# OIML CNML: international, follows BIPM recommendations.
|
|
43
|
+
cnml: {
|
|
44
|
+
rsa: 2048,
|
|
45
|
+
ecdsa_p256: 256,
|
|
46
|
+
ecdsa_p384: 384,
|
|
47
|
+
ed25519: 256,
|
|
48
|
+
name: "OIML CNML (BIPM)",
|
|
49
|
+
},
|
|
50
|
+
}.freeze
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
# @return [Symbol, nil] the active jurisdiction (:eu, :us, :cnml)
|
|
54
|
+
attr_reader :jurisdiction
|
|
55
|
+
|
|
56
|
+
# @return [Boolean] whether FIPS 140 mode is enabled
|
|
57
|
+
attr_reader :fips_mode
|
|
58
|
+
|
|
59
|
+
# Set the active jurisdiction. When non-nil, all signing and
|
|
60
|
+
# verification operations check algorithms against the policy.
|
|
61
|
+
# @param value [Symbol, nil] one of JURISDICTIONS.keys or nil
|
|
62
|
+
# @raise [ArgumentError] if value is not a known jurisdiction
|
|
63
|
+
def jurisdiction=(value)
|
|
64
|
+
if value.nil?
|
|
65
|
+
@jurisdiction = nil
|
|
66
|
+
return
|
|
67
|
+
end
|
|
68
|
+
unless JURISDICTIONS.key?(value.to_sym)
|
|
69
|
+
raise ArgumentError,
|
|
70
|
+
"unknown jurisdiction: #{value} (known: #{JURISDICTIONS.keys.join(', ')})"
|
|
71
|
+
end
|
|
72
|
+
@jurisdiction = value.to_sym
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Enable/disable FIPS 140 mode. When enabled, only FIPS-approved
|
|
76
|
+
# algorithms are accepted. Ed25519 is NOT FIPS-approved (as of
|
|
77
|
+
# FIPS 186-5 draft); ECDSA P-256/P-384 are.
|
|
78
|
+
# @param value [Boolean]
|
|
79
|
+
def fips_mode=(value)
|
|
80
|
+
@fips_mode = !!value
|
|
81
|
+
@jurisdiction = :us if @fips_mode && @jurisdiction.nil?
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Check whether an algorithm + key size is allowed under the
|
|
85
|
+
# active policy.
|
|
86
|
+
# @param algorithm [String, Symbol] e.g. "rsa", "ecdsa_p256"
|
|
87
|
+
# @param key_bits [Integer] the key size in bits
|
|
88
|
+
# @return [Boolean]
|
|
89
|
+
# @raise [Confium::PolicyViolationError] if the algorithm is
|
|
90
|
+
# disallowed or the key size is too small
|
|
91
|
+
def check!(algorithm, key_bits:)
|
|
92
|
+
alg = algorithm.to_sym
|
|
93
|
+
|
|
94
|
+
if @fips_mode
|
|
95
|
+
# FIPS mode: only FIPS-approved algorithms.
|
|
96
|
+
fips_approved = %i[ecdsa_p256 ecdsa_p384 rsa]
|
|
97
|
+
unless fips_approved.include?(alg)
|
|
98
|
+
raise Confium::PolicyViolationError.new(
|
|
99
|
+
"algorithm #{alg} is not FIPS-approved",
|
|
100
|
+
policy: :fips,
|
|
101
|
+
violation: :unapproved_algorithm,
|
|
102
|
+
)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
return true unless @jurisdiction
|
|
107
|
+
|
|
108
|
+
policy = JURISDICTIONS[@jurisdiction]
|
|
109
|
+
return true unless policy
|
|
110
|
+
|
|
111
|
+
min_bits = policy[alg]
|
|
112
|
+
return true if min_bits.nil?
|
|
113
|
+
|
|
114
|
+
if key_bits < min_bits
|
|
115
|
+
raise Confium::PolicyViolationError.new(
|
|
116
|
+
"#{alg} key size #{key_bits} below #{min_bits} for #{@jurisdiction}",
|
|
117
|
+
policy: @jurisdiction,
|
|
118
|
+
violation: :key_too_small,
|
|
119
|
+
)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# The list of known jurisdiction identifiers.
|
|
126
|
+
# @return [Array<Symbol>]
|
|
127
|
+
def known_jurisdictions
|
|
128
|
+
JURISDICTIONS.keys
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Reset all policies to defaults (no jurisdiction, no FIPS).
|
|
132
|
+
def reset!
|
|
133
|
+
@jurisdiction = nil
|
|
134
|
+
@fips_mode = false
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SecureBytes wraps sensitive cryptographic byte data (private keys,
|
|
4
|
+
# Shamir shares, shared secrets) with zeroize-on-clear semantics.
|
|
5
|
+
#
|
|
6
|
+
# MRI Ruby's String is backed by a heap-allocated char buffer that
|
|
7
|
+
# persists until GC. SecureBytes overwrites that buffer with zeros
|
|
8
|
+
# when #clear is called (explicitly or via finalizer).
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# key = Confium::SecureBytes.wrap(raw_bytes)
|
|
12
|
+
# key.bytes # non-destructive read
|
|
13
|
+
# key.clear # zeroize + deallocate
|
|
14
|
+
#
|
|
15
|
+
# After #clear, #bytes raises Confium::ClearedError.
|
|
16
|
+
|
|
17
|
+
class Confium::SecureBytes
|
|
18
|
+
# Raised when #bytes is called after #clear.
|
|
19
|
+
class ClearedError < Confium::Error
|
|
20
|
+
def initialize(message = "SecureBytes already cleared")
|
|
21
|
+
super(message, details: {})
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Create a SecureBytes wrapping a copy of the given String.
|
|
26
|
+
# The original String's contents are NOT modified; callers should
|
|
27
|
+
# zeroize the original separately if needed.
|
|
28
|
+
# @param raw [String] binary String (any encoding; bytes are copied)
|
|
29
|
+
# @return [Confium::SecureBytes]
|
|
30
|
+
def self.wrap(raw)
|
|
31
|
+
new(raw)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @api private
|
|
35
|
+
def initialize(raw)
|
|
36
|
+
@buffer = raw.dup.force_encoding(Encoding::ASCII_8BIT)
|
|
37
|
+
@cleared = false
|
|
38
|
+
# Register finalizer to zeroize if the object is GC'd without
|
|
39
|
+
# an explicit #clear call.
|
|
40
|
+
ObjectSpace.define_finalizer(self, finalizer_proc)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Non-destructive read of the wrapped bytes.
|
|
44
|
+
# @return [String] binary String (ASCII-8BIT encoding)
|
|
45
|
+
# @raise [ClearedError] if #clear was already called
|
|
46
|
+
def bytes
|
|
47
|
+
raise ClearedError if @cleared
|
|
48
|
+
|
|
49
|
+
@buffer.dup
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Destructive read: returns a copy, then zeroizes the original.
|
|
53
|
+
# @return [String] binary String
|
|
54
|
+
# @raise [ClearedError] if #clear was already called
|
|
55
|
+
def bytes!
|
|
56
|
+
raise ClearedError if @cleared
|
|
57
|
+
|
|
58
|
+
copy = @buffer.dup
|
|
59
|
+
clear
|
|
60
|
+
copy
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Number of bytes. Returns 0 after #clear.
|
|
64
|
+
# @return [Integer]
|
|
65
|
+
def length
|
|
66
|
+
@cleared ? 0 : @buffer.bytesize
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
alias size length
|
|
70
|
+
|
|
71
|
+
# Whether the buffer has been cleared.
|
|
72
|
+
# @return [Boolean]
|
|
73
|
+
def cleared?
|
|
74
|
+
@cleared
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Zeroize the buffer immediately. Idempotent.
|
|
78
|
+
# @return [self]
|
|
79
|
+
def clear
|
|
80
|
+
return self if @cleared
|
|
81
|
+
|
|
82
|
+
# Overwrite every byte with 0x00 in place.
|
|
83
|
+
@buffer.replace("\x00" * @buffer.bytesize)
|
|
84
|
+
@buffer = nil
|
|
85
|
+
@cleared = true
|
|
86
|
+
self
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# String representation for debugging. Does NOT expose the raw bytes.
|
|
90
|
+
# @return [String]
|
|
91
|
+
def inspect
|
|
92
|
+
if @cleared
|
|
93
|
+
"#<Confium::SecureBytes:0x#{object_id.to_s(16)} CLEARED>"
|
|
94
|
+
else
|
|
95
|
+
"#<Confium::SecureBytes:0x#{object_id.to_s(16)} #{length} bytes>"
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private
|
|
100
|
+
|
|
101
|
+
# Finalizer proc that zeroizes the buffer if GC collects this
|
|
102
|
+
# object without an explicit #clear. Uses object_id to find the
|
|
103
|
+
# buffer — but since the buffer is an instance variable that may
|
|
104
|
+
# already be collected, this is a best-effort path. Explicit #clear
|
|
105
|
+
# is the recommended path.
|
|
106
|
+
# @return [Proc]
|
|
107
|
+
def finalizer_proc
|
|
108
|
+
method(:finalize)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Called by the GC finalizer.
|
|
112
|
+
def finalize(_id)
|
|
113
|
+
# Best-effort: the buffer may already be collected by the time
|
|
114
|
+
# the finalizer runs. If @buffer still exists, zeroize it.
|
|
115
|
+
# This is a closure over the instance — MRI guarantees the
|
|
116
|
+
# finalizer runs after the object is unreachable but before
|
|
117
|
+
# the buffer's memory is reused.
|
|
118
|
+
return if @cleared
|
|
119
|
+
|
|
120
|
+
@buffer&.replace("\x00" * @buffer.bytesize)
|
|
121
|
+
@buffer = nil
|
|
122
|
+
@cleared = true
|
|
123
|
+
end
|
|
124
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
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.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# coordinator = Confium::TC::Coordinator.new(quorum_id: "biml-root")
|
|
10
|
+
# session_id = coordinator.create_session(message: data,
|
|
11
|
+
# threshold: 5,
|
|
12
|
+
# unlock_window: 14400)
|
|
13
|
+
# coordinator.submit_commitment(session_id, signer_id, commitment_bytes)
|
|
14
|
+
# # ... wait for T commitments ...
|
|
15
|
+
# coordinator.submit_share(session_id, signer_id, share_bytes)
|
|
16
|
+
# # ... wait for T shares ...
|
|
17
|
+
# signature = coordinator.aggregate(session_id)
|
|
18
|
+
module Confium
|
|
19
|
+
module TC
|
|
20
|
+
class Coordinator
|
|
21
|
+
attr_reader :quorum_id, :sessions
|
|
22
|
+
|
|
23
|
+
def initialize(quorum_id:)
|
|
24
|
+
@quorum_id = quorum_id
|
|
25
|
+
@sessions = {}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def create_session(message:, threshold:, unlock_window: 14400)
|
|
29
|
+
session_id = "session-#{@sessions.length + 1}"
|
|
30
|
+
@sessions[session_id] = {
|
|
31
|
+
message: message,
|
|
32
|
+
threshold: threshold,
|
|
33
|
+
unlock_window: unlock_window,
|
|
34
|
+
state: :pending,
|
|
35
|
+
commitments: [],
|
|
36
|
+
shares: [],
|
|
37
|
+
}
|
|
38
|
+
session_id
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def session_state(session_id)
|
|
42
|
+
@sessions.dig(session_id, :state)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def submit_commitment(session_id, signer_id, commitment_bytes)
|
|
46
|
+
session = @sessions[session_id] or raise "Unknown session: #{session_id}"
|
|
47
|
+
session[:commitments] << { signer_id: signer_id, bytes: commitment_bytes }
|
|
48
|
+
if session[:commitments].length >= session[:threshold]
|
|
49
|
+
session[:state] = :commitments_collected
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def submit_share(session_id, signer_id, share_bytes)
|
|
54
|
+
session = @sessions[session_id] or raise "Unknown session: #{session_id}"
|
|
55
|
+
session[:shares] << { signer_id: signer_id, bytes: share_bytes }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def aggregate(session_id)
|
|
59
|
+
session = @sessions[session_id] or raise "Unknown session: #{session_id}"
|
|
60
|
+
raise "Threshold not met" if session[:shares].length < session[:threshold]
|
|
61
|
+
session[:state] = :completed
|
|
62
|
+
# In real implementation, calls FFI to aggregate shares
|
|
63
|
+
session[:shares].map { |s| s[:bytes] }.join
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
@local_share = share_bytes
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def complete?
|
|
40
|
+
@state == :completed
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def result
|
|
44
|
+
raise "Session not complete" unless complete?
|
|
45
|
+
@result
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Confium::TC::SessionStub — placeholder for the multi-party TC session
|
|
4
|
+
# interface. The full Session implementation wraps
|
|
5
|
+
# confium_tc::session::Session which drives 3-round FROST/CMP20/GG18
|
|
6
|
+
# signing ceremonies.
|
|
7
|
+
#
|
|
8
|
+
# This stub provides the interface shape so consumers can write code
|
|
9
|
+
# against it. The actual session orchestration will be wired through
|
|
10
|
+
# magnus in a future PR (TODO.completion/009-multi-party-tc-sessions.md).
|
|
11
|
+
|
|
12
|
+
module Confium
|
|
13
|
+
module TC
|
|
14
|
+
class SessionStub
|
|
15
|
+
attr_reader :scheme, :threshold, :party_count, :this_party_idx, :round
|
|
16
|
+
|
|
17
|
+
def initialize(scheme:, threshold:, party_count:, this_party_idx:)
|
|
18
|
+
@scheme = scheme
|
|
19
|
+
@threshold = threshold
|
|
20
|
+
@party_count = party_count
|
|
21
|
+
@this_party_idx = this_party_idx
|
|
22
|
+
@round = 0
|
|
23
|
+
@complete = false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def complete?
|
|
27
|
+
@complete
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Stub: returns an empty RoundResult. Real implementation will
|
|
31
|
+
# call confium_tc::session::Session::round_step.
|
|
32
|
+
def round_step(_incoming_messages)
|
|
33
|
+
@round += 1
|
|
34
|
+
@complete = @round >= 3 # FROST is 3 rounds
|
|
35
|
+
{ outgoing: [], complete: @complete }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def result
|
|
39
|
+
nil # Real implementation returns the signature/secret bytes.
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
module Confium
|
|
7
|
+
module TC
|
|
8
|
+
# Filesystem-backed share persistence.
|
|
9
|
+
#
|
|
10
|
+
# Share blobs produced by `Confium::TC::Cmp20.keygen` /
|
|
11
|
+
# `Confium::TC::Gg18.keygen` are 71-byte binary strings. This
|
|
12
|
+
# class wraps them in a JSON envelope so they can be saved to
|
|
13
|
+
# disk, transferred between hosts, and loaded back without
|
|
14
|
+
# encoding ambiguity.
|
|
15
|
+
#
|
|
16
|
+
# The envelope format:
|
|
17
|
+
#
|
|
18
|
+
# {
|
|
19
|
+
# "scheme": "CMP20-ECDSA-P256",
|
|
20
|
+
# "threshold": 3,
|
|
21
|
+
# "party_count": 5,
|
|
22
|
+
# "public_key": "<33-byte hex>",
|
|
23
|
+
# "shares": ["<71-byte hex>", ...]
|
|
24
|
+
# }
|
|
25
|
+
#
|
|
26
|
+
# The format is identical to what the Python binding's
|
|
27
|
+
# `confium.tc.ShareFile` produces, so shares saved from one
|
|
28
|
+
# binding can be loaded by the other.
|
|
29
|
+
class ShareFile
|
|
30
|
+
attr_reader :scheme, :threshold, :party_count, :public_key, :shares
|
|
31
|
+
|
|
32
|
+
def initialize(scheme:, threshold:, party_count:, public_key:, shares:)
|
|
33
|
+
@scheme = scheme
|
|
34
|
+
@threshold = threshold
|
|
35
|
+
@party_count = party_count
|
|
36
|
+
@public_key = public_key
|
|
37
|
+
@shares = shares
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Load a ShareFile from a JSON file at `path`.
|
|
41
|
+
def self.load(path)
|
|
42
|
+
from_json(File.read(path))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Parse a ShareFile from a JSON string.
|
|
46
|
+
def self.from_json(json)
|
|
47
|
+
d = JSON.parse(json)
|
|
48
|
+
new(
|
|
49
|
+
scheme: d.fetch("scheme"),
|
|
50
|
+
threshold: d.fetch("threshold"),
|
|
51
|
+
party_count: d.fetch("party_count"),
|
|
52
|
+
public_key: [d.fetch("public_key")].pack("H*"),
|
|
53
|
+
shares: d.fetch("shares").map { |h| [h].pack("H*") },
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Save to `path` as JSON. Creates parent directories if missing.
|
|
58
|
+
def save(path)
|
|
59
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
60
|
+
File.write(path, to_json)
|
|
61
|
+
self
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Serialize to a JSON string.
|
|
65
|
+
def to_json(*_args)
|
|
66
|
+
JSON.generate(
|
|
67
|
+
scheme: scheme,
|
|
68
|
+
threshold: threshold,
|
|
69
|
+
party_count: party_count,
|
|
70
|
+
public_key: public_key.unpack1("H*"),
|
|
71
|
+
shares: shares.map { |s| s.unpack1("H*") },
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Build a ShareFile from a CMP20 / GG18 keygen result Hash.
|
|
76
|
+
def self.from_keygen(scheme_name, keygen_result)
|
|
77
|
+
new(
|
|
78
|
+
scheme: scheme_name,
|
|
79
|
+
threshold: nil, # not carried by the keygen Hash; caller knows
|
|
80
|
+
party_count: keygen_result["shares"].length,
|
|
81
|
+
public_key: keygen_result["public_key"],
|
|
82
|
+
shares: keygen_result["shares"],
|
|
83
|
+
)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
data/lib/confium/tc.rb
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Confium::TC provides Ruby access to the threshold cryptography
|
|
4
|
+
# interface exposed by the Confium Rust workspace.
|
|
5
|
+
#
|
|
6
|
+
# This module is loaded lazily via autoload from lib/confium.rb.
|
|
7
|
+
# It wraps the C FFI surface for threshold sessions, coordinator,
|
|
8
|
+
# re-sharing, and KEM operations.
|
|
9
|
+
#
|
|
10
|
+
# See: TODO.roadmap/04-threshold-cryptography.md in the main confium repo
|
|
11
|
+
# for the full interface specification.
|
|
12
|
+
module Confium
|
|
13
|
+
module TC
|
|
14
|
+
autoload :Session, "confium/tc/session"
|
|
15
|
+
autoload :Coordinator, "confium/tc/coordinator"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Confium::Transparency::OTS — OpenTimestamps client interface.
|
|
4
|
+
#
|
|
5
|
+
# Wraps confium-transparency::ots::Client for anchoring Merkle tree
|
|
6
|
+
# roots in the Bitcoin blockchain via OTS calendar servers.
|
|
7
|
+
#
|
|
8
|
+
# This is a pure-Ruby stub that defines the interface. The actual
|
|
9
|
+
# OTS stamping requires network access to calendar servers and
|
|
10
|
+
# will be wired through the Rust extension in a future PR
|
|
11
|
+
# (TODO.completion/011-ots-ers-exposure.md).
|
|
12
|
+
|
|
13
|
+
module Confium
|
|
14
|
+
module Transparency
|
|
15
|
+
module OTS
|
|
16
|
+
# Default OTS calendar servers (from opentimestamps.org).
|
|
17
|
+
DEFAULT_CALENDARS = %w[
|
|
18
|
+
https://a.pool.opentimestamps.org
|
|
19
|
+
https://b.pool.opentimestamps.org
|
|
20
|
+
https://a.pool.eternitywall.com
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
# An OTS receipt proving that a hash was anchored in Bitcoin
|
|
24
|
+
# at a specific block height.
|
|
25
|
+
class Receipt
|
|
26
|
+
attr_reader :bytes, :block_height
|
|
27
|
+
|
|
28
|
+
def initialize(bytes:, block_height: nil)
|
|
29
|
+
@bytes = bytes
|
|
30
|
+
@block_height = block_height
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def to_bytes
|
|
34
|
+
@bytes
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Stamp a 32-byte hash via OTS calendar servers.
|
|
39
|
+
# Returns a Receipt (stub: returns nil — requires network).
|
|
40
|
+
#
|
|
41
|
+
# @param hash [String] 32-byte SHA-256 hash to anchor
|
|
42
|
+
# @return [Receipt, nil]
|
|
43
|
+
def self.stamp(_hash)
|
|
44
|
+
# Real implementation: calls the Rust OTS client which
|
|
45
|
+
# submits the hash to calendar servers and returns a
|
|
46
|
+
# merged proof. Requires network access.
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Verify an OTS receipt against a hash.
|
|
51
|
+
# Returns true if the receipt proves the hash was anchored.
|
|
52
|
+
#
|
|
53
|
+
# @param receipt [Receipt, String] the OTS proof
|
|
54
|
+
# @param hash [String] the 32-byte hash
|
|
55
|
+
# @return [Boolean]
|
|
56
|
+
def self.verify(_receipt, _hash)
|
|
57
|
+
# Real implementation: calls the Rust OTS verifier which
|
|
58
|
+
# walks the Bitcoin blockchain proof.
|
|
59
|
+
false
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
data/lib/confium/version.rb
CHANGED
data/lib/confium.rb
CHANGED
|
@@ -1,29 +1,59 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# Confium is the Ruby entry point for the Confium cryptographic framework.
|
|
4
|
+
#
|
|
5
|
+
# The native Rust extension is loaded first; everything else (Cert, CMS,
|
|
6
|
+
# Composite, Transparency, TC::*, etc.) is registered by the extension
|
|
7
|
+
# via magnus when this file is required.
|
|
8
|
+
#
|
|
9
|
+
# Pure-Ruby companions (the error hierarchy, Enumerable mixins, etc.)
|
|
10
|
+
# are loaded lazily via autoload — see lib/confium/<name>.rb.
|
|
4
11
|
|
|
5
|
-
|
|
6
|
-
# class Error < StandardError; end
|
|
12
|
+
require_relative "confium/version"
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
begin
|
|
15
|
+
require_relative "confium_native/confium_native"
|
|
16
|
+
rescue LoadError => e
|
|
17
|
+
warn "confium: native extension not built — run `bundle exec rake compile`"
|
|
18
|
+
raise e
|
|
19
|
+
end
|
|
13
20
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
end
|
|
21
|
+
# Register autoloads on the native-defined PKI::CMS module so Ruby
|
|
22
|
+
# companions like SignedDataBuilder load on first reference. Eager-
|
|
23
|
+
# required because the module already exists at this point.
|
|
24
|
+
require_relative "confium/pki/cms"
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
26
|
+
# The native extension defines Confium::OpenPGP with _native_armor /
|
|
27
|
+
# _native_dearmor. This file adds the idiomatic Ruby wrappers with
|
|
28
|
+
# default args. Eager-required for the same reason as PKI::CMS.
|
|
29
|
+
require_relative "confium/openpgp"
|
|
24
30
|
|
|
31
|
+
module Confium
|
|
32
|
+
# Error hierarchy autoloads. Each subclass lives in its own file so
|
|
33
|
+
# callers can `autoload :FooError, "confium/errors/foo"` and avoid
|
|
34
|
+
# loading the whole hierarchy if they only rescue one type.
|
|
35
|
+
autoload :Error, "confium/errors"
|
|
36
|
+
autoload :ParseError, "confium/errors/parse_error"
|
|
37
|
+
autoload :ValidationError, "confium/errors/validation_error"
|
|
38
|
+
autoload :VerificationError, "confium/errors/verification_error"
|
|
39
|
+
autoload :ThresholdError, "confium/errors/threshold_error"
|
|
40
|
+
autoload :CryptoError, "confium/errors/crypto_error"
|
|
41
|
+
autoload :NotFoundError, "confium/errors/not_found_error"
|
|
42
|
+
autoload :IndexError, "confium/errors/index_error"
|
|
43
|
+
autoload :UnresolvedSignerError,"confium/errors/unresolved_signer_error"
|
|
44
|
+
autoload :PolicyViolationError, "confium/errors/policy_violation_error"
|
|
45
|
+
autoload :SecureBytes, "confium/secure_bytes"
|
|
46
|
+
autoload :Policy, "confium/policy"
|
|
47
|
+
autoload :PKI, "confium/pki"
|
|
25
48
|
end
|
|
26
49
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
50
|
+
# Eager-load the Audit Ruby companion. The native extension registers
|
|
51
|
+
# `Confium::Audit` as a Ruby module with the `record`/`sink=`/`sink`
|
|
52
|
+
# methods; the companion file defines the Sink class hierarchy on top
|
|
53
|
+
# of that module.
|
|
54
|
+
require_relative "confium/audit"
|
|
55
|
+
|
|
56
|
+
# Eager-load the TC ShareFile Ruby companion. The native extension
|
|
57
|
+
# defines `Confium::TC` as a Ruby module; this file adds the
|
|
58
|
+
# `ShareFile` class for filesystem-backed share persistence.
|
|
59
|
+
require_relative "confium/tc/share_file"
|