linzer 0.7.7 → 0.7.8

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +3 -1
  5. data/flake.lock +109 -0
  6. data/flake.nix +73 -0
  7. data/lib/linzer/common.rb +51 -0
  8. data/lib/linzer/ecdsa.rb +51 -0
  9. data/lib/linzer/ed25519.rb +35 -0
  10. data/lib/linzer/helper.rb +79 -0
  11. data/lib/linzer/hmac.rb +47 -1
  12. data/lib/linzer/http/bootstrap.rb +11 -0
  13. data/lib/linzer/http/signature_feature.rb +53 -1
  14. data/lib/linzer/http.rb +54 -0
  15. data/lib/linzer/jws.rb +74 -0
  16. data/lib/linzer/key/helper.rb +186 -10
  17. data/lib/linzer/key.rb +73 -0
  18. data/lib/linzer/message/adapter/abstract.rb +75 -10
  19. data/lib/linzer/message/adapter/generic/request.rb +27 -0
  20. data/lib/linzer/message/adapter/generic/response.rb +17 -0
  21. data/lib/linzer/message/adapter/http_gem/request.rb +11 -0
  22. data/lib/linzer/message/adapter/http_gem/response.rb +8 -5
  23. data/lib/linzer/message/adapter/net_http/request.rb +7 -0
  24. data/lib/linzer/message/adapter/net_http/response.rb +4 -0
  25. data/lib/linzer/message/adapter/rack/common.rb +14 -6
  26. data/lib/linzer/message/adapter/rack/request.rb +13 -0
  27. data/lib/linzer/message/adapter/rack/response.rb +11 -0
  28. data/lib/linzer/message/adapter.rb +17 -0
  29. data/lib/linzer/message/field/parser.rb +14 -0
  30. data/lib/linzer/message/field.rb +32 -2
  31. data/lib/linzer/message/wrapper.rb +20 -0
  32. data/lib/linzer/message.rb +113 -3
  33. data/lib/linzer/options.rb +13 -0
  34. data/lib/linzer/rsa.rb +34 -0
  35. data/lib/linzer/rsa_pss.rb +44 -0
  36. data/lib/linzer/signature.rb +113 -1
  37. data/lib/linzer/signer.rb +69 -0
  38. data/lib/linzer/verifier.rb +52 -0
  39. data/lib/linzer/version.rb +3 -1
  40. data/lib/linzer.rb +104 -0
  41. data/lib/rack/auth/signature.rb +90 -6
  42. metadata +30 -16
@@ -5,6 +5,8 @@ require_relative "bootstrap"
5
5
  module Linzer
6
6
  module HTTP
7
7
  class << self
8
+ # Registers the HTTP gem request adapter.
9
+ # @api private
8
10
  def register_adapter
9
11
  request_class = ::HTTP::Request
10
12
  adapter_class = Linzer::Message::Adapter::HTTPGem::Request
@@ -15,15 +17,62 @@ module Linzer
15
17
  Bootstrap.load_dependencies
16
18
  register_adapter
17
19
 
20
+ # HTTP.rb gem feature for automatic request signing.
21
+ #
22
+ # This feature integrates with the http.rb gem to automatically sign
23
+ # outgoing HTTP requests. It wraps each request before sending and
24
+ # adds the `signature` and `signature-input` headers.
25
+ #
26
+ # @note This file must be explicitly required:
27
+ # `require "linzer/http/signature_feature"`
28
+ #
29
+ # @example Basic usage
30
+ # require "linzer/http/signature_feature"
31
+ #
32
+ # key = Linzer.generate_ed25519_key("my-key")
33
+ # response = HTTP
34
+ # .use(http_signature: { key: key })
35
+ # .get("https://example.com/api")
36
+ #
37
+ # @example With custom components and parameters
38
+ # response = HTTP
39
+ # .use(http_signature: {
40
+ # key: key,
41
+ # covered_components: %w[@method @authority @path date],
42
+ # params: { nonce: SecureRandom.hex(16) }
43
+ # })
44
+ # .post("https://example.com/api", json: { data: "value" })
45
+ #
46
+ # @see https://github.com/httprb/http http.rb gem
18
47
  class SignatureFeature < ::HTTP::Feature
