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,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
@@ -8,19 +8,19 @@ module Confium
8
8
 
9
9
  def initialize(cfm, name)
10
10
  @name = name
11
- pptr = FFI::MemoryPointer.new(:pointer)
11
+ pptr = ::FFI::MemoryPointer.new(:pointer)
12
12
  Confium.call_ffi(:cfm_hash_create, cfm.ptr, pptr, name, nil, nil, nil)
13
13
  ptr = pptr.read_pointer
14
14
  raise if ptr.null?
15
- @ptr = FFI::AutoPointer.new(ptr, self.class.method(:destroy))
15
+ @ptr = ::FFI::AutoPointer.new(ptr, self.class.method(:destroy))
16
16
  end
17
17
 
18
18
  def initialize_copy(source)
19
19
  @name = source.name
20
- pptr = FFI::MemoryPointer.new(:pointer)
20
+ pptr = ::FFI::MemoryPointer.new(:pointer)
21
21
  Confium.call_ffi(:cfm_hash_clone, source.ptr, pptr)
22
22
  ptr = pptr.read_pointer
23
- @ptr = FFI::AutoPointer.new(ptr, self.class.method(:destroy))
23
+ @ptr = ::FFI::AutoPointer.new(ptr, self.class.method(:destroy))
24
24
  end
25
25
 
26
26
  def self.destroy(ptr)
@@ -28,13 +28,13 @@ module Confium
28
28
  end
29
29
 
30
30
  def block_length
31
- plength = FFI::MemoryPointer.new(:uint32)
31
+ plength = ::FFI::MemoryPointer.new(:uint32)
32
32
  Confium.call_ffi(:cfm_hash_block_size, @ptr, plength)
33
33
  plength.read(:uint32)
34
34
  end
35
35
 
36
36
  def digest_length
37
- plength = FFI::MemoryPointer.new(:uint32)
37
+ plength = ::FFI::MemoryPointer.new(:uint32)
38
38
  Confium.call_ffi(:cfm_hash_output_size, @ptr, plength)
39
39
  plength.read(:uint32)
40
40
  end
@@ -50,7 +50,7 @@ module Confium
50
50
  end
51
51
 
52
52
  def finish
53
- buf = FFI::MemoryPointer.new(:uint8, digest_length)
53
+ buf = ::FFI::MemoryPointer.new(:uint8, digest_length)
54
54
  Confium.call_ffi(:cfm_hash_finalize, @ptr, buf, buf.size)
55
55
  buf.read_bytes(buf.size)
56
56
  end
