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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +126 -0
  3. data/Cargo.lock +2534 -0
  4. data/Cargo.toml +9 -0
  5. data/README.adoc +114 -14
  6. data/Rakefile +8 -3
  7. data/confium.gemspec +42 -21
  8. data/ext/confium_native/Cargo.toml +62 -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 +341 -0
  22. data/ext/confium_native/src/util.rs +196 -0
  23. data/lib/confium/audit.rb +125 -0
  24. data/lib/confium/cfm.rb +2 -4
  25. data/lib/confium/crypto.rb +50 -0
  26. data/lib/confium/digest.rb +7 -7
  27. data/lib/confium/errors/coerce.rb +49 -0
  28. data/lib/confium/errors/crypto_error.rb +13 -0
  29. data/lib/confium/errors/index_error.rb +13 -0
  30. data/lib/confium/errors/not_found_error.rb +13 -0
  31. data/lib/confium/errors/parse_error.rb +13 -0
  32. data/lib/confium/errors/policy_violation_error.rb +13 -0
  33. data/lib/confium/errors/threshold_error.rb +14 -0
  34. data/lib/confium/errors/unresolved_signer_error.rb +12 -0
  35. data/lib/confium/errors/validation_error.rb +15 -0
  36. data/lib/confium/errors/verification_error.rb +13 -0
  37. data/lib/confium/errors.rb +26 -0
  38. data/lib/confium/ffi.rb +23 -0
  39. data/lib/confium/lib.rb +2 -39
  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 +124 -0
  48. data/lib/confium/tc/coordinator.rb +67 -0
  49. data/lib/confium/tc/session.rb +49 -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,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
@@ -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.0"
5
5
  end
data/lib/confium.rb CHANGED
@@ -1,29 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "confium/version"
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
- module Confium
6
- # class Error < StandardError; end
12
+ require_relative "confium/version"
7
13
 
8
- # def self.context
9
- # cfm = Confium::CFM.new
10
- # cfm.load_plugin('botan', ENV['CFM_HASH_BOTAN_PLUGIN_PATH'])
11
- # cfm
12
- # end
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
- def self.call_ffi_rc(fn, *args)
15
- rc = Confium::Lib.method(fn).call(*args)
16
- raise "FFI call to #{fn} failed (rc: #{rc})" unless rc.zero?
17
- rc
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
- def self.call_ffi(fn, *args)
21
- call_ffi_rc(fn, *args)
22
- nil
23
- end
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
- require_relative 'confium/lib'
28
- require_relative 'confium/cfm'
29
- require_relative 'confium/digest'
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"