48
+ # Creates a new signature feature.
49
+ #
50
+ # @param key [Linzer::Key] The signing key (required)
51
+ # @param params [Hash] Additional signature parameters
52
+ # (created, nonce, tag, etc.)
53
+ # @param covered_components [Array<String>] Components to include
54
+ # in the signature. Defaults to `@method`, `@request-target`,
55
+ # `@authority`, and `date`.
56
+ #
57
+ # @raise [HTTP::Error] If key is nil or invalid
19
58
  def initialize(key:, params: {}, covered_components: default_components)
20
59
  @fields = Array(covered_components)
21
60
  @key = validate_key(key)
22
61
  @params = Hash(params)
23
62
  end
24
63
 
25
- attr_reader :fields, :params
64
+ # @return [Array<String>] The components to include in signatures
65
+ attr_reader :fields
26
66
 
67
+ # @return [Hash] Additional signature parameters
68
+ attr_reader :params
69
+
70
+ # Wraps an outgoing request to add signature headers.
71
+ #
72
+ # Called automatically by http.rb for each request.
73
+ #
74
+ # @param request [HTTP::Request] The outgoing request
75
+ # @return [HTTP::Request] The request with signature headers added
27
76
  def wrap_request(request)
28
77
  message = Linzer::Message.new(request)
29
78
  signature = Linzer.sign(key, message, fields, **params)
@@ -31,6 +80,8 @@ module Linzer
31
80
  request
32
81
  end
33
82
 
83
+ # Returns the default covered components.
84
+ # @return [Array<String>] Default components from {Options::DEFAULT}
34
85
  def default_covered_components
35
86
  Linzer::Options::DEFAULT[:covered_components]
36
87
  end
@@ -47,6 +98,7 @@ module Linzer
47
98
  key
48
99
  end
49
100
 
101
+ # Register this feature with http.rb
50
102
  ::HTTP::Options.register_feature(:http_signature, self)
51
103
  end
52
104
  end
data/lib/linzer/http.rb CHANGED
@@ -3,9 +3,38 @@
3
3
  require "net/http"
4
4
 
5
5
  module Linzer
6
+ # Simple HTTP client with automatic request signing.
7
+ #
8
+ # This module provides convenience methods for making signed HTTP requests
9
+ # using Net::HTTP. It automatically signs outgoing requests with the
10
+ # provided key.
11
+ #
12
+ # For each standard HTTP method (GET, POST, PUT, DELETE, etc.), a
13
+ # corresponding method is dynamically defined.
14
+ #
15
+ # @example Making a signed GET request
16
+ # key = Linzer.generate_ed25519_key("my-key")
17
+ # response = Linzer::HTTP.get("https://example.com/api", key: key)
18
+ #
19
+ # @example Making a signed POST request with body
20
+ # response = Linzer::HTTP.post("https://example.com/api",
21
+ # key: key,
22
+ # data: { "name" => "value" }.to_json,
23
+ # headers: { "Content-Type" => "application/json" }
24
+ # )
25
+ #
26
+ # @example Customizing covered components
27
+ # response = Linzer::HTTP.get("https://example.com/api",
28
+ # key: key,
29
+ # covered_components: %w[@method @authority @path date content-type]
30
+ # )
31
+ #
32
+ # @see SignatureFeature For http.rb gem integration
6
33
  module HTTP
7
34
  extend self
8
35
 
36
+ # Returns the list of known HTTP methods from Net::HTTP.
37
+ # @return [Array<String>] HTTP method names (e.g., "GET", "POST")
9
38
  def self.known_http_methods
10
39
  Net::HTTP
11
40
  .constants
@@ -15,6 +44,29 @@ module Linzer
15
44
  .freeze
16
45
  end
17
46
 
47
+ # Dynamically define methods for each HTTP verb (get, post, put, etc.)
48
+ #
49
+ # @!method get(uri, options)
50
+ # Makes a signed GET request.
51
+ # @param uri [String] The request URI
52
+ # @param options [Hash] Request options
53
+ # @option options [Key] :key The signing key (required)
54
+ # @option options [Hash] :headers Additional headers
55
+ # @option options [Array<String>] :covered_components Components to sign
56
+ # @option options [Hash] :params Additional signature parameters
57
+ # @option options [Boolean] :debug Enable debug output
58
+ # @return [Net::HTTPResponse] The response
59
+ #
60
+ # @!method post(uri, options)
61
+ # Makes a signed POST request.
62
+ # @param uri [String] The request URI
63
+ # @param options [Hash] Request options
64
+ # @option options [Key] :key The signing key (required)
65
+ # @option options [String] :data Request body (required for POST)
66
+ # @option options [Hash] :headers Additional headers
67
+ # @option options [Array<String>] :covered_components Components to sign
68
+ # @option options [Hash] :params Additional signature parameters
69
+ # @return [Net::HTTPResponse] The response
18
70
  known_http_methods.each do |http_method| # e.g.:
