legion-crypt 1.4.6 → 1.4.7

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: 58236395edc171db0660e744e8ed15ed7497af43bb6154f9361693a6576f070c
4
- data.tar.gz: '008c7865a64d8a43c3c167ae3fc36aef7b537f26522b2ce05f40615b98ff3e80'
3
+ metadata.gz: 7a91784c5d25f02bae1517ea0192185a147df4739c5f40467c4a1eaac4c5d073
4
+ data.tar.gz: a5c94a7cbd0074c8b2494f0fcf7bf46eff9daa2c45fe04b86f4f7548e79e1c93
5
5
  SHA512:
6
- metadata.gz: eb66a3e5aaf65cf06e463e0ed4024121d9f8b9459731ef8422761fa0d9888c6e90bef58e0a4bb13cfd57eb2d14063ebfe07a8968209f738d15f99080669055e9
7
- data.tar.gz: 980231e72cdb5a7eb36702fcb69dfc25ff41811c35df5485476410e386126ff45c4911715ec43cc3c347932f6a5d79d4c70728afde25f99cf423d03aae223265
6
+ metadata.gz: c5695228ec9226e79cf6b14322ce944e7eb80c24450c91ddbcf4d4a93ee33ef825b92ee8f742029155b301e18d676e597e13ea33ecfbb2397b7cb13f45f0ef4e
7
+ data.tar.gz: 6b37ee3075d821ec4c3bc01b44ad1450177ba6ec8aa6ea2c1ffe7fa6e44c4d40ab3e5a15c63464dd0432d15c2eaaddee13802533333e8f35011895fde47cda0a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Legion::Crypt
2
2
 
3
+ ## [1.4.7] - 2026-03-22
4
+
5
+ ### Added
6
+ - Logging across vault, JWT, JWKS, Ed25519, PartitionKeys, Attestation, LdapAuth, VaultJwtAuth, VaultCluster operations
7
+ - `vault.rb`: `.info` on Vault connect, `.info` on cluster token renewal, `.debug` on read/write/get paths, `.warn` on read/write/get failures, `.debug` on renewal cycle start/complete
8
+ - `jwt.rb`: `.info` on JWT issue (subject, expiry, algorithm), `.debug` on verify success, `.warn` on verify failures (expired, invalid, decode) before raising
9
+ - `jwks_client.rb`: `.debug` on JWKS fetch URL, `.debug` on cache hit, `.warn` on fetch failure
10
+ - `ed25519.rb`: `.debug` on keypair generation, sign, verify, and Vault store/load paths
11
+ - `partition_keys.rb`: `.debug` on key derivation, `.warn` on encrypt/decrypt failures
12
+ - `attestation.rb`: `.debug` on attestation create/verify, `.warn` on verification failure
13
+ - `ldap_auth.rb`: `.info` on LDAP login success, `.warn` on LDAP login failure
14
+ - `vault_jwt_auth.rb`: `.warn` on JWT auth client/server errors in non-bang `login`
15
+ - `vault_cluster.rb`: `.info` on successful cluster connect
16
+
3
17
  ## [1.4.6] - 2026-03-21
4
18
 
5
19
  ### Fixed
data/CLAUDE.md CHANGED
@@ -8,7 +8,7 @@
8
8
  Handles encryption, decryption, secrets management, JWT token management, and HashiCorp Vault connectivity for the LegionIO framework. Provides AES-256-CBC message encryption, RSA key pair generation, cluster secret management, JWT issue/verify operations, and Vault token lifecycle management.
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/legion-crypt
11
- **Version**: 1.4.4
11
+ **Version**: 1.4.7
12
12
  **License**: Apache-2.0
13
13
 
14
14
  ## Architecture
data/CODEOWNERS CHANGED
@@ -1 +1,40 @@
1
+ # Default owner — all files
1
2
  * @Esity
