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 +4 -4
- data/CHANGELOG.md +9 -0
- data/CLAUDE.md +9 -0
- data/legion-crypt.gemspec +1 -0
- data/lib/legion/crypt/attestation.rb +39 -0
- data/lib/legion/crypt/ed25519.rb +63 -0
- data/lib/legion/crypt/erasure.rb +43 -0
- data/lib/legion/crypt/partition_keys.rb +43 -0
- data/lib/legion/crypt/version.rb +1 -1
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 171a3a22eeb730be2a47dccb7f96ec1b4ffcf4df03792eae594f63b61106224a
|
|
4
|
+
data.tar.gz: 597ca3ea73e5864a572621312cf7ff0efea424c4e3e05c22e16f0044deb97753
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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
|
data/lib/legion/crypt/version.rb
CHANGED
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.
|
|
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
|