19
71
  method = http_method.downcase.to_sym #
20
72
  define_method(method) do |uri, options| # def post(uri, **options)
@@ -25,6 +77,7 @@ module Linzer
25
77
 
26
78
  private
27
79
 
80
+ # Executes a signed HTTP request.
28
81
  def request(verb, uri, options = {})
29
82
  validate_verb(verb)
30
83
 
@@ -76,6 +129,7 @@ module Linzer
76
129
  request
77
130
  end
78
131
 
132
+ # Determines if the HTTP method typically has a request body.
79
133
  def with_body?(verb)
80
134
  # common HTTP
81
135
  return false if %i[get head options trace delete].include?(verb)
data/lib/linzer/jws.rb CHANGED
@@ -5,13 +5,58 @@ require "jwt/eddsa"
5
5
  require "ed25519"
6
6
 
7
7
  module Linzer
8
+ # JSON Web Signature (JWS) compatible key support.
9
+ #
10
+ # This module provides integration with the jwt gem for working with
11
+ # JWK (JSON Web Key) format keys. It enables interoperability with
12
+ # systems using JWS/JWT standards.
13
+ #
14
+ # Currently supports:
15
+ # - EdDSA (Ed25519)
16
+ #
17
+ # @note This module requires the `jwt` and `ed25519` gems.
18
+ #
19
+ # @example Generating a JWS-compatible EdDSA key
20
+ # key = Linzer.generate_jws_key(algorithm: "EdDSA")
21
+ #
22
+ # @example Importing from JWK format
23
+ # jwk = {
24
+ # "kty" => "OKP",
25
+ # "crv" => "Ed25519",
26
+ # "x" => "...",
27
+ # "d" => "..." # private key component (optional)
28
+ # }
29
+ # key = Linzer.jwk_import(jwk)
30
+ #
31
+ # @see https://www.rfc-editor.org/rfc/rfc7517 RFC 7517 - JSON Web Key (JWK)
32
+ # @see https://www.rfc-editor.org/rfc/rfc8037 RFC 8037 - EdDSA for JWS/JWK
8
33
  module JWS
34
+ # Imports a key from JWK (JSON Web Key) format.
35
+ #
36
+ # @param key [Hash] The JWK as a Hash with string keys
37
+ # @param params [Hash] Additional key parameters
38
+ # @option params [String] :id Key identifier (overrides JWK "kid" if present)
39
+ # @return [JWS::Key] The imported key
40
+ # @raise [Error] If the JWK format is invalid or algorithm unsupported
41
+ #
42
+ # @example
43
+ # jwk = JSON.parse(File.read("key.jwk"))
44
+ # key = Linzer::JWS.jwk_import(jwk, id: "my-key-id")
9
45
  def jwk_import(key, params = {})
10
46
  material = JWT::JWK.import(key)
11
47
  Linzer::JWS::Key.new(material, params)
12
48
  end
13
49
  module_function :jwk_import
14
50
 
51
+ # Generates a new JWS-compatible key pair.
52
+ #
53
+ # @param algorithm [String] The JWS algorithm identifier.
54
+ # Currently only "EdDSA" is supported.
55
+ # @return [JWS::Key] A new key pair
56
+ # @raise [Error] If the algorithm is not supported
57
+ #
58
+ # @example
59
+ # key = Linzer::JWS.generate_key(algorithm: "EdDSA")
15
60
  def generate_key(algorithm:)
16
61
  case String(algorithm)
17
62
  when "EdDSA"
@@ -25,25 +70,52 @@ module Linzer
25
70
  end
26
71
  module_function :generate_key
27
72
 
