confium 0.3.2-aarch64-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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +183 -0
- data/Cargo.lock +2635 -0
- data/Cargo.toml +9 -0
- data/LICENSE.txt +21 -0
- data/README.adoc +139 -0
- data/Rakefile +30 -0
- data/confium.gemspec +56 -0
- data/lib/confium/audit.rb +125 -0
- data/lib/confium/cfm.rb +23 -0
- data/lib/confium/crypto.rb +50 -0
- data/lib/confium/digest.rb +62 -0
- 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 +33 -0
- 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 +5 -0
- data/lib/confium.rb +71 -0
- data/lib/confium_native/3.1/confium_native.so +0 -0
- data/lib/confium_native/3.2/confium_native.so +0 -0
- data/lib/confium_native/3.3/confium_native.so +0 -0
- 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'
|
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
ADDED
|
@@ -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
|
data/lib/confium/pki.rb
ADDED
|
@@ -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
|