confium 0.2.0 → 0.3.1

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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +152 -0
  3. data/Cargo.lock +2634 -0
  4. data/Cargo.toml +9 -0
  5. data/README.adoc +114 -14
  6. data/Rakefile +11 -6
  7. data/confium.gemspec +50 -29
  8. data/ext/confium_native/Cargo.toml +63 -0
  9. data/ext/confium_native/build.rs +72 -0
  10. data/ext/confium_native/extconf.rb +10 -0
  11. data/ext/confium_native/src/attributes.rs +88 -0
  12. data/ext/confium_native/src/audit.rs +169 -0
  13. data/ext/confium_native/src/composite.rs +302 -0
  14. data/ext/confium_native/src/deployment.rs +176 -0
  15. data/ext/confium_native/src/ers.rs +93 -0
  16. data/ext/confium_native/src/lib.rs +56 -0
  17. data/ext/confium_native/src/openpgp.rs +55 -0
  18. data/ext/confium_native/src/path.rs +118 -0
  19. data/ext/confium_native/src/pki.rs +431 -0
  20. data/ext/confium_native/src/tc.rs +420 -0
  21. data/ext/confium_native/src/transparency.rs +343 -0
  22. data/ext/confium_native/src/util.rs +201 -0
  23. data/lib/confium/audit.rb +125 -0
  24. data/lib/confium/cfm.rb +4 -5
  25. data/lib/confium/crypto.rb +50 -0
  26. data/lib/confium/digest.rb +11 -9
  27. data/lib/confium/errors/coerce.rb +47 -0
  28. data/lib/confium/errors/crypto_error.rb +15 -0
  29. data/lib/confium/errors/index_error.rb +15 -0
  30. data/lib/confium/errors/not_found_error.rb +15 -0
  31. data/lib/confium/errors/parse_error.rb +15 -0
  32. data/lib/confium/errors/policy_violation_error.rb +15 -0
  33. data/lib/confium/errors/threshold_error.rb +16 -0
  34. data/lib/confium/errors/unresolved_signer_error.rb +14 -0
  35. data/lib/confium/errors/validation_error.rb +17 -0
  36. data/lib/confium/errors/verification_error.rb +15 -0
  37. data/lib/confium/errors.rb +26 -0
  38. data/lib/confium/ffi.rb +23 -0
  39. data/lib/confium/lib.rb +18 -56
  40. data/lib/confium/openpgp.rb +34 -0
  41. data/lib/confium/pki/certificate_builder.rb +60 -0
  42. data/lib/confium/pki/cms/signed_data_builder.rb +92 -0
  43. data/lib/confium/pki/cms.rb +15 -0
  44. data/lib/confium/pki/cnml.rb +80 -0
  45. data/lib/confium/pki.rb +13 -0
  46. data/lib/confium/policy.rb +138 -0
  47. data/lib/confium/secure_bytes.rb +126 -0
  48. data/lib/confium/tc/coordinator.rb +68 -0
  49. data/lib/confium/tc/session.rb +51 -0
  50. data/lib/confium/tc/session_stub.rb +43 -0
  51. data/lib/confium/tc/share_file.rb +87 -0
  52. data/lib/confium/tc.rb +17 -0
  53. data/lib/confium/transparency/ots.rb +63 -0
  54. data/lib/confium/version.rb +1 -1
  55. data/lib/confium.rb +50 -20
  56. metadata +142 -25
  57. data/CODE_OF_CONDUCT.md +0 -84
  58. data/Gemfile +0 -10
  59. data/sig/confium.rbs +0 -4
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # OIML CNML certificate profile definition.
4
+ #
5
+ # Defines the required X.509 extensions for measuring instrument
6
+ # certificates per OIML R 76 (non-automatic weighing instruments)
7
+ # and the broader CNML framework.
8
+ #
9
+ # This is an interface definition — the actual OIML R 76 PDF is a
10
+ # paid publication. The extension OIDs listed here are from public
11
+ # OIML documentation and the BIPM/CIPM MRA framework.
12
+
13
+ module Confium
14
+ module PKI
15
+ module CNML
16
+ # Required X.509 v3 extensions for a CNML certificate.
17
+ REQUIRED_EXTENSIONS = {
18
+ # Standard X.509 extensions required by CNML:
19
+ '2.5.29.19' => 'basicConstraints (CA=true for IA certs, CA=false for leaf)',
20
+ '2.5.29.15' => 'keyUsage (digitalSignature for signing certs)',
21
+ '2.5.29.37' => 'extKeyUsage (id-kp-OCSPSigning or custom CNML OIDs)',
22
+ '2.5.29.14' => 'subjectKeyIdentifier (required for CMS signer resolution)',
23
+ '2.5.29.35' => 'authorityKeyIdentifier (required for chain building)'
24
+ }.freeze
25
+
26
+ # Optional but recommended extensions.
27
+ OPTIONAL_EXTENSIONS = {
28
+ '2.5.29.31' => 'cRLDistributionPoints (for revocation checking)',
29
+ '2.5.29.32' => 'certificatePolicies (CNML policy OID)',
30
+ '1.3.6.1.5.5.7.1.1' => 'authorityInfoAccess (OCSP responder URL)'
31
+ }.freeze
32
+
33
+ # CNML certificate roles (maps to ActorType in Confium::Identity).
34
+ CERT_ROLES = %i[
35
+ manufacturer
36
+ testing_lab
37
+ issuing_authority_officer
38
+ biml_director
39
+ quorum_coordinator
40
+ verifier
41
+ ].freeze
42
+
43
+ # Validate that a certificate has the required CNML extensions.
44
+ # This is a structural check (extension OID presence), not a
45
+ # semantic check (extension value correctness). Full validation
46
+ # requires the OIML R 76 specification.
47
+ #
48
+ # @param cert [Confium::PKI::Certificate] the cert to check
49
+ # @return [Array<String>] list of missing required extension OIDs
50
+ def self.missing_extensions(_cert)
51
+ # Full implementation requires parsing the cert's extensions,
52
+ # which needs the x509-cert crate exposed through the Ruby
53
+ # extension. For now, returns an empty array (no validation).
54
+ #
55
+ # TODO: when confium-pki exposes Certificate#extensions, walk
56
+ # the extension list and cross-reference against
57
+ # REQUIRED_EXTENSIONS.
58
+ []
59
+ end
60
+
61
+ # The list of required extension OIDs.
62
+ # @return [Array<String>]
63
+ def self.required_extension_oids
64
+ REQUIRED_EXTENSIONS.keys
65
+ end
66
+
67
+ # The list of optional extension OIDs.
68
+ # @return [Array<String>]
69
+ def self.optional_extension_oids
70
+ OPTIONAL_EXTENSIONS.keys
71
+ end
72
+
73
+ # All known CNML certificate roles.
74
+ # @return [Array<Symbol>]
75
+ def self.cert_roles
76
+ CERT_ROLES
77
+ end
78
+ end
79
+ end
80
+ end
@@ -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.nil?
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,126 @@
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
+ module Confium
18
+ class SecureBytes
19
+ # Raised when #bytes is called after #clear.
20
+ class ClearedError < Confium::Error
21
+ def initialize(message = 'SecureBytes already cleared')
22
+ super(message, details: {})
23
+ end
24
+ end
25
+
26
+ # Create a SecureBytes wrapping a copy of the given String.
27
+ # The original String's contents are NOT modified; callers should
28
+ # zeroize the original separately if needed.
29
+ # @param raw [String] binary String (any encoding; bytes are copied)
30
+ # @return [Confium::SecureBytes]
31
+ def self.wrap(raw)
32
+ new(raw)
33
+ end
34
+
35
+ # @api private
36
+ def initialize(raw)
37
+ @buffer = raw.dup.force_encoding(Encoding::ASCII_8BIT)
38
+ @cleared = false
39
+ # Register finalizer to zeroize if the object is GC'd without
40
+ # an explicit #clear call.
41
+ ObjectSpace.define_finalizer(self, finalizer_proc)
42
+ end
43
+
44
+ # Non-destructive read of the wrapped bytes.
45
+ # @return [String] binary String (ASCII-8BIT encoding)
46
+ # @raise [ClearedError] if #clear was already called
47
+ def bytes
48
+ raise ClearedError if @cleared
49
+
50
+ @buffer.dup
51
+ end
52
+
53
+ # Destructive read: returns a copy, then zeroizes the original.
54
+ # @return [String] binary String
55
+ # @raise [ClearedError] if #clear was already called
56
+ def bytes!
57
+ raise ClearedError if @cleared
58
+
59
+ copy = @buffer.dup
60
+ clear
61
+ copy
62
+ end
63
+
64
+ # Number of bytes. Returns 0 after #clear.
65
+ # @return [Integer]
66
+ def length
67
+ @cleared ? 0 : @buffer.bytesize
68
+ end
69
+
70
+ alias size length
71
+
72
+ # Whether the buffer has been cleared.
73
+ # @return [Boolean]
74
+ def cleared?
75
+ @cleared
76
+ end
77
+
78
+ # Zeroize the buffer immediately. Idempotent.
79
+ # @return [self]
80
+ def clear
81
+ return self if @cleared
82
+
83
+ # Overwrite every byte with 0x00 in place.
84
+ @buffer.replace("\x00" * @buffer.bytesize)
85
+ @buffer = nil
86
+ @cleared = true
87
+ self
88
+ end
89
+
90
+ # String representation for debugging. Does NOT expose the raw bytes.
91
+ # @return [String]
92
+ def inspect
93
+ if @cleared
94
+ "#<Confium::SecureBytes:0x#{object_id.to_s(16)} CLEARED>"
95
+ else
96
+ "#<Confium::SecureBytes:0x#{object_id.to_s(16)} #{length} bytes>"
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ # Finalizer proc that zeroizes the buffer if GC collects this
103
+ # object without an explicit #clear. Uses object_id to find the
104
+ # buffer — but since the buffer is an instance variable that may
105
+ # already be collected, this is a best-effort path. Explicit #clear
106
+ # is the recommended path.
107
+ # @return [Proc]
108
+ def finalizer_proc
109
+ method(:finalize)
110
+ end
111
+
112
+ # Called by the GC finalizer.
113
+ def finalize(_id)
114
+ # Best-effort: the buffer may already be collected by the time
115
+ # the finalizer runs. If @buffer still exists, zeroize it.
116
+ # This is a closure over the instance — MRI guarantees the
117
+ # finalizer runs after the object is unreachable but before
118
+ # the buffer's memory is reused.
119
+ return if @cleared
120
+
121
+ @buffer&.replace("\x00" * @buffer.bytesize)
122
+ @buffer = nil
123
+ @cleared = true
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,68 @@
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: 14_400)
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
+ return unless session[:commitments].length >= session[:threshold]
49
+
50
+ session[:state] = :commitments_collected
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
+
62
+ session[:state] = :completed
63
+ # In real implementation, calls FFI to aggregate shares
64
+ session[:shares].map { |s| s[:bytes] }.join
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,51 @@
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Confium
4
- VERSION = "0.2.0"
4
+ VERSION = '0.3.1'
5
5
  end