73
+ # JWS-compatible signing key implementation.
74
+ #
75
+ # Wraps a JWT::JWK key object to provide the Linzer Key interface.
76
+ # This enables using JWK-format keys with HTTP Message Signatures.
77
+ #
78
+ # @see Linzer::Key::Helper#generate_jws_key
79
+ # @see Linzer::Key::Helper#jwk_import
28
80
  class Key < Linzer::Key
81
+ # Signs data using the JWS key.
82
+ #
83
+ # @param data [String] The data to sign
84
+ # @return [String] The signature bytes
85
+ # @raise [SigningError] If this key cannot be used for signing
29
86
  def sign(data)
30
87
  validate_signing_key
31
88
  algo = resolve_algorithm
32
89
  algo.sign(data: data, signing_key: signing_key)
33
90
  end
34
91
 
92
+ # Verifies a signature using the JWS key.
93
+ #
94
+ # @param signature [String] The signature bytes to verify
95
+ # @param data [String] The data that was signed
96
+ # @return [Boolean] true if valid, false otherwise
97
+ # @raise [VerifyError] If this key cannot be used for verification
35
98
  def verify(signature, data)
36
99
  validate_verify_key
37
100
  algo = resolve_algorithm
38
101
  algo.verify(data: data, signature: signature, verification_key: verify_key)
39
102
  end
40
103
 
104
+ # @return [Boolean] true if this key can verify signatures
41
105
  def public?
42
106
  !!verify_key
43
107
  end
44
108
 
109
+ # @return [Boolean] true if this key can create signatures
110
+ def private?
111
+ !!signing_key
112
+ end
113
+
45
114
  private
46
115
 
116
+ # Resolves the appropriate JWT algorithm implementation.
117
+ # @return [JWT::JWA::SigningAlgorithm] The algorithm implementation
118
+ # @raise [Error] If the algorithm cannot be determined
47
119
  def resolve_algorithm
48
120
  case
49
121
  when material.verify_key.is_a?(::Ed25519::VerifyKey)
@@ -53,10 +125,12 @@ module Linzer
53
125
  end
54
126
  end
55
127
 
128
+ # @return [Ed25519::VerifyKey, nil] The verification key
56
129
  def verify_key
57
130
  material.verify_key
58
131
  end
59
132
 
133
+ # @return [Ed25519::SigningKey, nil] The signing key
60
134
  def signing_key
61
135
  material.signing_key
62
136
  end
@@ -2,94 +2,270 @@
2
2
 
3
3
  module Linzer
4
4
  class Key
5
+ # Helper methods for generating and loading cryptographic keys.
6
+ #
7
+ # These methods provide convenient factory functions for creating
8
+ # {Linzer::Key} instances for various algorithms. They are mixed into
9
+ # the {Linzer} module and can be called directly.
10
+ #
11
+ # @example Generating keys
12
+ # ed25519_key = Linzer.generate_ed25519_key("my-key")
13
+ # hmac_key = Linzer.generate_hmac_sha256_key("hmac-key")
14
+ # ecdsa_key = Linzer.generate_ecdsa_p256_sha256_key("ecdsa-key")
15
+ #
16
+ # @example Loading keys from PEM
17
+ # key = Linzer.new_ed25519_key(File.read("private.pem"), "my-key")
18
+ # pubkey = Linzer.new_ed25519_public_key(File.read("public.pem"), "my-key")
19
+ #
20
+ # @see Key
5
21
  module Helper
22
+ # Generates a new RSA-PSS key pair with SHA-512 digest.
23
+ #
24
+ # RSA-PSS (RSASSA-PSS) is the recommended RSA signature scheme.
25
+ # Uses the `rsa-pss-sha512` algorithm identifier.
26
+ #
27
+ # @param size [Integer] Key size in bits (ignored, uses OpenSSL default)
28
+ # @param key_id [String, nil] Optional key identifier
29
+ # @return [RSAPSS::Key] A new RSA-PSS key pair
30
+ #
31
+ # @example
32
+ # key = Linzer.generate_rsa_pss_sha512_key(2048, "my-rsa-key")
6
33
  def generate_rsa_pss_sha512_key(size, key_id = nil)
7
34
  material = OpenSSL::PKey.generate_key("RSASSA-PSS")
8
35
  Linzer::RSAPSS::Key.new(material, id: key_id, digest: "SHA512")
9
36
  end
