linzer 0.8.0 → 0.8.1.beta1
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/.standard.yml +1 -1
- data/CHANGELOG.md +13 -0
- data/README.md +1 -0
- data/lib/linzer/key/helper.rb +93 -0
- data/lib/linzer/ml_dsa.rb +102 -0
- data/lib/linzer/verifier.rb +4 -0
- data/lib/linzer/version.rb +1 -1
- data/lib/linzer.rb +3 -1
- data/lib/rack/auth/signature/helpers.rb +4 -1
- metadata +17 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ced6e7bed237dbb28cefa79ed9289fa19db7510a7078c64315f92d50ae637b7d
|
|
4
|
+
data.tar.gz: 291d1e92359fba0801ad5dfa543e6eb132c43c1f94686be5749462fcf3e18f2e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 968bac61643099b34b73696193f8b5af9ab4489393b7d3d67d4f3fe70c35509c61e48acd659d4e78f101a8fabd7be65b549e87e6376bbaa146ff6cb755b894a8
|
|
7
|
+
data.tar.gz: d901254dd56d886b96b8e4c6425958e63b97841f2682958cb1fd2b020b2799f2a81c15609f74aa8790f80692756d2647f9359f45729ac15f2ca1eaf1e20b0699
|
data/.standard.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.8.1.beta1] - 2026-08-08
|
|
4
|
+
|
|
5
|
+
- Add ML-DSA-44, ML-DSA-65, and ML-DSA-87 support following the C2SP
|
|
6
|
+
post-quantum HTTP Message Signatures profile, including raw, DER, and PEM
|
|
7
|
+
key loading and strict parameter-set binding.
|
|
8
|
+
Pull request [#29](https://github.com/nomadium/linzer/pull/29)
|
|
9
|
+
by [soatok](https://github.com/soatok).
|
|
10
|
+
|
|
11
|
+
- Due to the `ml_dsa` gemspec, the minimum Ruby version was bumped to 2.7.2.
|
|
12
|
+
|
|
3
13
|
## [0.8.0] - 2026-08-08
|
|
4
14
|
|
|
5
15
|
(Only one change since the last beta: the Faraday bug fix below. Everything
|
|
@@ -37,6 +47,9 @@ else shipped in earlier 0.8.0 betas.)
|
|
|
37
47
|
- A `params_encoder` configured on the connection or request is now
|
|
38
48
|
preserved, instead of being replaced with the default encoder.
|
|
39
49
|
|
|
50
|
+
Pull request [#31](https://github.com/nomadium/linzer/pull/31)
|
|
51
|
+
by [Aupajo](https://github.com/Aupajo).
|
|
52
|
+
|
|
40
53
|
## [0.8.0.beta2] - 2026-05-20
|
|
41
54
|
|
|
42
55
|
- Add Web Bot Auth support, implementing the current IETF draft
|
data/README.md
CHANGED
|
@@ -554,6 +554,7 @@ Linzer currently supports the following signature algorithms:
|
|
|
554
554
|
- HMAC-SHA256
|
|
555
555
|
- Ed25519
|
|
556
556
|
- ECDSA (P-256 and P-384 curves).
|
|
557
|
+
- ML-DSA-44, ML-DSA-65, and ML-DSA-87 ([C2SP profile](https://c2sp.org/httpsig-pq))
|
|
557
558
|
|
|
558
559
|
Of the JSON Web Signature (JWS) algorithms mentioned in RFC 9421,
|
|
559
560
|
only Ed25519 is currently supported. Support for additional
|
data/lib/linzer/key/helper.rb
CHANGED
|
@@ -241,6 +241,58 @@ module Linzer
|
|
|
241
241
|
Linzer::ECDSA::Key.new(key, id: key_id, digest: "SHA384")
|
|
242
242
|
end
|
|
243
243
|
|
|
244
|
+
# Generates an ML-DSA-44 key pair.
|
|
245
|
+
#
|
|
246
|
+
# @param key_id [String, nil] Optional key identifier
|
|
247
|
+
# @return [MLDSA::Key] A new ML-DSA-44 key pair
|
|
248
|
+
# @see https://c2sp.org/httpsig-pq C2SP post-quantum HTTP signatures
|
|
249
|
+
def generate_ml_dsa_44_key(key_id = nil)
|
|
250
|
+
generate_ml_dsa_key("ml-dsa-44", key_id)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Generates an ML-DSA-65 key pair.
|
|
254
|
+
#
|
|
255
|
+
# @param key_id [String, nil] Optional key identifier
|
|
256
|
+
# @return [MLDSA::Key] A new ML-DSA-65 key pair
|
|
257
|
+
def generate_ml_dsa_65_key(key_id = nil)
|
|
258
|
+
generate_ml_dsa_key("ml-dsa-65", key_id)
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
# Generates an ML-DSA-87 key pair.
|
|
262
|
+
#
|
|
263
|
+
# @param key_id [String, nil] Optional key identifier
|
|
264
|
+
# @return [MLDSA::Key] A new ML-DSA-87 key pair
|
|
265
|
+
def generate_ml_dsa_87_key(key_id = nil)
|
|
266
|
+
generate_ml_dsa_key("ml-dsa-87", key_id)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
# Loads a raw, DER, or PEM ML-DSA-44 private or public key.
|
|
270
|
+
#
|
|
271
|
+
# @param material [String, MlDsa::SecretKey, MlDsa::PublicKey] Key material
|
|
272
|
+
# @param key_id [String, nil] Optional key identifier
|
|
273
|
+
# @return [MLDSA::Key] The loaded key
|
|
274
|
+
def new_ml_dsa_44_key(material, key_id = nil)
|
|
275
|
+
new_ml_dsa_key(material, "ml-dsa-44", key_id)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Loads a raw, DER, or PEM ML-DSA-65 private or public key.
|
|
279
|
+
#
|
|
280
|
+
# @param material [String, MlDsa::SecretKey, MlDsa::PublicKey] Key material
|
|
281
|
+
# @param key_id [String, nil] Optional key identifier
|
|
282
|
+
# @return [MLDSA::Key] The loaded key
|
|
283
|
+
def new_ml_dsa_65_key(material, key_id = nil)
|
|
284
|
+
new_ml_dsa_key(material, "ml-dsa-65", key_id)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Loads a raw, DER, or PEM ML-DSA-87 private or public key.
|
|
288
|
+
#
|
|
289
|
+
# @param material [String, MlDsa::SecretKey, MlDsa::PublicKey] Key material
|
|
290
|
+
# @param key_id [String, nil] Optional key identifier
|
|
291
|
+
# @return [MLDSA::Key] The loaded key
|
|
292
|
+
def new_ml_dsa_87_key(material, key_id = nil)
|
|
293
|
+
new_ml_dsa_key(material, "ml-dsa-87", key_id)
|
|
294
|
+
end
|
|
295
|
+
|
|
244
296
|
# Generates a new JWS key for the specified algorithm.
|
|
245
297
|
#
|
|
246
298
|
# This method generates keys compatible with JSON Web Signature (JWS)
|
|
@@ -269,6 +321,47 @@ module Linzer
|
|
|
269
321
|
def jwk_import(key, params = {})
|
|
270
322
|
Linzer::JWS.jwk_import(key, params)
|
|
271
323
|
end
|
|
324
|
+
|
|
325
|
+
private
|
|
326
|
+
|
|
327
|
+
def generate_ml_dsa_key(algorithm, key_id)
|
|
328
|
+
parameter_set = Linzer::MLDSA::ALGORITHMS.fetch(algorithm)
|
|
329
|
+
pair = MlDsa.keygen(parameter_set)
|
|
330
|
+
Linzer::MLDSA::Key.new(pair.secret_key, id: key_id, algorithm: algorithm)
|
|
331
|
+
rescue MlDsa::Error, ArgumentError, TypeError => e
|
|
332
|
+
raise Linzer::Error, e.message, cause: e
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def new_ml_dsa_key(material, algorithm, key_id)
|
|
336
|
+
parameter_set = Linzer::MLDSA::ALGORITHMS.fetch(algorithm)
|
|
337
|
+
key = deserialize_ml_dsa_key(material, parameter_set)
|
|
338
|
+
Linzer::MLDSA::Key.new(key, id: key_id, algorithm: algorithm)
|
|
339
|
+
rescue MlDsa::Error, ArgumentError, TypeError => e
|
|
340
|
+
raise Linzer::Error, e.message, cause: e
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def deserialize_ml_dsa_key(material, parameter_set)
|
|
344
|
+
return material if material.is_a?(MlDsa::PublicKey) || material.is_a?(MlDsa::SecretKey)
|
|
345
|
+
unless material.is_a?(String)
|
|
346
|
+
raise TypeError, "ML-DSA key material must be a String, MlDsa::PublicKey, or MlDsa::SecretKey"
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
if material.bytesize == parameter_set.public_key_bytes
|
|
350
|
+
MlDsa::PublicKey.from_bytes(material, parameter_set)
|
|
351
|
+
elsif material.bytesize == parameter_set.secret_key_bytes
|
|
352
|
+
MlDsa::SecretKey.from_bytes(material, parameter_set)
|
|
353
|
+
elsif material.start_with?("-----BEGIN PUBLIC KEY-----")
|
|
354
|
+
MlDsa::PublicKey.from_pem(material)
|
|
355
|
+
elsif material.start_with?("-----BEGIN PRIVATE KEY-----")
|
|
356
|
+
MlDsa::SecretKey.from_pem(material)
|
|
357
|
+
else
|
|
358
|
+
begin
|
|
359
|
+
MlDsa::SecretKey.from_der(material)
|
|
360
|
+
rescue MlDsa::Error::Deserialization
|
|
361
|
+
MlDsa::PublicKey.from_der(material)
|
|
362
|
+
end
|
|
363
|
+
end
|
|
364
|
+
end
|
|
272
365
|
end
|
|
273
366
|
end
|
|
274
367
|
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Linzer
|
|
4
|
+
# ML-DSA support for HTTP Message Signatures as specified by https://c2sp.org/httpsig-pq
|
|
5
|
+
#
|
|
6
|
+
# @see https://c2sp.org/httpsig-pq C2SP post-quantum HTTP signatures
|
|
7
|
+
# @see https://csrc.nist.gov/pubs/fips/204/final FIPS 204
|
|
8
|
+
module MLDSA
|
|
9
|
+
ALGORITHMS = {
|
|
10
|
+
"ml-dsa-44" => MlDsa::ML_DSA_44,
|
|
11
|
+
"ml-dsa-65" => MlDsa::ML_DSA_65,
|
|
12
|
+
"ml-dsa-87" => MlDsa::ML_DSA_87
|
|
13
|
+
}.freeze
|
|
14
|
+
|
|
15
|
+
# An ML-DSA signing or verification key.
|
|
16
|
+
class Key < Linzer::Key
|
|
17
|
+
attr_reader :algorithm
|
|
18
|
+
|
|
19
|
+
def initialize(material, params = {})
|
|
20
|
+
@algorithm = String(params.fetch(:algorithm))
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Validates that the HTTP `alg` parameter matches this key's parameter set.
|
|
25
|
+
#
|
|
26
|
+
# @param parameters [Hash] HTTP signature parameters
|
|
27
|
+
# @return [true] If `alg` is absent or matches this key
|
|
28
|
+
# @raise [VerifyError] If `alg` selects another ML-DSA parameter set
|
|
29
|
+
def validate_signature_parameters(parameters)
|
|
30
|
+
supplied_algorithm = parameters["alg"] || parameters[:alg]
|
|
31
|
+
return true if supplied_algorithm.nil? || supplied_algorithm == algorithm
|
|
32
|
+
|
|
33
|
+
raise VerifyError,
|
|
34
|
+
"Signature algorithm #{supplied_algorithm} does not match key algorithm #{algorithm}"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Signs an RFC 9421 signature base with an empty FIPS 204 context.
|
|
38
|
+
#
|
|
39
|
+
# @param data [String] Signature base bytes
|
|
40
|
+
# @return [String] Raw FIPS 204 signature bytes
|
|
41
|
+
# @raise [SigningError] If private key material is unavailable
|
|
42
|
+
def sign(data)
|
|
43
|
+
validate_signing_key
|
|
44
|
+
material.sign(data, context: "")
|
|
45
|
+
rescue MlDsa::Error => e
|
|
46
|
+
raise SigningError, e.message, cause: e
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Verifies an RFC 9421 signature base with an empty FIPS 204 context.
|
|
50
|
+
#
|
|
51
|
+
# @param signature [String] Raw FIPS 204 signature bytes
|
|
52
|
+
# @param data [String] Signature base bytes
|
|
53
|
+
# @return [Boolean] Whether the signature is valid
|
|
54
|
+
# @raise [VerifyError] If public key material is unavailable
|
|
55
|
+
def verify(signature, data)
|
|
56
|
+
validate_verify_key
|
|
57
|
+
return false unless signature.is_a?(String)
|
|
58
|
+
return false unless signature.bytesize == parameter_set.signature_bytes
|
|
59
|
+
|
|
60
|
+
verification_material.verify(data, signature, context: "")
|
|
61
|
+
rescue MlDsa::Error, ArgumentError, TypeError
|
|
62
|
+
false
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def validate
|
|
68
|
+
super
|
|
69
|
+
expected = ALGORITHMS[algorithm]
|
|
70
|
+
raise Error, "Unsupported ML-DSA algorithm: #{algorithm}" unless expected
|
|
71
|
+
|
|
72
|
+
valid_type = material.is_a?(MlDsa::PublicKey) || material.is_a?(MlDsa::SecretKey)
|
|
73
|
+
raise Error, "Invalid ML-DSA key material" unless valid_type
|
|
74
|
+
|
|
75
|
+
return if material.param_set == expected
|
|
76
|
+
|
|
77
|
+
raise Error,
|
|
78
|
+
"ML-DSA key parameter set #{material.param_set} does not match #{algorithm}"
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def parameter_set
|
|
82
|
+
ALGORITHMS.fetch(algorithm)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def verification_material
|
|
86
|
+
return material if material.is_a?(MlDsa::PublicKey)
|
|
87
|
+
return material.public_key if material.public_key
|
|
88
|
+
|
|
89
|
+
raise VerifyError, "Public key is needed!"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def compute_private?
|
|
93
|
+
material.is_a?(MlDsa::SecretKey)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def compute_public?
|
|
97
|
+
material.is_a?(MlDsa::PublicKey) ||
|
|
98
|
+
(material.is_a?(MlDsa::SecretKey) && !material.public_key.nil?)
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
data/lib/linzer/verifier.rb
CHANGED
|
@@ -85,6 +85,10 @@ module Linzer
|
|
|
85
85
|
raise VerifyError, "Signature raw value to cannot be null" if signature.value.nil?
|
|
86
86
|
raise VerifyError, "Components cannot be null" if signature.serialized_components.nil?
|
|
87
87
|
|
|
88
|
+
if key.respond_to?(:validate_signature_parameters)
|
|
89
|
+
key.validate_signature_parameters(signature.parameters)
|
|
90
|
+
end
|
|
91
|
+
|
|
88
92
|
begin
|
|
89
93
|
validate_components message, signature.serialized_components,
|
|
90
94
|
field_ids: signature.field_ids
|
data/lib/linzer/version.rb
CHANGED
data/lib/linzer.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "starry"
|
|
4
4
|
require "openssl"
|
|
5
|
+
require "ml_dsa"
|
|
5
6
|
require "uri"
|
|
6
7
|
require "net/http"
|
|
7
8
|
|
|
@@ -26,6 +27,7 @@ require_relative "linzer/rsa_pss"
|
|
|
26
27
|
require_relative "linzer/hmac"
|
|
27
28
|
require_relative "linzer/ed25519"
|
|
28
29
|
require_relative "linzer/ecdsa"
|
|
30
|
+
require_relative "linzer/ml_dsa"
|
|
29
31
|
require_relative "linzer/key/helper"
|
|
30
32
|
require_relative "linzer/signer"
|
|
31
33
|
require_relative "linzer/verifier"
|
|
@@ -33,7 +35,7 @@ require_relative "linzer/verifier"
|
|
|
33
35
|
# Linzer is a Ruby library for HTTP Message Signatures as defined in RFC 9421.
|
|
34
36
|
#
|
|
35
37
|
# It provides functionality to sign and verify HTTP messages using various
|
|
36
|
-
# cryptographic algorithms including RSA-PSS, HMAC-SHA256, ECDSA, and
|
|
38
|
+
# cryptographic algorithms including RSA-PSS, HMAC-SHA256, ECDSA, Ed25519, and ML-DSA.
|
|
37
39
|
#
|
|
38
40
|
# @example Signing a request with Ed25519
|
|
39
41
|
# key = Linzer.generate_ed25519_key("my-key-id")
|
|
@@ -193,7 +193,10 @@ module Rack
|
|
|
193
193
|
"hmac-sha256" => :new_hmac_sha256_key,
|
|
194
194
|
"ecdsa-p256-sha256" => :new_ecdsa_p256_sha256_key,
|
|
195
195
|
"ecdsa-p384-sha384" => :new_ecdsa_p384_sha384_key,
|
|
196
|
-
"ed25519" => :new_ed25519_public_key
|
|
196
|
+
"ed25519" => :new_ed25519_public_key,
|
|
197
|
+
"ml-dsa-44" => :new_ml_dsa_44_key,
|
|
198
|
+
"ml-dsa-65" => :new_ml_dsa_65_key,
|
|
199
|
+
"ml-dsa-87" => :new_ml_dsa_87_key
|
|
197
200
|
}
|
|
198
201
|
method = key_methods[alg]
|
|
199
202
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: linzer
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.8.
|
|
4
|
+
version: 0.8.1.beta1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Miguel Landaeta
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0.2'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ml_dsa
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.1'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.1'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: uri
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -162,6 +176,7 @@ files:
|
|
|
162
176
|
- lib/linzer/message/field/parser.rb
|
|
163
177
|
- lib/linzer/message/overlay.rb
|
|
164
178
|
- lib/linzer/message/wrapper.rb
|
|
179
|
+
- lib/linzer/ml_dsa.rb
|
|
165
180
|
- lib/linzer/options.rb
|
|
166
181
|
- lib/linzer/rack.rb
|
|
167
182
|
- lib/linzer/rsa.rb
|
|
@@ -191,7 +206,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
191
206
|
requirements:
|
|
192
207
|
- - ">="
|
|
193
208
|
- !ruby/object:Gem::Version
|
|
194
|
-
version: 2.
|
|
209
|
+
version: 2.7.2
|
|
195
210
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
196
211
|
requirements:
|
|
197
212
|
- - ">="
|