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,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ # {Confium::Crypto} is the registry namespace for Confium's cryptographic
4
+ # interfaces.
5
+ #
6
+ # Mirroring the Rust plugin-interface registry (TODO #02), each Ruby
7
+ # interface module (Digest, and future Cipher, AEAD, KDF, RNG, Signature,
8
+ # KEM, ...) registers itself here on load rather than being enumerated in
9
+ # a central case statement. This keeps the registry open for extension
10
+ # (OCP) and makes the set of available interfaces a single source of
11
+ # truth.
12
+ #
13
+ # Example:
14
+ #
15
+ # module Confium
16
+ # module Digest
17
+ # Confium::Crypto.register(:hash, self)
18
+ # end
19
+ # end
20
+ #
21
+ # Confium::Crypto.lookup(:hash) # => Confium::Digest
22
+ module Confium
23
+ module Crypto
24
+ @interfaces = {}
25
+
26
+ class << self
27
+ # Register an interface +klass+ under the symbolic +name+. Adding a
28
+ # new interface is a one-line registration; no central switch needs
29
+ # editing.
30
+ def register(name, klass)
31
+ @interfaces[name] = klass
32
+ klass
33
+ end
34
+
35
+ # Resolve a registered interface by +name+. Raises ArgumentError if
36
+ # nothing has been registered under that name.
37
+ def lookup(name)
38
+ @interfaces.fetch(name) do
39
+ raise ArgumentError, "unknown interface #{name.inspect}"
40
+ end
41
+ end
42
+
43
+ # Enumerate the names of every registered interface. Primarily for
44
+ # introspection and tooling.
45
+ def interfaces
46
+ @interfaces.keys.dup
47
+ end
48
+ end
49
+ end
50
+ end
@@ -1,26 +1,28 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ffi'
2
4
  require 'digest'
3
5
 
4
6
  module Confium
5
7
  class Digest < ::Digest::Class
6
- attr_reader :name
7
- attr_reader :ptr
8
+ attr_reader :name, :ptr
8
9
 
9
10
  def initialize(cfm, name)
10
11
  @name = name
11
- pptr = FFI::MemoryPointer.new(:pointer)
12
+ pptr = ::FFI::MemoryPointer.new(:pointer)
12
13
  Confium.call_ffi(:cfm_hash_create, cfm.ptr, pptr, name, nil, nil, nil)
13
14
  ptr = pptr.read_pointer
14
15
  raise if ptr.null?
15
- @ptr = FFI::AutoPointer.new(ptr, self.class.method(:destroy))
16
+
17
+ @ptr = ::FFI::AutoPointer.new(ptr, self.class.method(:destroy))
16
18
  end
17
19
 
18
20
  def initialize_copy(source)
19
21
  @name = source.name
20
- pptr = FFI::MemoryPointer.new(:pointer)
22
+ pptr = ::FFI::MemoryPointer.new(:pointer)
21
23
  Confium.call_ffi(:cfm_hash_clone, source.ptr, pptr)
22
24
  ptr = pptr.read_pointer
23
- @ptr = FFI::AutoPointer.new(ptr, self.class.method(:destroy))
25
+ @ptr = ::FFI::AutoPointer.new(ptr, self.class.method(:destroy))
24
26
  end
25
27
 
26
28
  def self.destroy(ptr)
@@ -28,13 +30,13 @@ module Confium
28
30
  end
29
31
 
30
32
  def block_length
31
- plength = FFI::MemoryPointer.new(:uint32)
33
+ plength = ::FFI::MemoryPointer.new(:uint32)
32
34
  Confium.call_ffi(:cfm_hash_block_size, @ptr, plength)
33
35
  plength.read(:uint32)
34
36
  end
35
37
 
36
38
  def digest_length
37
- plength = FFI::MemoryPointer.new(:uint32)
39
+ plength = ::FFI::MemoryPointer.new(:uint32)
38
40
  Confium.call_ffi(:cfm_hash_output_size, @ptr, plength)
39
41
  plength.read(:uint32)
40
42
  end
@@ -50,7 +52,7 @@ module Confium
50
52
  end
51
53
 
52
54
  def finish
53
- buf = FFI::MemoryPointer.new(:uint8, digest_length)
55
+ buf = ::FFI::MemoryPointer.new(:uint8, digest_length)
54
56
  Confium.call_ffi(:cfm_hash_finalize, @ptr, buf, buf.size)
