legion-crypt 1.4.1 → 1.4.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 13bcc3fe80b97d57f54b1e5449f0319865ee5b05c5402ce9a5f3b3a30f89ad88
4
- data.tar.gz: 1ba4f54c017ec83868defedd3fe4d7a3659b5d84f3afa030722dce9d9bb1211c
3
+ metadata.gz: 755c4278bc3e6f3ba845f5d46ba61b753a19b9a4fc0212f9da9827e6fefe3ac2
4
+ data.tar.gz: '04585ab20ddb62568949ca90c33405b61bb477cd2750503944acadcf665925b9'
5
5
  SHA512:
6
- metadata.gz: 6f120ed5f85a929fe22e750e482253550000888afbc6633c989df680c7acdc93673303014d911871642a48f66ab05e6899ab773319b1a195c64736eb052fc204
7
- data.tar.gz: 8a4d154360dca522f05982d986eb28d25212038c7ca37e94253a4a1a89b03a9a51a15e46e2dedc77f382b5b7f84fcca3c8924332c9dae230858a3c154e3e8b15
6
+ metadata.gz: 53c5f43b6ab1ca2f0dad978a3cee6b1d3314b8aa57ffae077641d9882756f3a331b21258f3b45a9789639095facdff27baec911e6cbd066c7d2c2fc1058237bd
7
+ data.tar.gz: f3b542c8ca98c768ddfae7763da33ddc3638fd46dbb174abf037cd0baca348fa6127c4ad1d3efa46c86a943b2cd3d2c6f2df5d8d179e0a64a5b94d5b3ec4f35c
data/CHANGELOG.md CHANGED
@@ -1,6 +1,21 @@
1
1
  # Legion::Crypt
2
2
 
3
- ## [Unreleased]
3
+ ## [1.4.3] - 2026-03-17
4
+
5
+ ### Added
6
+ - `Crypt::TLS`: mTLS configuration for RabbitMQ (Bunny) and PostgreSQL (Sequel) connections
7
+ - `TLS.ssl_context` builds OpenSSL::SSL::SSLContext with TLS 1.2+ and VERIFY_PEER
8
+ - `TLS.bunny_options` and `TLS.sequel_options` generate adapter-specific TLS option hashes
9
+ - Configurable cert/key/ca paths via settings with sensible defaults
10
+
11
+ ## [1.4.2] - 2026-03-16
12
+
13
+ ### Added
14
+ - `Legion::Crypt::Ed25519`: Ed25519 key generation, signing, verification, Vault key storage
15
+ - `Legion::Crypt::PartitionKeys`: HKDF-based per-tenant key derivation with AES-256-GCM encrypt/decrypt
16
+ - `Legion::Crypt::Erasure`: cryptographic erasure via Vault master key deletion with event emission
17
+ - `Legion::Crypt::Attestation`: signed identity claims with Ed25519 signatures and freshness checking
18
+ - Dependency: `ed25519` gem ~> 1.3
4
19
 
5
20
  ## [1.4.1] - 2026-03-16
6
21
 
data/CLAUDE.md CHANGED
@@ -51,6 +51,10 @@ Legion::Crypt (singleton module)
51
51
  │ └── .worker_login # Issue a Legion JWT and authenticate to Vault in one step
52
52
 
53
53
  ├── VaultRenewer # Background Vault token renewal thread
54
+ ├── Ed25519 # Ed25519 key generation, signing, verification, Vault storage
55
+ ├── PartitionKeys # HKDF per-tenant key derivation, AES-256-GCM encrypt/decrypt
56
+ ├── Erasure # Cryptographic erasure via Vault master key deletion
57
+ ├── Attestation # Signed identity claims with Ed25519, freshness checking
54
58
  ├── LeaseManager # Dynamic Vault lease lifecycle: fetch, cache, renew, rotate, push-back
55
59
  ├── MockVault # In-memory Vault mock for local development mode
56
60
  ├── Settings # Default crypt config
@@ -91,6 +95,7 @@ Legion::Crypt (singleton module)
91
95
 