10
37
 
38
+ # Loads an RSA-PSS key from PEM-encoded material.
39
+ #
40
+ # Can load either a private key (for signing) or public key (for verification).
41
+ #
42
+ # @param material [String] PEM-encoded RSA key
43
+ # @param key_id [String, nil] Optional key identifier
44
+ # @return [RSAPSS::Key] The loaded key
45
+ #
46
+ # @example Loading a private key
47
+ # key = Linzer.new_rsa_pss_sha512_key(File.read("private.pem"), "my-key")
48
+ #
49
+ # @example Loading a public key
50
+ # pubkey = Linzer.new_rsa_pss_sha512_key(File.read("public.pem"), "my-key")
11
51
  def new_rsa_pss_sha512_key(material, key_id = nil)
12
52
  key = OpenSSL::PKey.read(material)
13
53
  Linzer::RSAPSS::Key.new(key, id: key_id, digest: "SHA512")
14
54
  end
15
55
 
16
- # XXX: investigate: was this method made redundant after:
17
- # https://github.com/nomadium/linzer/pull/10
56
+ # Loads an RSA-PSS public key from PEM-encoded material.
57
+ #
58
+ # @deprecated Use {#new_rsa_pss_sha512_key} instead, which handles both
59
+ # public and private keys.
60
+ # @param material [String] PEM-encoded RSA public key
61
+ # @param key_id [String, nil] Optional key identifier
62
+ # @return [RSAPSS::Key] The loaded public key
18
63
  def new_rsa_pss_sha512_public_key(material, key_id = nil)
19
64
  key = OpenSSL::PKey::RSA.new(material)
20
65
  Linzer::RSAPSS::Key.new(key, id: key_id, digest: "SHA512")
21
66
  end
22
67
 
68
+ # Generates a new RSA PKCS#1 v1.5 key pair with SHA-256 digest.
69
+ #
70
+ # @note RSA-PSS is preferred for new applications. Use this only for
71
+ # compatibility with systems requiring PKCS#1 v1.5.
72
+ #
73
+ # Uses the `rsa-v1_5-sha256` algorithm identifier.
74
+ #
75
+ # @param size [Integer] Key size in bits (e.g., 2048, 4096)
76
+ # @param key_id [String, nil] Optional key identifier
77
+ # @return [RSA::Key] A new RSA key pair
78
+ #
79
+ # @example
80
+ # key = Linzer.generate_rsa_v1_5_sha256_key(2048, "legacy-rsa")
23
81
  def generate_rsa_v1_5_sha256_key(size, key_id = nil)
24
82
  material = OpenSSL::PKey::RSA.generate(size)
25
83
  Linzer::RSA::Key.new(material, id: key_id, digest: "SHA256")
26
84
  end
27
85
 
86
+ # Loads an RSA PKCS#1 v1.5 key from PEM-encoded material.
87
+ #
88
+ # @param material [String] PEM-encoded RSA key
89
+ # @param key_id [String, nil] Optional key identifier
90
+ # @return [RSA::Key] The loaded key
28
91
  def new_rsa_v1_5_sha256_key(material, key_id = nil)
29
92
  key = OpenSSL::PKey.read(material)
30
93
  Linzer::RSA::Key.new(key, id: key_id, digest: "SHA256")
31
94
  end
32
95
 
33
- # XXX: investigate: was this method made redundant after:
34
- # https://github.com/nomadium/linzer/pull/10
96
+ # Loads an RSA PKCS#1 v1.5 public key from PEM-encoded material.
97
+ #
98
+ # @deprecated Use {#new_rsa_v1_5_sha256_key} instead.
99
+ # @param material [String] PEM-encoded RSA public key
100
+ # @param key_id [String, nil] Optional key identifier
101
+ # @return [RSA::Key] The loaded public key
35
102
  def new_rsa_v1_5_sha256_public_key(material, key_id = nil)
36
103
  key = OpenSSL::PKey.read(material)
37
104
  Linzer::RSA::Key.new(key, id: key_id, digest: "SHA256")
38
105
  end
39
106
 