3
+
4
+ # Core library code
5
+ # lib/ @Esity @future-security-team
6
+
7
+ # Cipher and key management
8
+ # lib/legion/crypt/cipher.rb @Esity @future-security-team
9
+ # lib/legion/crypt/partition_keys.rb @Esity @future-security-team
10
+ # lib/legion/crypt/ed25519.rb @Esity @future-security-team
11
+
12
+ # Vault integration
13
+ # lib/legion/crypt/vault.rb @Esity @future-security-team
14
+ # lib/legion/crypt/vault_jwt_auth.rb @Esity @future-security-team
15
+ # lib/legion/crypt/vault_renewer.rb @Esity @future-security-team
16
+ # lib/legion/crypt/vault_cluster.rb @Esity @future-security-team
17
+ # lib/legion/crypt/lease_manager.rb @Esity @future-security-team
18
+
19
+ # JWT and JWKS
20
+ # lib/legion/crypt/jwt.rb @Esity @future-security-team
21
+ # lib/legion/crypt/jwks_client.rb @Esity @future-security-team
22
+
23
+ # Auth integrations
24
+ # lib/legion/crypt/ldap_auth.rb @Esity @future-security-team
25
+ # lib/legion/crypt/vault_kerberos_auth.rb @Esity @future-security-team
26
+
27
+ # Cluster secret
28
+ # lib/legion/crypt/cluster_secret.rb @Esity @future-security-team
29
+
30
+ # Cryptographic erasure
31
+ # lib/legion/crypt/erasure.rb @Esity @future-security-team
32
+
33
+ # Specs
34
+ # spec/ @Esity @future-contributors
35
+
36
+ # Documentation
37
+ # *.md @Esity @future-docs-team
38
+
39
+ # CI/CD
40
+ # .github/ @Esity
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Encryption, secrets management, JWT token management, and HashiCorp Vault integration for the [LegionIO](https://github.com/LegionIO/LegionIO) framework. Provides AES-256-CBC message encryption, RSA key pair generation, cluster secret management, JWT issue/verify operations, Vault token lifecycle management, and multi-cluster Vault connectivity.
4
4
 
5
- **Version**: 1.4.4
5
+ **Version**: 1.4.7
6
6
 
7
7
  ## Installation
8
8
 
@@ -17,6 +17,7 @@ module Legion
17
17
 
18
18
  payload = Legion::JSON.dump(claim)
19
19
  signature = Legion::Crypt::Ed25519.sign(payload, private_key)
20
+ Legion::Logging.debug "Attestation created for agent #{agent_id}, state=#{state}" if defined?(Legion::Logging)
20
21
 
21
22
  { claim: claim, signature: signature.unpack1('H*'), payload: payload }
22
23
  end
@@ -24,7 +25,15 @@ module Legion
24
25
  def verify(claim_hash:, signature_hex:, public_key:)
25
26
  payload = Legion::JSON.dump(claim_hash)
26
27
  signature = [signature_hex].pack('H*')
27
- Legion::Crypt::Ed25519.verify(payload, signature, public_key)
28
+ result = Legion::Crypt::Ed25519.verify(payload, signature, public_key)
29
+ if defined?(Legion::Logging)
30
+ if result
31
+ Legion::Logging.debug "Attestation verified for agent #{claim_hash[:agent_id]}"
32
+ else
33
+ Legion::Logging.warn "Attestation verification failed for agent #{claim_hash[:agent_id]}"
34
+ end
35
+ end
36
+ result
28
37
  end
29
38
 
30
39
  def fresh?(claim_hash, max_age_seconds: 300)
@@ -8,6 +8,7 @@ module Legion
8
8
  class << self
9
9
  def generate_keypair
10
10
  signing_key = ::Ed25519::SigningKey.generate
11
+ Legion::Logging.debug 'Ed25519 keypair generated' if defined?(Legion::Logging)
11
12
  {
12
13
  private_key: signing_key.to_bytes,
13
14
  public_key: signing_key.verify_key.to_bytes,
@@ -17,12 +18,15 @@ module Legion
17
18
 
18
19
  def sign(message, private_key_bytes)
19
20
  signing_key = ::Ed25519::SigningKey.new(private_key_bytes)
20
- signing_key.sign(message)
21
+ result = signing_key.sign(message)
22
+ Legion::Logging.debug 'Ed25519 sign complete' if defined?(Legion::Logging)
23
+ result
21
24
  end
22
25
 
23
26
  def verify(message, signature, public_key_bytes)
24
27
  verify_key = ::Ed25519::VerifyKey.new(public_key_bytes)
25
28
  verify_key.verify(signature, message)
29
+ Legion::Logging.debug 'Ed25519 verify success' if defined?(Legion::Logging)
26
30
  true
27
31
  rescue ::Ed25519::VerifyError
28
32
  false
@@ -32,6 +36,7 @@ module Legion
32
36
  keypair ||= generate_keypair
33
37
  vault_path = "#{key_prefix}/#{agent_id}"
34
38
  if defined?(Legion::Crypt::Vault)
39
+ Legion::Logging.debug "Ed25519 storing keypair at #{vault_path}" if defined?(Legion::Logging)
35
40
  Legion::Crypt::Vault.write(vault_path, {
36
41
  private_key: keypair[:private_key].unpack1('H*'),
37
42
  public_key: keypair[:public_key_hex]
@@ -42,6 +47,7 @@ module Legion
42
47
 
43
48
  def load_private_key(agent_id:)
44
49
  vault_path = "#{key_prefix}/#{agent_id}"
50
+ Legion::Logging.debug "Ed25519 loading private key from #{vault_path}" if defined?(Legion::Logging)
45
51
  data = Legion::Crypt::Vault.read(vault_path)
46
52
  [data[:private_key]].pack('H*') if data&.dig(:private_key)
47
53
  rescue StandardError
@@ -17,6 +17,7 @@ module Legion
17
17
  class << self
18
18
  def fetch_keys(jwks_url)
19
19
  @mutex.synchronize do
20
+ Legion::Logging.debug "JWKS fetch: #{jwks_url}" if defined?(Legion::Logging)
20
21
  response = http_get(jwks_url)
21
22
  jwks_data = parse_response(response)
22
23
  keys = parse_jwks(jwks_data)
@@ -24,6 +25,9 @@ module Legion
24
25
  @cache[jwks_url] = { keys: keys, fetched_at: Time.now }
25
26
  keys
26
27
  end
28
+ rescue StandardError => e
29
+ Legion::Logging.warn "JWKS fetch failed for #{jwks_url}: #{e.message}" if defined?(Legion::Logging)
30
+ raise
27
31
  end
28
32
 
29
33
  def find_key(jwks_url, kid)
@@ -31,10 +35,12 @@ module Legion
31
35
 
32
36
  if cached && !expired?(cached[:fetched_at])
33
37
  key = cached[:keys][kid]
34
- return key if key
38
+ if key
39
+ Legion::Logging.debug "JWKS cache hit: kid=#{kid}" if defined?(Legion::Logging)
40
+ return key
41
+ end
35
42
  end
36
43
 
37
- # Re-fetch once on cache miss or expiry
38
44
  keys = fetch_keys(jwks_url)
39
45
  key = keys[kid]
40
46
  return key if key
@@ -25,7 +25,9 @@ module Legion
25
25
  jti: SecureRandom.uuid
26
26
  }.merge(payload)
27
27
 
28
- ::JWT.encode(claims, signing_key, algorithm)
28
+ token = ::JWT.encode(claims, signing_key, algorithm)
29
+ Legion::Logging.info "JWT issued: sub=#{claims[:sub]}, exp=#{Time.at(claims[:exp]).utc.iso8601}, alg=#{algorithm}" if defined?(Legion::Logging)
30
+ token
29
31
  end
30
32
 
31
33
  def self.verify(token, verification_key:, **opts)
@@ -44,12 +46,17 @@ module Legion
44
46
  decode_opts[:iss] = issuer if verify_issuer
45
47
 
46
48
  payload, _header = ::JWT.decode(token, verification_key, true, decode_opts)
47
- symbolize_keys(payload)
49
+ result = symbolize_keys(payload)
50
+ Legion::Logging.debug "JWT verify success: sub=#{result[:sub]}, jti=#{result[:jti]}" if defined?(Legion::Logging)
51
+ result
48
52
  rescue ::JWT::ExpiredSignature
53
+ Legion::Logging.warn 'JWT verify failed: token has expired' if defined?(Legion::Logging)
49
54
  raise ExpiredTokenError, 'token has expired'
50
55
  rescue ::JWT::VerificationError, ::JWT::IncorrectAlgorithm
56
+ Legion::Logging.warn 'JWT verify failed: signature verification failed' if defined?(Legion::Logging)
51
57
  raise InvalidTokenError, 'token signature verification failed'
52
58
  rescue ::JWT::DecodeError => e
59
+ Legion::Logging.warn "JWT verify failed: #{e.message}" if defined?(Legion::Logging)
53
60
  raise DecodeError, "failed to decode token: #{e.message}"
54
61
  end
55
62
 
@@ -91,16 +98,23 @@ module Legion
91
98
  end
92
99
 
93
100
  payload, _header = ::JWT.decode(token, public_key, true, decode_opts)
94
- symbolize_keys(payload)
101
+ result = symbolize_keys(payload)
102
+ Legion::Logging.debug "JWT JWKS verify success: sub=#{result[:sub]}, kid=#{kid}" if defined?(Legion::Logging)
103
+ result
95
104
  rescue ::JWT::ExpiredSignature
105
+ Legion::Logging.warn "JWT JWKS verify failed: token has expired, kid=#{kid}" if defined?(Legion::Logging)
96
106
  raise ExpiredTokenError, 'token has expired'
97
107
  rescue ::JWT::VerificationError, ::JWT::IncorrectAlgorithm
108
+ Legion::Logging.warn "JWT JWKS verify failed: signature verification failed, kid=#{kid}" if defined?(Legion::Logging)
98
109
  raise InvalidTokenError, 'token signature verification failed'
99
110
  rescue ::JWT::InvalidIssuerError
111
+ Legion::Logging.warn "JWT JWKS verify failed: issuer not allowed, kid=#{kid}" if defined?(Legion::Logging)
100
112
  raise InvalidTokenError, 'token issuer not allowed'
101
113
  rescue ::JWT::InvalidAudError
114
+ Legion::Logging.warn "JWT JWKS verify failed: audience mismatch, kid=#{kid}" if defined?(Legion::Logging)
102
115
  raise InvalidTokenError, 'token audience mismatch'
103
116
  rescue ::JWT::DecodeError => e
117
+ Legion::Logging.warn "JWT JWKS verify failed: #{e.message}, kid=#{kid}" if defined?(Legion::Logging)
104
118
  raise DecodeError, "failed to decode token: #{e.message}"
105
119
  end
106
120
 
@@ -13,8 +13,12 @@ module Legion
13
13
  clusters[cluster_name][:token] = token
14
14
  clusters[cluster_name][:connected] = true
15
15
 
16
+ Legion::Logging.info "LDAP login success: user=#{username}, cluster=#{cluster_name}" if defined?(Legion::Logging)
16
17
  { token: token, lease_duration: auth.lease_duration,
17
18
  renewable: auth.renewable, policies: auth.policies }
19
+ rescue StandardError => e
20
+ Legion::Logging.warn "LDAP login failed: user=#{username}, cluster=#{cluster_name}: #{e.message}" if defined?(Legion::Logging)
21
+ raise
18
22
  end
19
23
 
20
24
  def ldap_login_all(username:, password:)
@@ -12,6 +12,7 @@ module Legion
12
12
  rescue StandardError
13
13
  nil
14
14
  end || 'legion-partition'
15
+ Legion::Logging.debug "PartitionKeys key derivation for tenant #{tenant_id}" if defined?(Legion::Logging)
15
16
  salt = OpenSSL::Digest::SHA256.digest(tenant_id.to_s)
16
17
  OpenSSL::KDF.hkdf(master_key, salt: salt, info: context, length: 32, hash: 'SHA256')
17
18
  end
@@ -26,6 +27,9 @@ module Legion
26
27
  auth_tag = cipher.auth_tag
27
28
 
28
29
  { ciphertext: ciphertext, iv: iv, auth_tag: auth_tag }
30
+ rescue StandardError => e
31
+ Legion::Logging.warn "PartitionKeys encrypt failed for tenant #{tenant_id}: #{e.message}" if defined?(Legion::Logging)
32
+ raise
29
33
  end
30
34
 
31
35
  def decrypt_for_tenant(ciphertext:, init_vector:, auth_tag:, tenant_id:, master_key:)
@@ -36,6 +40,9 @@ module Legion
36
40
  decipher.iv = init_vector
37
41
  decipher.auth_tag = auth_tag
38
42
  decipher.update(ciphertext) + decipher.final
43
+ rescue StandardError => e
44
+ Legion::Logging.warn "PartitionKeys decrypt failed for tenant #{tenant_id}: #{e.message}" if defined?(Legion::Logging)
45
+ raise
39
46
  end
40
47
  end
41
48
  end
@@ -32,7 +32,10 @@ module Legion
32
32
  return nil if Legion::Settings[:crypt][:vault][:token].nil?
33
33
 
34
34
  ::Vault.token = Legion::Settings[:crypt][:vault][:token]
35
- Legion::Settings[:crypt][:vault][:connected] = true if ::Vault.sys.health_status.initialized?
35
+ if ::Vault.sys.health_status.initialized?
36
+ Legion::Settings[:crypt][:vault][:connected] = true
37
+ Legion::Logging.info "Vault connected at #{::Vault.address}" if defined?(Legion::Logging)
38
+ end
36
39
  return unless Legion.const_defined? 'Extensions::Actors::Every'
37
40
 
38
41
  require_relative 'vault_renewer'
@@ -45,20 +48,32 @@ module Legion
45
48
 
46
49
  def read(path, type = 'legion')
47
50
  full_path = type.nil? || type.empty? ? "#{type}/#{path}" : path
51
+ Legion::Logging.debug "Vault read: #{full_path}" if defined?(Legion::Logging)
48
52
  lease = ::Vault.logical.read(full_path)
49
53
  add_session(path: lease.lease_id) if lease.respond_to? :lease_id
50
54
  lease.data
55
+ rescue StandardError => e
56
+ Legion::Logging.warn "Vault read failed at #{full_path}: #{e.message}" if defined?(Legion::Logging)
57
+ raise
51
58
  end
52
59
 
53
60
  def get(path)
61
+ Legion::Logging.debug "Vault kv get: #{path}" if defined?(Legion::Logging)
54
62
  result = ::Vault.kv(settings[:vault][:kv_path]).read(path)
55
63
  return nil if result.nil?
56
64
 
57
65
  result.data
66
+ rescue StandardError => e
67
+ Legion::Logging.warn "Vault kv get failed at #{path}: #{e.message}" if defined?(Legion::Logging)
68
+ raise
58
69
  end
59
70
 
60
71
  def write(path, **hash)
72
+ Legion::Logging.debug "Vault kv write: #{path}" if defined?(Legion::Logging)
61
73
  ::Vault.kv(settings[:vault][:kv_path]).write(path, **hash)
74
+ rescue StandardError => e
75
+ Legion::Logging.warn "Vault kv write failed at #{path}: #{e.message}" if defined?(Legion::Logging)
76
+ raise
62
77
  end
63
78
 
64
79
  def exist?(path)
@@ -96,19 +111,23 @@ module Legion
96
111
  end
97
112
 
98
113
  def renew_sessions(**_opts)
99
- if respond_to?(:connected_clusters) && connected_clusters.any?
100
- renew_cluster_tokens
101
- else
102
- @sessions.each do |session|
103
- renew_session(session: session)
104
- end
105
- end
114
+ Legion::Logging.debug 'Vault renewal cycle start' if defined?(Legion::Logging)
115
+ result = if respond_to?(:connected_clusters) && connected_clusters.any?
116
+ renew_cluster_tokens
117
+ else
118
+ @sessions.each do |session|
119
+ renew_session(session: session)
120
+ end
121
+ end
122
+ Legion::Logging.debug 'Vault renewal cycle complete' if defined?(Legion::Logging)
123
+ result
106
124
  end
107
125
 
108
126
  def renew_cluster_tokens
109
127
  connected_clusters.each_key do |name|
110
128
  client = vault_client(name)
111
129
  client.auth_token.renew_self
130
+ Legion::Logging.info "Vault token renewed for cluster #{name}" if defined?(Legion::Logging)
112
131
  rescue StandardError => e
113
132
  log_vault_error(name, e)
114
133
  end
@@ -37,6 +37,7 @@ module Legion
37
37
  client = vault_client(name)
38
38
  config[:connected] = client.sys.health_status.initialized?
39
39
  results[name] = config[:connected]
40
+ Legion::Logging.info "Vault cluster connected: #{name} at #{config[:address]}" if config[:connected] && defined?(Legion::Logging)
40
41
  rescue StandardError => e
41
42
  config[:connected] = false
42
43
  results[name] = false
@@ -44,8 +44,10 @@ module Legion
44
44
  metadata: response.auth.metadata
45
45
  }
46
46
  rescue ::Vault::HTTPClientError => e
47
+ Legion::Logging.warn "Vault JWT auth failed (client error): role=#{role}, #{e.message}" if defined?(Legion::Logging)
47
48
  raise AuthError, "Vault JWT auth failed: #{e.message}"
48
49
  rescue ::Vault::HTTPServerError => e
50
+ Legion::Logging.warn "Vault JWT auth failed (server error): role=#{role}, #{e.message}" if defined?(Legion::Logging)
49
51
  raise AuthError, "Vault server error during JWT auth: #{e.message}"
50
52
  end
51
53
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Crypt
5
- VERSION = '1.4.6'
5
+ VERSION = '1.4.7'
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.6
4
+ version: 1.4.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity