confium 0.5.0 → 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.
@@ -16,6 +16,14 @@ crate-type = ["cdylib"]
16
16
 
17
17
  build = "build.rs"
18
18
 
19
+ [features]
20
+ # OpenPGP signature verification via a vendored librnp (librnp +
21
+ # Botan + json-c). OFF by default: the C/C++ build would restore the
22
+ # multi-minute compile times and heavy binaries 0.4.0 removed (armor
23
+ # itself is pure Ruby). Source builders opt in; pre-compiled platform
24
+ # gems always build default features.
25
+ pgp = ["rnp"]
26
+
19
27
  [dependencies]
20
28
  # rb-sys for Ruby C API access.
21
29
  rb-sys = { version = "0.9.124", features = ["global-allocator"] }
@@ -57,3 +65,11 @@ serde_json = "1"
57
65
 
58
66
  [profile.release]
59
67
  lto = "off"
68
+
69
+ # OpenPGP verification (feature "pgp") — vendored librnp via rnp-rs.
70
+ # No system librnp needed; adds the Botan/json-c C/C++ build.
71
+ [dependencies.rnp]
72
+ package = "rnp-rs"
73
+ version = "0.1.10"
74
+ features = ["vendored"]
75
+ optional = true
@@ -7,4 +7,9 @@ create_rust_makefile('confium_native') do |r|
7
7
  r.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :dev).to_sym
8
8
  r.use_stable_api_compiled_fallback = true
9
9
  r.force_install_rust_toolchain = false
10
+ # rb_sys only threads --features through to cargo when the builder
11
+ # has a non-empty feature list; seeding it from the env var makes
12
+ # RB_SYS_CARGO_FEATURES=pgp bundle exec rake compile work.
13
+ r.features = ENV.fetch('RB_SYS_CARGO_FEATURES', '')
14
+ .split(',').map(&:strip).reject(&:empty?)
10
15
  end
@@ -9,6 +9,7 @@ mod attributes;
9
9
  mod composite;
10
10
  mod deployment;
11
11
  mod ers;
12
+ mod openpgp_verify;
12
13
  mod path;
13
14
  mod pki;
14
15
  mod tc;
@@ -42,6 +43,9 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
42
43
  confium.define_module_function("core_version", function!(core_version, 0))?;
43
44
 
44
45
  transparency::init(ruby, confium)?;
46
+ openpgp_verify::init(ruby, confium)?;
47
+ #[cfg(feature = "pgp")]
48
+ openpgp_verify::init_pgp(ruby, &confium)?;
45
49
  composite::init(ruby, confium)?;
46
50
  attributes::init(ruby, confium)?;
47
51
  pki::init(ruby, confium)?;