92
96
  | Gem | Purpose |
93
97
  |-----|---------|
98
+ | `ed25519` (~> 1.3) | Ed25519 key operations (pure Ruby) |
94
99
  | `jwt` (>= 2.7) | JSON Web Token encoding/decoding |
95
100
  | `vault` (>= 0.17) | HashiCorp Vault Ruby client |
96
101
 
@@ -110,6 +115,10 @@ Dev dependencies: `legion-logging`, `legion-settings`
110
115
  | `lib/legion/crypt/vault_renewer.rb` | Background Vault token renewal |
111
116
  | `lib/legion/crypt/lease_manager.rb` | Dynamic Vault lease lifecycle management |
112
117
  | `lib/legion/crypt/settings.rb` | Default configuration |
118
+ | `lib/legion/crypt/ed25519.rb` | Ed25519 key generation, signing, verification, Vault storage |
119
+ | `lib/legion/crypt/partition_keys.rb` | HKDF per-tenant key derivation with AES-256-GCM |
120
+ | `lib/legion/crypt/erasure.rb` | Cryptographic erasure via Vault master key deletion |
121
+ | `lib/legion/crypt/attestation.rb` | Signed identity claims with Ed25519 signatures |
113
122
  | `lib/legion/crypt/version.rb` | VERSION constant |
114
123
 
115
124
  ## Role in LegionIO
data/legion-crypt.gemspec CHANGED
@@ -25,6 +25,7 @@ Gem::Specification.new do |spec|
25
25
  'rubygems_mfa_required' => 'true'
26
26
  }
27
27
 
28
+ spec.add_dependency 'ed25519', '~> 1.3'
28
29
  spec.add_dependency 'jwt', '>= 2.7'
29
30
  spec.add_dependency 'vault', '>= 0.17'