55
57
  buf.read_bytes(buf.size)
56
58
  end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Shared coercion helper for the typed-error hierarchy.
4
+ #
5
+ # Every `Confium::*Error` subclass accepts the same three calling
6
+ # conventions (keyword form, positional-Hash form from the native
7
+ # extension, hash-only form). Before this module existed, each class
8
+ # duplicated the same 8-line coercion preamble. Centralizing it here
9
+ # keeps the per-class initializer focused on its own field extraction.
10
+ #
11
+ # Usage in a subclass:
12
+ #
13
+ # def initialize(message = nil, details_hash = nil, **kwargs)
14
+ # message, kwargs = Coerce.args(message, details_hash, kwargs)
15
+ # @have_count = kwargs.delete(:have_count)
16
+ # @need_count = kwargs.delete(:need_count)
17
+ # super(message, details: { have_count: @have_count, need_count: @need_count, **kwargs })
18
+ # end
19
+ module Confium
20
+ module Errors
21
+ module Coerce
22
+ module_function
23
+
24
+ # Normalize the (message, details_hash, kwargs) triple that every
25
+ # typed-error initializer receives. Returns `[message, kwargs]`
26
+ # where:
27
+ #
28
+ # - `message` is `nil` or a String (never a Hash)
29
+ # - `kwargs` is a Hash with symbol keys, containing every key
30
+ # from the original `details_hash` and `**kwargs`, plus
31
+ # anything that was tucked inside `message` (when the caller
32
+ # passed a Hash as the first arg).
33
+ #
34
+ # The subclass initializer then `kwargs.delete(:specific_field)`
35
+ # to pull its own fields out, and the leftover `**kwargs` flows
36
+ # to `super` as `details:`.
37
+ def args(message, details_hash, kwargs)
38
+ if message.is_a?(Hash)
39
+ kwargs = message.transform_keys(&:to_sym).merge(kwargs)
40
+ message = kwargs.delete(:message)
41
+ end
42
+ kwargs = details_hash.transform_keys(&:to_sym).merge(kwargs) if details_hash.is_a?(Hash)
43
+ [message, kwargs]
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a primitive-level crypto operation fails (invalid scalar,
4
+ # bad key derivation).
5
+ module Confium
6
+ class CryptoError < Confium::Error
7
+ attr_reader :primitive
8
+
9
+ def initialize(message = nil, details_hash = nil, **kwargs)
10
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
11
+ @primitive = kwargs.delete(:primitive)
12
+ super(message, details: { primitive: @primitive, **kwargs })
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when an out-of-range index is supplied.
4
+ module Confium
5
+ class IndexError < Confium::Error
6
+ attr_reader :index, :valid_range
7
+
8
+ def initialize(message = nil, details_hash = nil, **kwargs)
9
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
10
+ @index = kwargs.delete(:index)
11
+ @valid_range = kwargs.delete(:valid_range)
12
+ super(message, details: { index: @index, valid_range: @valid_range, **kwargs })
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a referenced slot/cert/share is not present.
4
+ module Confium
5
+ class NotFoundError < Confium::Error
6
+ attr_reader :kind, :identifier
7
+
8
+ def initialize(message = nil, details_hash = nil, **kwargs)
9
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
10
+ @kind = kwargs.delete(:kind)
11
+ @identifier = kwargs.delete(:identifier)
12
+ super(message, details: { kind: @kind, identifier: @identifier, **kwargs })
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when input cannot be parsed (bad JSON, malformed PEM, etc.).
4
+ module Confium
5
+ class ParseError < Confium::Error
6
+ attr_reader :format, :offset
7
+
8
+ def initialize(message = nil, details_hash = nil, **kwargs)
9
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
10
+ @format = kwargs.delete(:format)
11
+ @offset = kwargs.delete(:offset)
12
+ super(message, details: { format: @format, offset: @offset, **kwargs })
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a FIPS / jurisdictional policy is violated.
4
+ module Confium
5
+ class PolicyViolationError < Confium::Error
6
+ attr_reader :policy, :violation
7
+
8
+ def initialize(message = nil, details_hash = nil, **kwargs)
9
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
10
+ @policy = kwargs.delete(:policy)
11
+ @violation = kwargs.delete(:violation)
12
+ super(message, details: { policy: @policy, violation: @violation, **kwargs })
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a Shamir/threshold operation fails (insufficient shares,
4
+ # duplicate coordinates, etc.).
5
+ module Confium
6
+ class ThresholdError < Confium::Error
7
+ attr_reader :have_count, :need_count
8
+
9
+ def initialize(message = nil, details_hash = nil, **kwargs)
10
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
11
+ @have_count = kwargs.delete(:have_count)
12
+ @need_count = kwargs.delete(:need_count)
13
+ super(message, details: { have_count: @have_count, need_count: @need_count, **kwargs })
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a CMS signer_info cannot be resolved to a certificate.
4
+ module Confium
5
+ class UnresolvedSignerError < Confium::Error
6
+ attr_reader :signer_index
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
+ super(message, details: { signer_index: @signer_index, **kwargs })
12
+ end
13
+ end
14
+ end
@@ -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
data/lib/confium/lib.rb CHANGED
@@ -1,71 +1,33 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ffi'
2
4
 
3
5
  module Confium
4
6
  module Lib
5
- # FFI load pattern taken from: ffi-geos
6
- # https://github.com/dark-panda/ffi-geos/blob/master/lib/ffi-geos.rb
7
- extend FFI::Library
8
-
9
- def self.search_paths
10
- @search_paths ||= \
11
- if ENV['CONFIUM_LIBRARY_PATH']
12
- [ENV['CONFIUM_LIBRARY_PATH']]
13
- elsif FFI::Platform::IS_WINDOWS
14
- ENV['PATH'].split(File::PATH_SEPARATOR)
15
- else
16
- [
17
- '/usr/local/{lib64,lib}',
18
- '/opt/local/{lib64,lib}',
19
- '/usr/{lib64,lib}',
20
- '/opt/homebrew/lib',
21
- '/usr/lib/{x86_64,i386,aarch64}-linux-gnu'
22
- ]
23
- end
24
- end
25
-
26
- def self.find_lib(lib)
27
- if ENV['CONFIUM_LIBRARY_PATH'] && File.file?(ENV['CONFIUM_LIBRARY_PATH'])
28
- ENV['CONFIUM_LIBRARY_PATH']
29
- else
30
- Dir.glob(search_paths.map do |path|
31
- File.expand_path(File.join(path, "#{lib}.#{FFI::Platform::LIBSUFFIX}{,.?}"))
32
- end).first
33
- end
34
- end
35
-
36
- def self.confium_library_path
37
- @confium_library_path ||= \
38
- # On MingW the libraries have version numbers
39
- find_lib('{lib,}confium{,-?}')
40
- end
41
-
42
7
  extend ::FFI::Library
43
8
 
44
9
  FFI_LAYOUT = {
45
- cfm_create: [ %i[pointer], :uint32 ],
46
- cfm_destroy: [ %i[pointer], :uint32 ],
47
- cfm_plugin_load: [ %i[pointer string string pointer pointer], :uint32 ],
48
- cfm_hash_create: [ %i[pointer pointer pointer pointer pointer pointer], :uint32 ],
49
- cfm_hash_output_size: [ %i[pointer pointer], :uint32 ],
50
- cfm_hash_block_size: [ %i[pointer pointer], :uint32 ],
51
- cfm_hash_update: [ %i[pointer pointer uint32], :uint32 ],
52
- cfm_hash_reset: [ %i[pointer], :uint32 ],
53
- cfm_hash_clone: [ %i[pointer pointer], :uint32 ],
54
- cfm_hash_finalize: [ %i[pointer pointer uint32], :uint32 ],
55
- cfm_hash_destroy: [ %i[pointer], :void ],
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]
56
21
  }.freeze
57
22
 
58
- ffi_lib(confium_library_path)
23
+ ffi_lib([ENV.fetch('CONFIUM_LIB', nil), 'confium', 'libconfium'].compact)
59
24
 
60
25
  FFI_LAYOUT.each do |func, ary|
61
- begin
62
- class_eval do
63
- attach_function(func, ary.first, ary.last)
64
- end
65
- rescue FFI::NotFoundError
66
- # that's okay
26
+ class_eval do
27
+ attach_function(func, ary.first, ary.last)
67
28
  end
29
+ rescue ::FFI::NotFoundError
30
+ # that's okay
68
31
  end
69
-
70
32
  end
71
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