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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +1 -1
- data/README.md +3 -1
- data/flake.lock +109 -0
- data/flake.nix +73 -0
- data/lib/linzer/common.rb +51 -0
- data/lib/linzer/ecdsa.rb +51 -0
- data/lib/linzer/ed25519.rb +35 -0
- data/lib/linzer/helper.rb +79 -0
- data/lib/linzer/hmac.rb +47 -1
- data/lib/linzer/http/bootstrap.rb +11 -0
- data/lib/linzer/http/signature_feature.rb +53 -1
- data/lib/linzer/http.rb +54 -0
- data/lib/linzer/jws.rb +74 -0
- data/lib/linzer/key/helper.rb +186 -10
- data/lib/linzer/key.rb +73 -0
- data/lib/linzer/message/adapter/abstract.rb +75 -10
- data/lib/linzer/message/adapter/generic/request.rb +27 -0
- data/lib/linzer/message/adapter/generic/response.rb +17 -0
- data/lib/linzer/message/adapter/http_gem/request.rb +11 -0
- data/lib/linzer/message/adapter/http_gem/response.rb +8 -5
- data/lib/linzer/message/adapter/net_http/request.rb +7 -0
- data/lib/linzer/message/adapter/net_http/response.rb +4 -0
- data/lib/linzer/message/adapter/rack/common.rb +14 -6
- data/lib/linzer/message/adapter/rack/request.rb +13 -0
- data/lib/linzer/message/adapter/rack/response.rb +11 -0
- data/lib/linzer/message/adapter.rb +17 -0
- data/lib/linzer/message/field/parser.rb +14 -0
- data/lib/linzer/message/field.rb +32 -2
- data/lib/linzer/message/wrapper.rb +20 -0
- data/lib/linzer/message.rb +113 -3
- data/lib/linzer/options.rb +13 -0
- data/lib/linzer/rsa.rb +34 -0
- data/lib/linzer/rsa_pss.rb +44 -0
- data/lib/linzer/signature.rb +113 -1
- data/lib/linzer/signer.rb +69 -0
- data/lib/linzer/verifier.rb +52 -0
- data/lib/linzer/version.rb +3 -1
- data/lib/linzer.rb +104 -0
- data/lib/rack/auth/signature.rb +90 -6
- 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
|
-
|
|
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
|
data/lib/linzer/key/helper.rb
CHANGED
|
@@ -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
|
-
#
|
|
17
|
-
#
|
|
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
|
-
#
|
|
34
|
-
#
|
|
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
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
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
|
-
#
|
|
77
|
-
#
|
|
78
|
-
# secp384r1
|
|
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
|