107
+ # Generates a new HMAC-SHA256 symmetric key.
108
+ #
109
+ # HMAC keys are symmetric, meaning the same key is used for both
110
+ # signing and verification. The key material must be kept secret.
111
+ #
112
+ # Uses the `hmac-sha256` algorithm identifier.
113
+ #
114
+ # @param key_id [String, nil] Optional key identifier
115
+ # @return [HMAC::Key] A new 64-byte random HMAC key
116
+ #
117
+ # @example
118
+ # key = Linzer.generate_hmac_sha256_key("shared-secret")
40
119
  def generate_hmac_sha256_key(key_id = nil)
41
120
  material = OpenSSL::Random.random_bytes(64)
42
121
  Linzer::HMAC::Key.new(material, id: key_id, digest: "SHA256")
43
122
  end
44
123
 
124
+ # Creates an HMAC-SHA256 key from existing key material.
125
+ #
126
+ # @param material [String] The secret key bytes (should be at least 32 bytes)
127
+ # @param key_id [String, nil] Optional key identifier
128
+ # @return [HMAC::Key] The HMAC key
129
+ #
130
+ # @example Loading from environment
131
+ # key = Linzer.new_hmac_sha256_key(
132
+ # Base64.decode64(ENV["HMAC_SECRET"]),
133
+ # "api-key"
134
+ # )
45
135
  def new_hmac_sha256_key(material, key_id = nil)
46
136
  Linzer::HMAC::Key.new(material, id: key_id, digest: "SHA256")
47
137
  end
48
138
 
139
+ # Generates a new Ed25519 key pair.
140
+ #
141
+ # Ed25519 is a modern elliptic curve signature algorithm that provides
142
+ # excellent security and performance. Recommended for new applications.
143
+ #
144
+ # Uses the `ed25519` algorithm identifier.
145
+ #
146
+ # @param key_id [String, nil] Optional key identifier
147
+ # @return [Ed25519::Key] A new Ed25519 key pair
148
+ #
149
+ # @example
150
+ # key = Linzer.generate_ed25519_key("my-ed25519-key")
49
151
  def generate_ed25519_key(key_id = nil)
50
152
  material = OpenSSL::PKey.generate_key("ed25519")
51
153
  Linzer::Ed25519::Key.new(material, id: key_id)
52
154
  end
53
155
 
156
+ # Loads an Ed25519 key from PEM-encoded material.
157
+ #
158
+ # Can load either a private key (for signing) or public key (for verification).
159
+ #
160
+ # @param material [String] PEM-encoded Ed25519 key
161
+ # @param key_id [String, nil] Optional key identifier
162
+ # @return [Ed25519::Key] The loaded key
163
+ #
164
+ # @example Loading a private key for signing
165
+ # key = Linzer.new_ed25519_key(File.read("ed25519.pem"), "my-key")
54
166
  def new_ed25519_key(material, key_id = nil)
55
167
  key = OpenSSL::PKey.read(material)
56
168
  Linzer::Ed25519::Key.new(key, id: key_id)
57
169
  end
58
170
 
171
+ # Loads an Ed25519 public key from PEM-encoded material.
172
+ #
173
+ # This is an alias for {#new_ed25519_key} for clarity when loading
174
+ # public keys specifically.
175
+ #
176
+ # @param material [String] PEM-encoded Ed25519 public key
177
+ # @param key_id [String, nil] Optional key identifier
178
+ # @return [Ed25519::Key] The loaded public key
179
+ #
180
+ # @example
181
+ # pubkey = Linzer.new_ed25519_public_key(File.read("ed25519_pub.pem"), "my-key")
59
182
  def new_ed25519_public_key(material, key_id = nil)
60
183
  new_ed25519_key(material, key_id)
61
184
  end
62
185
 
63
- # https://www.rfc-editor.org/rfc/rfc4492.html#appendix-A
64
- # Table 6: Equivalent curves defined by SECG, ANSI, and NIST
65
- # secp256r1 | prime256v1 | NIST P-256
186
+ # Generates a new ECDSA P-256 key pair with SHA-256 digest.
187
+ #
188
+ # ECDSA P-256 (also known as secp256r1 or prime256v1) is widely supported
189
+ # and provides good security for most applications.
190
+ #
191
+ # Uses the `ecdsa-p256-sha256` algorithm identifier.
192
+ #
193
+ # @param key_id [String, nil] Optional key identifier
194
+ # @return [ECDSA::Key] A new ECDSA P-256 key pair
195
+ #
196
+ # @see https://www.rfc-editor.org/rfc/rfc4492.html#appendix-A RFC 4492 Appendix A
197
+ #
198
+ # @example
199
+ # key = Linzer.generate_ecdsa_p256_sha256_key("ecdsa-key")
66
200
  def generate_ecdsa_p256_sha256_key(key_id = nil)
