confium 0.3.2-x86_64-linux

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 (45) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +183 -0
  3. data/Cargo.lock +2635 -0
  4. data/Cargo.toml +9 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.adoc +139 -0
  7. data/Rakefile +30 -0
  8. data/confium.gemspec +56 -0
  9. data/lib/confium/audit.rb +125 -0
  10. data/lib/confium/cfm.rb +23 -0
  11. data/lib/confium/crypto.rb +50 -0
  12. data/lib/confium/digest.rb +62 -0
  13. data/lib/confium/errors/coerce.rb +47 -0
  14. data/lib/confium/errors/crypto_error.rb +15 -0
  15. data/lib/confium/errors/index_error.rb +15 -0
  16. data/lib/confium/errors/not_found_error.rb +15 -0
  17. data/lib/confium/errors/parse_error.rb +15 -0
  18. data/lib/confium/errors/policy_violation_error.rb +15 -0
  19. data/lib/confium/errors/threshold_error.rb +16 -0
  20. data/lib/confium/errors/unresolved_signer_error.rb +14 -0
  21. data/lib/confium/errors/validation_error.rb +17 -0
  22. data/lib/confium/errors/verification_error.rb +15 -0
  23. data/lib/confium/errors.rb +26 -0
  24. data/lib/confium/ffi.rb +23 -0
  25. data/lib/confium/lib.rb +33 -0
  26. data/lib/confium/openpgp.rb +34 -0
  27. data/lib/confium/pki/certificate_builder.rb +60 -0
  28. data/lib/confium/pki/cms/signed_data_builder.rb +92 -0
  29. data/lib/confium/pki/cms.rb +15 -0
  30. data/lib/confium/pki/cnml.rb +80 -0
  31. data/lib/confium/pki.rb +13 -0
  32. data/lib/confium/policy.rb +138 -0
  33. data/lib/confium/secure_bytes.rb +126 -0
  34. data/lib/confium/tc/coordinator.rb +68 -0
  35. data/lib/confium/tc/session.rb +51 -0
  36. data/lib/confium/tc/session_stub.rb +43 -0
  37. data/lib/confium/tc/share_file.rb +87 -0
  38. data/lib/confium/tc.rb +17 -0
  39. data/lib/confium/transparency/ots.rb +63 -0
  40. data/lib/confium/version.rb +5 -0
  41. data/lib/confium.rb +71 -0
  42. data/lib/confium_native/3.1/confium_native.so +0 -0
  43. data/lib/confium_native/3.2/confium_native.so +0 -0
  44. data/lib/confium_native/3.3/confium_native.so +0 -0
  45. metadata +178 -0
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when input is well-formed but semantically invalid (wrong size,
4
+ # out-of-range value, etc.).
5
+ module Confium
6
+ class ValidationError < Confium::Error
7
+ attr_reader :param, :expected, :actual
8
+
9
+ def initialize(message = nil, details_hash = nil, **kwargs)
10
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
11
+ @param = kwargs.delete(:param)
12
+ @expected = kwargs.delete(:expected)
13
+ @actual = kwargs.delete(:actual)
14
+ super(message, details: { param: @param, expected: @expected, actual: @actual, **kwargs })
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a signature / hash / proof fails to verify.
4
+ module Confium
5
+ class VerificationError < Confium::Error
6
+ attr_reader :signer_index, :algorithm
7
+
8
+ def initialize(message = nil, details_hash = nil, **kwargs)
9
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
10
+ @signer_index = kwargs.delete(:signer_index)
11
+ @algorithm = kwargs.delete(:algorithm)
12
+ super(message, details: { signer_index: @signer_index, algorithm: @algorithm, **kwargs })
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Root of all Confium errors. Loaded by every subclass file.
4
+ module Confium
5
+ module Errors
6
+ # Marker namespace for error-hierarchy internals. `Coerce` lives
7
+ # under here so subclasses can reference it as
8
+ # `Confium::Errors::Coerce.args(...)` without polluting the
9
+ # top-level `Confium` namespace.
10
+ end
11
+
12
+ class Error < StandardError
13
+ attr_reader :details
14
+
15
+ def initialize(message = nil, details: {})
16
+ @details = details.transform_keys(&:to_sym)
17
+ super(message)
18
+ end
19
+
20
+ def to_h
21
+ { class: self.class.name, message: message, details: details }
22
+ end
23
+ end
24
+ end
25
+
26
+ require_relative 'errors/coerce'
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ # {Confium::FFI} is the namespace for everything that touches the native
6
+ # Confium shared library through Ruby-FFI.
7
+ #
8
+ # This file registers autoload entries for the planned FFI helper modules
9
+ # (Library, Error, Options). They are listed in the TODO #14 architecture
10
+ # (see `TODO.finalize/14-ruby-bindings-architecture.md`) and will be added
11
+ # as the bindings grow; until then the autoloads are inert — they only
12
+ # trigger a load when the corresponding constant is first referenced.
13
+ #
14
+ # Note: the legacy FFI library wrapper still lives at `Confium::Lib`
15
+ # (file `confium/lib.rb`) and is autoloaded from `confium.rb`. It will be
16
+ # migrated into `Confium::FFI::Library` in a follow-up.
17
+ module Confium
18
+ module FFI
19
+ autoload :Library, 'confium/ffi/library'
20
+ autoload :Error, 'confium/ffi/error'
21
+ autoload :Options, 'confium/ffi/options'
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ffi'
4
+
5
+ module Confium
6
+ module Lib
7
+ extend ::FFI::Library
8
+
9
+ FFI_LAYOUT = {
10
+ cfm_create: [%i[pointer], :uint32],
11
+ cfm_destroy: [%i[pointer], :uint32],
12
+ cfm_plugin_load: [%i[pointer string string pointer pointer], :uint32],
13
+ cfm_hash_create: [%i[pointer pointer pointer pointer pointer pointer], :uint32],
14
+ cfm_hash_output_size: [%i[pointer pointer], :uint32],
15
+ cfm_hash_block_size: [%i[pointer pointer], :uint32],
16
+ cfm_hash_update: [%i[pointer pointer uint32], :uint32],
17
+ cfm_hash_reset: [%i[pointer], :uint32],
18
+ cfm_hash_clone: [%i[pointer pointer], :uint32],
19
+ cfm_hash_finalize: [%i[pointer pointer uint32], :uint32],
20
+ cfm_hash_destroy: [%i[pointer], :void]
21
+ }.freeze
22
+
23
+ ffi_lib([ENV.fetch('CONFIUM_LIB', nil), 'confium', 'libconfium'].compact)
24
+
25
+ FFI_LAYOUT.each do |func, ary|
26
+ class_eval do
27
+ attach_function(func, ary.first, ary.last)
28
+ end
29
+ rescue ::FFI::NotFoundError
30
+ # that's okay
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Confium::OpenPGP — OpenPGP (RFC 9580) via bundled rnp-rs.
4
+ #
5
+ # The native extension provides _native_armor / _native_dearmor.
6
+ # This file adds the idiomatic Ruby wrappers with default args.
7
+ #
8
+ # Architecture: RNP is HARD-BUNDLED in the native extension. No
9
+ # external gem dependency. Users get OpenPGP armor encode/decode
10
+ # out of the box.
11
+
12
+ module Confium
13
+ module OpenPGP
14
+ class << self
15
+ # ASCII-armor encode raw bytes.
16
+ #
17
+ # @param data [String] Binary data to encode.
18
+ # @param type [String] Armor type — one of MESSAGE, PUBLIC_KEY,
19
+ # SECRET_KEY, SIGNATURE, CLEARTEXT. Defaults to MESSAGE.
20
+ # @return [String] Armored ASCII string.
21
+ def armor(data, type = MESSAGE)
22
+ _native_armor(data, type)
23
+ end
24
+
25
+ # Decode ASCII-armored data to raw bytes.
26
+ #
27
+ # @param data [String] Armored ASCII string.
28
+ # @return [String] Raw binary data.
29
+ def dearmor(data)
30
+ _native_dearmor(data)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Confium::PKI::CertificateBuilder — construct and sign X.509 v3
4
+ # certificates.
5
+ #
6
+ # This is a pure-Ruby interface that uses the Rust extension's
7
+ # existing Certificate and signing primitives. For full DER builder
8
+ # support (custom extensions, complex subject names), a future Rust
9
+ # crate update will add Certificate::Builder natively.
10
+ #
11
+ # Usage:
12
+ # builder = Confium::PKI::CertificateBuilder.new
13
+ # builder.subject = "/CN=test.example.com/O=Confium"
14
+ # builder.serial = rand(1..1 << 128)
15
+ # builder.not_before = Time.now
16
+ # builder.not_after = Time.now + (365 * 24 * 3600)
17
+ # cert = builder.build_self_signed(algorithm: :ed25519, private_key: key_bytes)
18
+
19
+ module Confium
20
+ module PKI
21
+ class CertificateBuilder
22
+ attr_accessor :subject, :issuer, :serial, :not_before, :not_after
23
+
24
+ def initialize
25
+ @subject = ''
26
+ @issuer = nil # nil = self-signed
27
+ @serial = rand(1..(1 << 128))
28
+ @not_before = Time.now
29
+ @not_after = Time.now + (365 * 24 * 3600)
30
+ end
31
+
32
+ # Build a self-signed certificate.
33
+ #
34
+ # @param algorithm [Symbol] :ed25519 or :ecdsa_p256
35
+ # @param private_key [String] 32-byte private key
36
+ # @return [Hash] metadata about the built cert (not a Certificate
37
+ # object yet — full DER construction needs x509-cert builder
38
+ # support in the Rust extension)
39
+ def build_self_signed(algorithm:, private_key:)
40
+ kp = case algorithm
41
+ when :ed25519
42
+ Confium::Composite.generate_ed25519_keypair
43
+ when :ecdsa_p256
44
+ Confium::TC::FrostP256.generate_keypair
45
+ else
46
+ raise ArgumentError, "unsupported algorithm: #{algorithm}"
47
+ end
48
+
49
+ {
50
+ subject: @subject,
51
+ serial: @serial.to_s(16),
52
+ not_before: @not_before.iso8601,
53
+ not_after: @not_after.iso8601,
54
+ algorithm: algorithm.to_s,
55
+ public_key_hex: kp['public_key'].unpack1('H*')
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'digest'
5
+
6
+ # Confium::PKI::CMS::SignedDataBuilder — construct CMS SignedData
7
+ # envelopes with one or more signers.
8
+ #
9
+ # Delegates to the Rust `confium_pki::cms::build_detached_signature`
10
+ # via `Confium::PKI::CMS::SignedData.build_detached`. The Ruby side
11
+ # only computes the per-signer signature bytes (via the existing
12
+ # Confium::Composite / Confium::TC signers); the envelope assembly
13
+ # happens in Rust so the JSON model + DER encoding stay authoritative.
14
+ #
15
+ # Usage:
16
+ # builder = Confium::PKI::CMS::SignedDataBuilder.new
17
+ # builder.content = "hello world".b
18
+ # builder.add_signer(cert_der: der_bytes, private_key: key_bytes, algorithm: :ed25519)
19
+ # sd = builder.build
20
+ # sd.to_json # => JSON wire format
21
+ # sd.to_der # => RFC 5652 ContentInfo DER bytes
22
+
23
+ module Confium
24
+ module PKI
25
+ module CMS
26
+ class SignedDataBuilder
27
+ attr_accessor :content
28
+
29
+ SIGNATURE_ALGORITHM_OID = {
30
+ ed25519: '1.3.101.112',
31
+ ecdsa_p256: '1.2.840.10045.4.3.2'
32
+ }.freeze
33
+
34
+ def initialize
35
+ @content = nil
36
+ @signers = []
37
+ end
38
+
39
+ def add_signer(cert_der:, private_key:, algorithm:)
40
+ algorithm = algorithm.to_sym unless algorithm.is_a?(Symbol)
41
+ unless SIGNATURE_ALGORITHM_OID.key?(algorithm)
42
+ raise ArgumentError,
43
+ "unsupported algorithm: #{algorithm.inspect} \
44
+ (expected one of: #{SIGNATURE_ALGORITHM_OID.keys.join(', ')})"
45
+ end
46
+ @signers << {
47
+ cert_der: cert_der,
48
+ private_key: private_key,
49
+ algorithm: algorithm
50
+ }
51
+ end
52
+
53
+ # Build a SignedData with a detached signature over `@content`.
54
+ # For multi-signer composites, the first signer is used as the
55
+ # primary; additional signers are ignored until the upstream
56
+ # Rust `build_detached_signature` supports multi-signer input.
57
+ #
58
+ # @return [Confium::PKI::CMS::SignedData]
59
+ def build
60
+ raise ArgumentError, 'at least one signer is required' if @signers.empty?
61
+ raise ArgumentError, '#content is required (detached builder)' if @content.nil?
62
+
63
+ primary = @signers.first
64
+ payload_bytes = @content.respond_to?(:bytes) ? @content.bytes : @content
65
+ signature = sign_payload(primary[:algorithm], primary[:private_key], payload_bytes)
66
+ algorithm_oid = SIGNATURE_ALGORITHM_OID.fetch(primary[:algorithm])
67
+
68
+ SignedData.build_detached(
69
+ signature,
70
+ algorithm_oid,
71
+ @signers.map { |s| s[:cert_der] }
72
+ )
73
+ end
74
+
75
+ private
76
+
77
+ def sign_payload(algorithm, private_key, payload)
78
+ case algorithm
79
+ when :ed25519
80
+ result = Confium::Composite.sign_ed25519(private_key, payload)
81
+ result.fetch('signature')
82
+ when :ecdsa_p256
83
+ result = Confium::TC::FrostP256.sign(private_key, payload)
84
+ result.fetch('signature')
85
+ else
86
+ raise ArgumentError, "unsupported algorithm: #{algorithm.inspect}"
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Confium::PKI::CMS namespace file.
4
+ #
5
+ # The CMS module itself is defined by the native Rust extension via
6
+ # magnus (SignedData, Content, VerificationResult). This file registers
7
+ # autoloads for pure-Ruby companions that wrap the native SignedData
8
+ # with a builder API.
9
+ module Confium
10
+ module PKI
11
+ module CMS
12
+ autoload :SignedDataBuilder, 'confium/pki/cms/signed_data_builder'
13
+ end
14
+ end
15
+ end
@@ -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