@@ -0,0 +1,189 @@
1
+ //! Confium::OpenPGP — OpenPGP (RFC 9580) signature verification via
2
+ //! the vendored librnp, behind the `pgp` cargo feature.
3
+ //!
4
+ //! Armor (encode/decode) is pure Ruby in lib/confium/openpgp.rb and
5
+ //! always available. This module adds verification only, and only
6
+ //! when the extension was built with `--features pgp`: the vendored
7
+ //! librnp brings a full Botan/json-c C/C++ build that default
8
+ //! (platform-gem) builds deliberately exclude.
9
+
10
+ use magnus::{Error, Module, RModule, Ruby, Value, function, prelude::*};
11
+
12
+ use crate::util::runtime;
13
+
14
+ fn stub_message(method: &str) -> String {
15
+ format!(
16
+ "Confium::OpenPGP.{} requires the pgp cargo feature — rebuild \
17
+ the extension with --features pgp (platform gems build \
18
+ default features)",
19
+ method
20
+ )
21
+ }
22
+
23
+ /// Registered when the extension builds WITHOUT the pgp feature:
24
+ /// `Confium::OpenPGP::PGP_AVAILABLE` is false and the verify methods
25
+ /// raise with instructions instead of pretending.
26
+ pub fn init(_ruby: &Ruby, parent: RModule) -> Result<(), Error> {
27
+ let openpgp = parent.define_module("OpenPGP")?;
28
+ let _ = openpgp.const_set("PGP_AVAILABLE", false);
29
+ openpgp.define_singleton_method(
30
+ "verify_detached",
31
+ function!(
32
+ move |_args: &[Value]| -> Result<Value, Error> {
33
+ Err(runtime(stub_message("verify_detached")))
34
+ },
35
+ -1
36
+ ),
37
+ )?;
38
+ openpgp.define_singleton_method(
39
+ "verify",
40
+ function!(
41
+ move |_args: &[Value]| -> Result<Value, Error> { Err(runtime(stub_message("verify"))) },
42
+ -1
43
+ ),
44
+ )?;
45
+ Ok(())
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // pgp feature implementation
50
+ // ---------------------------------------------------------------------------
51
+ #[cfg(feature = "pgp")]
52
+ mod pgp_impl {
53
+ use super::*;
54
+ use crate::util::{bytes_from_value, crypto_error, parse_error};
55
+
56
+ fn result_to_hash(ruby: &Ruby, result: &rnp::VerifyResult) -> Result<Value, Error> {
57
+ let hash = ruby.hash_new();
58
+ let _ = hash.aset("any_valid", result.any_valid().unwrap_or(false));
59
+ let _ = hash.aset(
60
+ "signature_count",
61
+ result.signature_count().unwrap_or(0) as i64,
62
+ );
63
+ let arr = ruby.ary_new();
64
+ for s in result.iter_signatures() {
65
+ let sh = ruby.hash_new();
66
+ let _ = sh.aset("valid", s.status_is_valid());
67
+ let _ = sh.aset("status", format!("{:?}", s.status()));
68
+ let _ = sh.aset("key_id", s.keyid().unwrap_or_else(|_| "?".to_string()));
69
+ let (created, expires) = s.times().unwrap_or((0, 0));
70
+ let _ = sh.aset("creation_time", created as i64);
71
+ let _ = sh.aset("expiration_time", expires as i64);
72
+ if let Ok(alg) = s.hash() {
73
+ let _ = sh.aset("hash", alg);
74
+ }
75
+ arr.push(sh)?;
76
+ }
77
+ let _ = hash.aset("signatures", arr);
78
+ Ok(hash.as_value())
79
+ }
80
+
81
+ fn negative_result(ruby: &Ruby) -> Result<Value, Error> {
82
+ // librnp reports a failed signature check as an error from
83
+ // rnp_op_verify_execute; for a verifier API that is the
84
+ // answer, not an exception.
85
+ let hash = ruby.hash_new();
86
+ let _ = hash.aset("any_valid", false);
87
+ let _ = hash.aset("signature_count", 0 as i64);
88
+ let _ = hash.aset("signatures", ruby.ary_new());
89
+ Ok(hash.as_value())
90
+ }
91
+
92
+ fn ctx() -> Result<rnp::Context, Error> {
93
+ rnp::Context::new().map_err(|e| crypto_error(e.to_string(), "OpenPGP", "openpgp"))
94
+ }
95
+
96
+ fn import_keys(context: &rnp::Context, keys: &[Value]) -> Result<(), Error> {
97
+ for k in keys {
98
+ let bytes = bytes_from_value(*k)?;
99
+ context
100
+ .import_keys(&bytes, rnp::LoadSaveFlags::PUBLIC)
101
+ .map_err(|e| {
102
+ parse_error(e.to_string(), "OpenPGP key import", Some("openpgp"), None)
103
+ })?;
104
+ }
105
+ Ok(())
106
+ }
107
+
108
+ /// `(message, signature, keys?)` → `(message, signature, keys)`
109
+ fn split_args(
110
+ args: &[Value],
111
+ min: usize,
112
+ method: &str,
113
+ ) -> Result<(Value, Option<Value>, Vec<Value>), Error> {
114
+ if args.len() < min || args.len() > min + 1 {
115
+ return Err(crate::util::arg_error(format!(
116
+ "Confium::OpenPGP.{}: wrong number of arguments (given {}, expected {}..{})",
117
+ method,
118
+ args.len(),
119
+ min,
120
+ min + 1
121
+ )));
122
+ }
123
+ let keys = if args.len() == min + 1 {
124
+ match <magnus::RArray as magnus::TryConvert>::try_convert(args[min]) {
125
+ Ok(array) => array.into_iter().collect(),
126
+ Err(_) => vec![args[min]],
127
+ }
128
+ } else {
129
+ Vec::new()
130
+ };
131
+ Ok((args[0], args.get(1).copied(), keys))
132
+ }
133
+
134
+ pub fn verify_detached(args: &[Value]) -> Result<Value, Error> {
135
+ let ruby = Ruby::get().map_err(|e| runtime(e.to_string()))?;
136
+ let (message, signature, keys) = split_args(args, 2, "verify_detached")?;
137
+ let msg = bytes_from_value(message)?;
138
+ let sig = bytes_from_value(
139
+ signature.ok_or_else(|| crate::util::arg_error("missing signature"))?,
140
+ )?;
141
+
142
+ let context = ctx()?;
143
+ import_keys(&context, &keys)?;
144
+ let result = match rnp::verify_detached(&context, &msg, &sig) {
145
+ Ok(result) => result_to_hash(&ruby, &result)?,
146
+ Err(e) if e.kind() == rnp::ErrorKind::SignatureInvalid => negative_result(&ruby)?,
147
+ Err(e) => {
148
+ return Err(parse_error(
149
+ e.to_string(),
150
+ "OpenPGP.verify_detached",
151
+ Some("openpgp"),
152
+ None,
153
+ ));
154
+ }
155
+ };
156
+ Ok(result)
157
+ }
158
+
159
+ pub fn verify(args: &[Value]) -> Result<Value, Error> {
160
+ let ruby = Ruby::get().map_err(|e| runtime(e.to_string()))?;
161
+ let (signed, _signature_ignored, keys) = split_args(args, 1, "verify")?;
162
+ let msg = bytes_from_value(signed)?;
163
+
164
+ let context = ctx()?;
165
+ import_keys(&context, &keys)?;
166
+ let result = match rnp::verify(&context, &msg) {
167
+ Ok(result) => result_to_hash(&ruby, &result)?,
168
+ Err(e) if e.kind() == rnp::ErrorKind::SignatureInvalid => negative_result(&ruby)?,
169
+ Err(e) => {
170
+ return Err(parse_error(
171
+ e.to_string(),
172
+ "OpenPGP.verify",
173
+ Some("openpgp"),
174
+ None,
175
+ ));
176
+ }
177
+ };
178
+ Ok(result)
179
+ }
180
+ }
181
+
182
+ #[cfg(feature = "pgp")]
183
+ pub fn init_pgp(_ruby: &Ruby, parent: &RModule) -> Result<(), Error> {
184
+ let openpgp = parent.define_module("OpenPGP")?;
185
+ let _ = openpgp.const_set("PGP_AVAILABLE", true);
186
+ openpgp.define_singleton_method("verify_detached", function!(pgp_impl::verify_detached, -1))?;
187
+ openpgp.define_singleton_method("verify", function!(pgp_impl::verify, -1))?;
188
+ Ok(())
189
+ }
@@ -16,19 +16,24 @@ module Confium
16
16
  # service_name: 'confium-issuer'
17
17
  # )
18
18
  #
19
- # Delivery is synchronous per record; failures are reported on
20
- # $stderr and the record is dropped (the audit core's policy — a
21
- # telemetry outage must never break signing). stdlib only.
19
+ # Delivery is synchronous per record, retried with exponential
20
+ # backoff (retries: attempts after the first, base doubled per
21
+ # attempt). After the final failure the record is dropped with a
22
+ # $stderr report — the audit core's policy: a telemetry outage
23
+ # must never break signing. stdlib only.
22
24
  class OtlpSink < Sink
23
25
  DEFAULT_ENDPOINT = 'http://localhost:4318/v1/logs'
24
26
  SEVERITY = { 'success' => 9, 'failure' => 17, 'error' => 17 }.freeze # INFO / ERROR
25
27
 
26
- def initialize(endpoint: DEFAULT_ENDPOINT, headers: {}, service_name: 'confium', timeout: 5)
28
+ def initialize(endpoint: DEFAULT_ENDPOINT, headers: {}, service_name: 'confium', timeout: 5,
29
+ retries: 2, retry_base: 0.1)
27
30
  super()
28
31
  @uri = URI.parse(endpoint)
29
32
  @headers = headers
30
33
  @service_name = service_name
31
34
  @timeout = timeout
35
+ @retries = retries
36
+ @retry_base = retry_base
32
37
  @dropped = 0
33
38
  end
34
39
 
@@ -37,8 +42,7 @@ module Confium
37
42
 
38
43
  def write(record)
39
44
  payload = envelope(record)
40
- response = Net::HTTP.post(@uri, JSON.generate(payload), headers.merge('Content-Type' => 'application/json'))
41
- raise "collector responded #{response.code}" unless response.is_a?(Net::HTTPSuccess)
45
+ deliver(JSON.generate(payload))
42
46
  rescue StandardError => e
43
47
  @dropped += 1
44
48
  warn "confium: OTLP delivery failed, audit record dropped (##{@dropped}): #{e.class}: #{e.message}"
@@ -48,6 +52,19 @@ module Confium
48
52
 
49
53
  private
50
54
 
55
+ # Attempts grow the backoff: retry_base * 2**attempt. When
56
+ # retry_base is 0 (specs) retries run without delay.
57
+ def deliver(body)
58
+ (@retries + 1).times do |attempt|
59
+ sleep(@retry_base * (2**attempt)) if attempt.positive? && @retry_base.positive?
60
+
61
+ response = Net::HTTP.post(@uri, body, headers.merge('Content-Type' => 'application/json'))
62
+ return response if response.is_a?(Net::HTTPSuccess)
63
+
64
+ raise "collector responded #{response.code}" if attempt == @retries
65
+ end
66
+ end
67
+
51
68
  def headers
52
69
  { 'User-Agent' => "confium-otlp-sink #{Confium::VERSION}" }.merge(@headers)
53
70
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Confium
4
+ # Pure resolution of which native-extension ABI windows to try, in
5
+ # order. Extracted from the loader so the decision — the piece with
6
+ # the release-incident history (3.1-window segfaults on 3.2, the
7
+ # cross-major 3.3→4.0 TypeError, windowless platform gems) — is
8
+ # testable without installing eight platform gems.
9
+ module NativeWindows
10
+ # Ordered candidate windows for +ruby_version+ (e.g. "3.4.8").
11
+ #
12
+ # Background: platform gems ship one extension per ABI window.
13
+ # Ruby 3.2 broke the 3.1 ABI (object shapes) and rb-sys references
14
+ # a VM pointer libruby stopped exporting in 3.3, so 3.1/3.2/4.0
15
+ # get exact-minor builds while a 3.3-window binary loads on both
16
+ # 3.3 and 3.4. The 3.3 fallback applies within the 3.x line
17
+ # alone: a cross-major load (4.x) fails TypedData class-identity
18
+ # checks, so 4.0 must not fall back to it.
19
+ def self.candidates(ruby_version)
20
+ minor = ruby_version[/\A\d+\.\d+/]
21
+ raise ArgumentError, "not a ruby version: #{ruby_version.inspect}" unless minor
22
+
23
+ major = ruby_version[/\A\d+/].to_i
24
+ if major == 3 && Gem::Version.new(ruby_version) >= Gem::Version.new('3.3')
25
+ [minor, '3.3'].uniq
26
+ else
27
+ [minor]
28
+ end
29
+ end
30
+ end
31
+ end
@@ -3,9 +3,13 @@
3
3
  # Confium::TC::Coordinator coordinates an in-process threshold
4
4
  # signing session: signers submit commitments and shares at their own
5
5
  # pace; once the threshold is met, aggregation runs the real
6
- # threshold-ECDSA combine (CMP20 or GG18 the same in-process
7
- # drivers behind Confium::TC::Cmp20 / Confium::TC::Gg18) and returns
8
- # a 64-byte (r, s) signature verifiable under the quorum public key.
6
+ # threshold-ECDSA combine (CMP20 or GG18) and returns a 64-byte
7
+ # (r, s) signature verifiable under the quorum public key.
8
+ #
9
+ # The session semantics (state machine, duplicate-signer rejection,
10
+ # the combine) live in SigningSession; this class is the session
11
+ # registry and quorum-scoped entry point. NetworkCoordinator is the
12
+ # TCP/NDJSON adapter over the same sessions.
9
13
  #
10
14
  # Usage:
11
15
  # coordinator = Confium::TC::Coordinator.new(quorum_id: "biml-root")
@@ -27,54 +31,38 @@ module Confium
27
31
  @sessions = {}
28
32
  end
29
33
 
30
- SCHEMES = {
31
- 'CMP20-ECDSA-P256' => ->(shares, threshold, message) { Cmp20.sign(shares, threshold, message) },
32
- 'GG18-ECDSA-P256' => ->(shares, threshold, message) { Gg18.sign(shares, threshold, message) }
33
- }.freeze
34
-
35
- def create_session(message:, threshold:, unlock_window: 14_400, scheme: 'CMP20-ECDSA-P256')
36
- raise ArgumentError, "unknown scheme: #{scheme} (known: #{SCHEMES.keys.join(', ')})" unless SCHEMES.key?(scheme)
37
-
34
+ def create_session(message:, threshold:, unlock_window: SigningSession::DEFAULT_UNLOCK_WINDOW,
35
+ scheme: 'CMP20-ECDSA-P256')
38
36
  session_id = "session-#{@sessions.length + 1}"
39
- @sessions[session_id] = {
40
- message: message,
41
- threshold: threshold,
42
- unlock_window: unlock_window,
43
- scheme: scheme,
44
- state: :pending,
45
- commitments: [],
46
- shares: []
47
- }
37
+ @sessions[session_id] = SigningSession.new(
38
+ message: message, threshold: threshold,
39
+ unlock_window: unlock_window, scheme: scheme
40
+ )
48
41
  session_id
49
42
  end
50
43
 
51
44
  def session_state(session_id)
52
- @sessions.dig(session_id, :state)
45
+ fetch(session_id).state
53
46
  end
54
47
 
55
48
  def submit_commitment(session_id, signer_id, commitment_bytes)
56
- session = @sessions[session_id] or raise "Unknown session: #{session_id}"
57
- session[:commitments] << { signer_id: signer_id, bytes: commitment_bytes }
58
- return unless session[:commitments].length >= session[:threshold]
59
-
60
- session[:state] = :commitments_collected
49
+ fetch(session_id).add_commitment(signer_id, commitment_bytes)
61
50
  end
62
51
 
63
52
  def submit_share(session_id, signer_id, share_bytes)
64
- session = @sessions[session_id] or raise "Unknown session: #{session_id}"
65
- session[:shares] << { signer_id: signer_id, bytes: share_bytes }
53
+ fetch(session_id).add_share(signer_id, share_bytes)
66
54
  end
67
55
 
68
56
  def aggregate(session_id)
69
- session = @sessions[session_id] or raise "Unknown session: #{session_id}"
70
- raise ThresholdError, 'Threshold not met' if session[:shares].length < session[:threshold]
57
+ fetch(session_id).aggregate
58
+ end
71
59
 
72
- session[:state] = :completed
73
- SCHEMES.fetch(session[:scheme]).call(
74
- session[:shares].map { |s| s[:bytes] },
75
- session[:threshold],
76
- session[:message]
77
- )
60
+ private
61
+
62
+ def fetch(session_id)
63
+ @sessions.fetch(session_id) do
64
+ raise NotFoundError.new("Unknown session: #{session_id}", kind: :session, identifier: session_id)
65
+ end
78
66
  end
79
67
  end
80
68
  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,12 +11,10 @@
11
11
  # for the full interface specification.
12
12
  module Confium
13
13
  module TC
14
- # The native extension defines Confium::TC; this namespace file is
15
- # eager-required from confium.rb, so autoloads registered here do
16
- # fire. Coordinator and ShareFile are pure Ruby and load with the
17
- # namespace; Session wraps the engine via FFI and needs the
18
- # external libconfium dylib, so it stays lazy.
19
- autoload :Session, 'confium/tc/session'
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'
20
18
  require_relative 'tc/coordinator'
21
19
  require_relative 'tc/network_coordinator'
22
20
  require_relative 'tc/share_file'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Confium
4
- VERSION = '0.5.0'
4
+ VERSION = '0.6.0'
5
5
  end
data/lib/confium.rb CHANGED
@@ -10,25 +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 C-ABI window
16
- # (Ruby 3.2 broke the 3.1 ABI — object shapes — and rb-sys
17
- # references the VM pointer libruby stopped exporting in 3.3, so
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
- major = RUBY_VERSION[/\A\d+/].to_i
24
- # 3.3-window binaries load on 3.3/3.4 only; a cross-major load
25
- # (4.x) fails TypedData class checks, so the fallback applies
26
- # within the 3.x line alone.
27
- candidates = major == 3 && Gem::Version.new(RUBY_VERSION) >= Gem::Version.new('3.3') ? [minor, '3.3'].uniq : [minor]
28
- # Windows gems carry an exact-minor window per Ruby (a PE import
29
- # names the version-specific ruby DLL); other platforms share the
30
- # 3.3 window for 3.3+. Prefer the exact minor when present.
31
- windowed = candidates.filter_map do |w|
20
+ windowed = Confium::NativeWindows.candidates(RUBY_VERSION).filter_map do |w|
32
21
  path = File.expand_path("confium_native/#{w}/confium_native.#{dlext}", __dir__ || '.')
33
22
  w if File.exist?(path)
34
23
  end
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.5.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-24 00:00:00.000000000 Z
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
@@ -129,10 +130,7 @@ files:
129
130
  - lib/confium.rb
130
131
  - lib/confium/audit.rb
131
132
  - lib/confium/audit/otlp_sink.rb
132
- - lib/confium/cfm.rb
133
133
  - lib/confium/composite.rb
134
- - lib/confium/crypto.rb
135
- - lib/confium/digest.rb
136
134
  - lib/confium/errors.rb
137
135
  - lib/confium/errors/coerce.rb
138
136
  - lib/confium/errors/crypto_error.rb
@@ -144,8 +142,7 @@ files:
144
142
  - lib/confium/errors/unresolved_signer_error.rb
145
143
  - lib/confium/errors/validation_error.rb
146
144
  - lib/confium/errors/verification_error.rb
147
- - lib/confium/ffi.rb
148
- - lib/confium/lib.rb
145
+ - lib/confium/native_windows.rb
149
146
  - lib/confium/openpgp.rb
150
147
  - lib/confium/pki.rb
151
148
  - lib/confium/pki/certificate_builder.rb
@@ -157,9 +154,8 @@ files:
157
154
  - lib/confium/tc.rb
158
155
  - lib/confium/tc/coordinator.rb
159
156
  - lib/confium/tc/network_coordinator.rb
160
- - lib/confium/tc/session.rb
161
- - lib/confium/tc/session_stub.rb
162
157
  - lib/confium/tc/share_file.rb
158
+ - lib/confium/tc/signing_session.rb
163
159
  - lib/confium/transparency.rb
164
160
  - lib/confium/transparency/ots.rb
165
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