legion-crypt 1.4.1 → 1.4.2

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: 171a3a22eeb730be2a47dccb7f96ec1b4ffcf4df03792eae594f63b61106224a
4
+ data.tar.gz: 597ca3ea73e5864a572621312cf7ff0efea424c4e3e05c22e16f0044deb97753
5
5
  SHA512:
6
- metadata.gz: 6f120ed5f85a929fe22e750e482253550000888afbc6633c989df680c7acdc93673303014d911871642a48f66ab05e6899ab773319b1a195c64736eb052fc204
7
- data.tar.gz: 8a4d154360dca522f05982d986eb28d25212038c7ca37e94253a4a1a89b03a9a51a15e46e2dedc77f382b5b7f84fcca3c8924332c9dae230858a3c154e3e8b15
6
+ metadata.gz: fb76b8b671a10380aaccb82eb62f611f9998112bd95ec8a5b184679b1694204d3b7adb14b3c7f141ae072254671728bf2b8b1701c94291f0b285ccb335d0e15e
7
+ data.tar.gz: 8d0d58cb2c9d4fc543fc7d3be4628142405c388e1436211df59d1d26614c18d49f6e158ab25e440b643401c8c527de1160470a0623536a4f79040fb2f475a6fc
data/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.4.2] - 2026-03-16
6
+
7
+ ### Added
8
+ - `Legion::Crypt::Ed25519`: Ed25519 key generation, signing, verification, Vault key storage
9
+ - `Legion::Crypt::PartitionKeys`: HKDF-based per-tenant key derivation with AES-256-GCM encrypt/decrypt
10
+ - `Legion::Crypt::Erasure`: cryptographic erasure via Vault master key deletion with event emission
11
+ - `Legion::Crypt::Attestation`: signed identity claims with Ed25519 signatures and freshness checking
12
+ - Dependency: `ed25519` gem ~> 1.3
13
+
5
14
  ## [1.4.1] - 2026-03-16
6
15
 
7
16
  ### Added
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Crypt
5
- VERSION = '1.4.1'
5
+ VERSION = '1.4.2'
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.2
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,12 +71,16 @@ 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
67
85
  - lib/legion/crypt/vault.rb
68
86
  - lib/legion/crypt/vault_jwt_auth.rb