30
31
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Legion
6
+ module Crypt
7
+ module Attestation
8
+ class << self
9
+ def create(agent_id:, capabilities:, state:, private_key:)
10
+ claim = {
11
+ agent_id: agent_id,
12
+ capabilities: Array(capabilities),
13
+ state: state.to_s,
14
+ timestamp: Time.now.utc.iso8601,
15
+ nonce: SecureRandom.hex(16)
16
+ }
17
+
18
+ payload = Legion::JSON.dump(claim)
19
+ signature = Legion::Crypt::Ed25519.sign(payload, private_key)
20
+
21
+ { claim: claim, signature: signature.unpack1('H*'), payload: payload }
22
+ end
23
+
24
+ def verify(claim_hash:, signature_hex:, public_key:)
25
+ payload = Legion::JSON.dump(claim_hash)
26
+ signature = [signature_hex].pack('H*')
27
+ Legion::Crypt::Ed25519.verify(payload, signature, public_key)
28
+ end
29
+
30
+ def fresh?(claim_hash, max_age_seconds: 300)
31
+ timestamp = Time.parse(claim_hash[:timestamp])
32
+ Time.now.utc - timestamp < max_age_seconds
33
+ rescue StandardError
34
+ false
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ed25519'
4
+
5
+ module Legion
6
+ module Crypt
7
+ module Ed25519
8
+ class << self
9
+ def generate_keypair
10
+ signing_key = ::Ed25519::SigningKey.generate
11
+ {
12
+ private_key: signing_key.to_bytes,
13
+ public_key: signing_key.verify_key.to_bytes,
14
+ public_key_hex: signing_key.verify_key.to_bytes.unpack1('H*')
15
+ }
16
+ end
17
+
18
+ def sign(message, private_key_bytes)
19
+ signing_key = ::Ed25519::SigningKey.new(private_key_bytes)
20
+ signing_key.sign(message)
21
+ end
22
+
23
+ def verify(message, signature, public_key_bytes)
24
+ verify_key = ::Ed25519::VerifyKey.new(public_key_bytes)
25
+ verify_key.verify(signature, message)
26
+ true
27
+ rescue ::Ed25519::VerifyError
28
+ false
29
+ end
30
+
31
+ def store_keypair(agent_id:, keypair: nil)
32
+ keypair ||= generate_keypair
33
+ vault_path = "#{key_prefix}/#{agent_id}"
34
+ if defined?(Legion::Crypt::Vault)
35
+ Legion::Crypt::Vault.write(vault_path, {
36
+ private_key: keypair[:private_key].unpack1('H*'),
37
+ public_key: keypair[:public_key_hex]
38
+ })
39
+ end
40
+ keypair
41
+ end
42
+
43
+ def load_private_key(agent_id:)
44
+ vault_path = "#{key_prefix}/#{agent_id}"
45
+ data = Legion::Crypt::Vault.read(vault_path)
46
+ [data[:private_key]].pack('H*') if data&.dig(:private_key)
47
+ rescue StandardError
48
+ nil
49
+ end
50
+
51
+ private
52
+
53
+ def key_prefix
54
+ begin
55
+ Legion::Settings[:crypt][:ed25519][:vault_key_prefix]
56
+ rescue StandardError
57
+ nil
58
+ end || 'secret/data/legion/keys'
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Crypt
5
+ module Erasure
6
+ class << self
7
+ def erase_tenant(tenant_id:)
8
+ key_path = "#{tenant_prefix}/#{tenant_id}/master_key"
9
+
10
+ delete_vault_key(key_path) if defined?(Legion::Crypt::Vault)
11
+ Legion::Events.emit('crypt.tenant_erased', { tenant_id: tenant_id, erased_at: Time.now.utc }) if defined?(Legion::Events)
12
+ Legion::Logging.warn "[crypt] Tenant #{tenant_id} cryptographically erased" if defined?(Legion::Logging)
13
+
14
+ { erased: true, tenant_id: tenant_id, path: key_path }
15
+ rescue StandardError => e
16
+ { erased: false, tenant_id: tenant_id, error: e.message }
17
+ end
18
+
19
+ def verify_erasure(tenant_id:)
20
+ key_path = "#{tenant_prefix}/#{tenant_id}/master_key"
21
+ data = Legion::Crypt::Vault.read(key_path)
22
+ { erased: data.nil?, tenant_id: tenant_id }
23
+ rescue StandardError
24
+ { erased: true, tenant_id: tenant_id }
25
+ end
26
+
27
+ private
28
+
29
+ def delete_vault_key(path)
30
+ ::Vault.logical.delete(path)
31
+ end
32
+
33
+ def tenant_prefix
34
+ begin
35
+ Legion::Settings[:crypt][:partition_keys][:vault_tenant_prefix]
36
+ rescue StandardError
37
+ nil
38
+ end || 'secret/data/legion/tenants'
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Legion
6
+ module Crypt
7
+ module PartitionKeys
8
+ class << self
9
+ def derive_key(master_key:, tenant_id:, context: nil)
10
+ context ||= begin
11
+ Legion::Settings[:crypt][:partition_keys][:derivation_context]
12
+ rescue StandardError
13
+ nil
14
+ end || 'legion-partition'
15
+ salt = OpenSSL::Digest::SHA256.digest(tenant_id.to_s)
16
+ OpenSSL::KDF.hkdf(master_key, salt: salt, info: context, length: 32, hash: 'SHA256')
17
+ end
18
+
19
+ def encrypt_for_tenant(plaintext:, tenant_id:, master_key:)
20
+ key = derive_key(master_key: master_key, tenant_id: tenant_id)
21
+ cipher = OpenSSL::Cipher.new('aes-256-gcm')
22
+ cipher.encrypt
23
+ cipher.key = key
24
+ iv = cipher.random_iv
25
+ ciphertext = cipher.update(plaintext) + cipher.final
26
+ auth_tag = cipher.auth_tag
27
+
28
+ { ciphertext: ciphertext, iv: iv, auth_tag: auth_tag }
29
+ end
30
+
31
+ def decrypt_for_tenant(ciphertext:, init_vector:, auth_tag:, tenant_id:, master_key:)
32
+ key = derive_key(master_key: master_key, tenant_id: tenant_id)
33
+ decipher = OpenSSL::Cipher.new('aes-256-gcm')
34
+ decipher.decrypt
35
+ decipher.key = key
36
+ decipher.iv = init_vector
37
+ decipher.auth_tag = auth_tag
38
+ decipher.update(ciphertext) + decipher.final
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ module Legion
6
+ module Crypt
7
+ module TLS
8
+ DEFAULT_CERT_DIR = '/etc/legion/tls'
9
+
10
+ class << self
11
+ def enabled?
12
+ settings_dig(:enabled) == true
13
+ end
14
+
15
+ def ssl_context(role: :client) # rubocop:disable Lint/UnusedMethodArgument
16
+ ctx = OpenSSL::SSL::SSLContext.new
17
+ ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION
18
+ ctx.verify_mode = OpenSSL::SSL::VERIFY_PEER
19
+
20
+ ctx.cert = OpenSSL::X509::Certificate.new(File.read(cert_path)) if cert_path && File.exist?(cert_path)
21
+ ctx.key = OpenSSL::PKey.read(File.read(key_path)) if key_path && File.exist?(key_path)
22
+ ctx.ca_file = ca_path if ca_path && File.exist?(ca_path)
23
+
24
+ ctx
25
+ end
26
+
27
+ def bunny_options
28
+ return {} unless enabled?
29
+
30
+ {
31
+ tls: true,
32
+ tls_cert: cert_path,
33
+ tls_key: key_path,
34
+ tls_ca_certificates: [ca_path].compact,
35
+ verify_peer: true
36
+ }
37
+ end
38
+
39
+ def sequel_options
40
+ return {} unless enabled?
41
+
42
+ {
43
+ sslmode: 'verify-full',
44
+ sslcert: cert_path,
45
+ sslkey: key_path,
46
+ sslrootcert: ca_path
47
+ }
48
+ end
49
+
50
+ def cert_path
51
+ settings_dig(:cert_path) || File.join(DEFAULT_CERT_DIR, 'legion.crt')
52
+ end
53
+
54
+ def key_path
55
+ settings_dig(:key_path) || File.join(DEFAULT_CERT_DIR, 'legion.key')
56
+ end
57
+
58
+ def ca_path
59
+ settings_dig(:ca_path) || File.join(DEFAULT_CERT_DIR, 'ca-bundle.crt')
60
+ end
61
+
62
+ private
63
+
64
+ def settings_dig(*keys)
65
+ return nil unless defined?(Legion::Settings)
66
+
67
+ result = Legion::Settings[:crypt]
68
+ [:tls, *keys].each do |key|
69
+ return nil unless result.is_a?(Hash)
70
+
71
+ result = result[key]
72
+ end
73
+ result
74
+ rescue StandardError
75
+ nil
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Crypt
5
- VERSION = '1.4.1'
5
+ VERSION = '1.4.3'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-crypt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.4.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ed25519
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '1.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '1.3'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: jwt
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -57,13 +71,18 @@ files:
57
71
  - README.md
58
72
  - legion-crypt.gemspec
59
73
  - lib/legion/crypt.rb
74
+ - lib/legion/crypt/attestation.rb
60
75
  - lib/legion/crypt/cipher.rb
61
76
  - lib/legion/crypt/cluster_secret.rb
77
+ - lib/legion/crypt/ed25519.rb
78
+ - lib/legion/crypt/erasure.rb
62
79
  - lib/legion/crypt/jwks_client.rb
63
80
  - lib/legion/crypt/jwt.rb
64
81
  - lib/legion/crypt/lease_manager.rb
65
82
  - lib/legion/crypt/mock_vault.rb
83
+ - lib/legion/crypt/partition_keys.rb
66
84
  - lib/legion/crypt/settings.rb
85
+ - lib/legion/crypt/tls.rb
67
86
  - lib/legion/crypt/vault.rb
68
87
  - lib/legion/crypt/vault_jwt_auth.rb
69
88
  - lib/legion/crypt/vault_renewer.rb