67
201
  material = OpenSSL::PKey::EC.generate("prime256v1")
68
202
  Linzer::ECDSA::Key.new(material, id: key_id, digest: "SHA256")
69
203
  end
70
204
 
205
+ # Loads an ECDSA P-256 key from PEM-encoded material.
206
+ #
207
+ # @param material [String] PEM-encoded EC key
208
+ # @param key_id [String, nil] Optional key identifier
209
+ # @return [ECDSA::Key] The loaded key
71
210
  def new_ecdsa_p256_sha256_key(material, key_id = nil)
72
211
  key = OpenSSL::PKey::EC.new(material)
73
212
  Linzer::ECDSA::Key.new(key, id: key_id, digest: "SHA256")
74
213
  end
75
214
 
76
- # https://www.rfc-editor.org/rfc/rfc4492.html#appendix-A
77
- # Table 6: Equivalent curves defined by SECG, ANSI, and NIST
78
- # secp384r1 | | NIST P-384
215
+ # Generates a new ECDSA P-384 key pair with SHA-384 digest.
216
+ #
217
+ # ECDSA P-384 (also known as secp384r1) provides higher security than P-256
218
+ # at the cost of larger signatures and slightly slower operations.
219
+ #
220
+ # Uses the `ecdsa-p384-sha384` algorithm identifier.
221
+ #
222
+ # @param key_id [String, nil] Optional key identifier
223
+ # @return [ECDSA::Key] A new ECDSA P-384 key pair
224
+ #
225
+ # @see https://www.rfc-editor.org/rfc/rfc4492.html#appendix-A RFC 4492 Appendix A
226
+ #
227
+ # @example
228
+ # key = Linzer.generate_ecdsa_p384_sha384_key("high-security-key")
79
229
  def generate_ecdsa_p384_sha384_key(key_id = nil)
80
230
  material = OpenSSL::PKey::EC.generate("secp384r1")
81
231
  Linzer::ECDSA::Key.new(material, id: key_id, digest: "SHA384")
82
232
  end
83
233
 
234
+ # Loads an ECDSA P-384 key from PEM-encoded material.
235
+ #
236
+ # @param material [String] PEM-encoded EC key
237
+ # @param key_id [String, nil] Optional key identifier
238
+ # @return [ECDSA::Key] The loaded key
84
239
  def new_ecdsa_p384_sha384_key(material, key_id = nil)
85
240
  key = OpenSSL::PKey::EC.new(material)
86
241
  Linzer::ECDSA::Key.new(key, id: key_id, digest: "SHA384")
87
242
  end
88
243
 
244
+ # Generates a new JWS key for the specified algorithm.
245
+ #
246
+ # This method generates keys compatible with JSON Web Signature (JWS)
247
+ # format. Currently only EdDSA (Ed25519) is supported.
248
+ #
249
+ # @param algorithm [String] The JWS algorithm identifier (e.g., "EdDSA")
250
+ # @return [JWS::Key] A new JWS-compatible key
251
+ # @raise [Error] If the algorithm is not supported
252
+ #
253
+ # @example
254
+ # key = Linzer.generate_jws_key(algorithm: "EdDSA")
89
255
  def generate_jws_key(algorithm:)
90
256
  Linzer::JWS.generate_key(algorithm: algorithm)
91
257
  end
92
258
 
259
+ # Imports a key from JWK (JSON Web Key) format.
260
+ #
261
+ # @param key [Hash] The JWK as a Hash (parsed from JSON)
262
+ # @param params [Hash] Additional key parameters
263
+ # @option params [String] :id Key identifier to use (overrides JWK "kid")
264
+ # @return [JWS::Key] The imported key
265
+ #
266
+ # @example Importing from JWK
267
+ # jwk = JSON.parse(File.read("key.jwk"))
268
+ # key = Linzer.jwk_import(jwk)
93
269
  def jwk_import(key, params = {})
94
270
  Linzer::JWS.jwk_import(key, params)
95
271
  end