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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +152 -0
- data/Cargo.lock +2634 -0
- data/Cargo.toml +9 -0
- data/README.adoc +114 -14
- data/Rakefile +11 -6
- data/confium.gemspec +50 -29
- data/ext/confium_native/Cargo.toml +63 -0
- data/ext/confium_native/build.rs +72 -0
- data/ext/confium_native/extconf.rb +10 -0
- data/ext/confium_native/src/attributes.rs +88 -0
- data/ext/confium_native/src/audit.rs +169 -0
- data/ext/confium_native/src/composite.rs +302 -0
- data/ext/confium_native/src/deployment.rs +176 -0
- data/ext/confium_native/src/ers.rs +93 -0
- data/ext/confium_native/src/lib.rs +56 -0
- data/ext/confium_native/src/openpgp.rs +55 -0
- data/ext/confium_native/src/path.rs +118 -0
- data/ext/confium_native/src/pki.rs +431 -0
- data/ext/confium_native/src/tc.rs +420 -0
- data/ext/confium_native/src/transparency.rs +343 -0
- data/ext/confium_native/src/util.rs +201 -0
- data/lib/confium/audit.rb +125 -0
- data/lib/confium/cfm.rb +4 -5
- data/lib/confium/crypto.rb +50 -0
- data/lib/confium/digest.rb +11 -9
- data/lib/confium/errors/coerce.rb +47 -0
- data/lib/confium/errors/crypto_error.rb +15 -0
- data/lib/confium/errors/index_error.rb +15 -0
- data/lib/confium/errors/not_found_error.rb +15 -0
- data/lib/confium/errors/parse_error.rb +15 -0
- data/lib/confium/errors/policy_violation_error.rb +15 -0
- data/lib/confium/errors/threshold_error.rb +16 -0
- data/lib/confium/errors/unresolved_signer_error.rb +14 -0
- data/lib/confium/errors/validation_error.rb +17 -0
- data/lib/confium/errors/verification_error.rb +15 -0
- data/lib/confium/errors.rb +26 -0
- data/lib/confium/ffi.rb +23 -0
- data/lib/confium/lib.rb +18 -56
- data/lib/confium/openpgp.rb +34 -0
- data/lib/confium/pki/certificate_builder.rb +60 -0
- data/lib/confium/pki/cms/signed_data_builder.rb +92 -0
- data/lib/confium/pki/cms.rb +15 -0
- data/lib/confium/pki/cnml.rb +80 -0
- data/lib/confium/pki.rb +13 -0
- data/lib/confium/policy.rb +138 -0
- data/lib/confium/secure_bytes.rb +126 -0
- data/lib/confium/tc/coordinator.rb +68 -0
- data/lib/confium/tc/session.rb +51 -0
- data/lib/confium/tc/session_stub.rb +43 -0
- data/lib/confium/tc/share_file.rb +87 -0
- data/lib/confium/tc.rb +17 -0
- data/lib/confium/transparency/ots.rb +63 -0
- data/lib/confium/version.rb +1 -1
- data/lib/confium.rb +50 -20
- metadata +142 -25
- data/CODE_OF_CONDUCT.md +0 -84
- data/Gemfile +0 -10
- 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
|
data/lib/confium/digest.rb
CHANGED
|
@@ -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
|
-
|
|
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'
|
data/lib/confium/ffi.rb
ADDED
|
@@ -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: [
|
|
46
|
-
cfm_destroy: [
|
|
47
|
-
cfm_plugin_load: [
|
|
48
|
-
cfm_hash_create: [
|
|
49
|
-
cfm_hash_output_size: [
|
|
50
|
-
cfm_hash_block_size: [
|
|
51
|
-
cfm_hash_update: [
|
|
52
|
-
cfm_hash_reset: [
|
|
53
|
-
cfm_hash_clone: [
|
|
54
|
-
cfm_hash_finalize: [
|
|
55
|
-
cfm_hash_destroy: [
|
|
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(
|
|
23
|
+
ffi_lib([ENV.fetch('CONFIUM_LIB', nil), 'confium', 'libconfium'].compact)
|
|
59
24
|
|
|
60
25
|
FFI_LAYOUT.each do |func, ary|
|
|
61
|
-
|
|
62
|
-
|
|
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
|