@@ -0,0 +1,49 @@
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
+ if details_hash.is_a?(Hash)
43
+ kwargs = details_hash.transform_keys(&:to_sym).merge(kwargs)
44
+ end
45
+ [message, kwargs]
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a primitive-level crypto operation fails (invalid scalar,
4
+ # bad key derivation).
5
+ class Confium::CryptoError < Confium::Error
6
+ attr_reader :primitive
7
+
8
+ def initialize(message = nil, details_hash = nil, **kwargs)
9
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
10
+ @primitive = kwargs.delete(:primitive)
11
+ super(message, details: { primitive: @primitive, **kwargs })
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when an out-of-range index is supplied.
4
+ class Confium::IndexError < Confium::Error
5
+ attr_reader :index, :valid_range
6
+
7
+ def initialize(message = nil, details_hash = nil, **kwargs)
8
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
9
+ @index = kwargs.delete(:index)
10
+ @valid_range = kwargs.delete(:valid_range)
11
+ super(message, details: { index: @index, valid_range: @valid_range, **kwargs })
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a referenced slot/cert/share is not present.
4
+ class Confium::NotFoundError < Confium::Error
5
+ attr_reader :kind, :identifier
6
+
7
+ def initialize(message = nil, details_hash = nil, **kwargs)
8
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
9
+ @kind = kwargs.delete(:kind)
10
+ @identifier = kwargs.delete(:identifier)
11
+ super(message, details: { kind: @kind, identifier: @identifier, **kwargs })
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when input cannot be parsed (bad JSON, malformed PEM, etc.).
4
+ class Confium::ParseError < Confium::Error
5
+ attr_reader :format, :offset
6
+
7
+ def initialize(message = nil, details_hash = nil, **kwargs)
8
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
9
+ @format = kwargs.delete(:format)
10
+ @offset = kwargs.delete(:offset)
11
+ super(message, details: { format: @format, offset: @offset, **kwargs })
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a FIPS / jurisdictional policy is violated.
4
+ class Confium::PolicyViolationError < Confium::Error
5
+ attr_reader :policy, :violation
6
+
7
+ def initialize(message = nil, details_hash = nil, **kwargs)
8
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
9
+ @policy = kwargs.delete(:policy)
10
+ @violation = kwargs.delete(:violation)
11
+ super(message, details: { policy: @policy, violation: @violation, **kwargs })
12
+ end
13
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a Shamir/threshold operation fails (insufficient shares,
4
+ # duplicate coordinates, etc.).
5
+ class Confium::ThresholdError < Confium::Error
6
+ attr_reader :have_count, :need_count
7
+
8
+ def initialize(message = nil, details_hash = nil, **kwargs)
9
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
10
+ @have_count = kwargs.delete(:have_count)
11
+ @need_count = kwargs.delete(:need_count)
12
+ super(message, details: { have_count: @have_count, need_count: @need_count, **kwargs })
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a CMS signer_info cannot be resolved to a certificate.
4
+ class Confium::UnresolvedSignerError < Confium::Error
5
+ attr_reader :signer_index
6
+
7
+ def initialize(message = nil, details_hash = nil, **kwargs)
8
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
9
+ @signer_index = kwargs.delete(:signer_index)
10
+ super(message, details: { signer_index: @signer_index, **kwargs })
11
+ end
12
+ end
@@ -0,0 +1,15 @@
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
+ class Confium::ValidationError < Confium::Error
6
+ attr_reader :param, :expected, :actual
7
+
8
+ def initialize(message = nil, details_hash = nil, **kwargs)
9
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
10
+ @param = kwargs.delete(:param)
11
+ @expected = kwargs.delete(:expected)
12
+ @actual = kwargs.delete(:actual)
13
+ super(message, details: { param: @param, expected: @expected, actual: @actual, **kwargs })
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Raised when a signature / hash / proof fails to verify.
4
+ class Confium::VerificationError < Confium::Error
5
+ attr_reader :signer_index, :algorithm
6
+
7
+ def initialize(message = nil, details_hash = nil, **kwargs)
8
+ message, kwargs = Confium::Errors::Coerce.args(message, details_hash, kwargs)
9
+ @signer_index = kwargs.delete(:signer_index)
10
+ @algorithm = kwargs.delete(:algorithm)
11
+ super(message, details: { signer_index: @signer_index, algorithm: @algorithm, **kwargs })
12
+ end
13
+ 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
@@ -2,43 +2,6 @@ require 'ffi'
2
2
 
3
3
  module Confium
4
4
  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
5
  extend ::FFI::Library
43
6
 
44
7
  FFI_LAYOUT = {
@@ -55,14 +18,14 @@ module Confium
55
18
  cfm_hash_destroy: [ %i[pointer], :void ],
56
19
  }.freeze
57
20
 
58
- ffi_lib(confium_library_path)
21
+ ffi_lib([ENV["CONFIUM_LIB"], "confium", "libconfium"].compact)
59
22
 
60
23
  FFI_LAYOUT.each do |func, ary|
61
24
  begin
62
25
  class_eval do
63
26
  attach_function(func, ary.first, ary.last)
64
27
  end
65
- rescue FFI::NotFoundError
28
+ rescue ::FFI::NotFoundError
66
29
  # that's okay
67
30
  end
68
31
  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