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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 47932906cedb5dd66d7d2bbc5e8eaac9f01b4e17dd7089bc87b4a06b9505b366
4
- data.tar.gz: abe9bf21ac6d66a08f229de3fb10e77bc911c112f812cd0cf620d9f3947d580e
3
+ metadata.gz: ced6e7bed237dbb28cefa79ed9289fa19db7510a7078c64315f92d50ae637b7d
4
+ data.tar.gz: 291d1e92359fba0801ad5dfa543e6eb132c43c1f94686be5749462fcf3e18f2e
5
5
  SHA512:
6
- metadata.gz: b4352492dedad2bd9881b9bc6ac5888cdf7855a798f2b650040a9a64c4f7ea81c34178a4bfadb77f5953d82498f1eea69c90529d8b3888d9d182d3896e8415ed
7
- data.tar.gz: b2540b62b29a07666c66969f5d6843f674994747fe80e47e71c0ba7f03ad554ccfbd1fcfefca796b7115f6fa2643f2b57b4d058de755c499a87ac87f883a4282
6
+ metadata.gz: 968bac61643099b34b73696193f8b5af9ab4489393b7d3d67d4f3fe70c35509c61e48acd659d4e78f101a8fabd7be65b549e87e6376bbaa146ff6cb755b894a8
7
+ data.tar.gz: d901254dd56d886b96b8e4c6425958e63b97841f2682958cb1fd2b020b2799f2a81c15609f74aa8790f80692756d2647f9359f45729ac15f2ca1eaf1e20b0699
data/.standard.yml CHANGED
@@ -1,6 +1,6 @@
1
1
  # For available configuration options, see:
2
2
  # https://github.com/testdouble/standard
3
- ruby_version: 2.6
3
+ ruby_version: 2.7
4
4
 
5
5
  ignore:
6
6
  - '**/*':
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
@@ -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
@@ -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
@@ -3,5 +3,5 @@
3
3
  module Linzer
4
4
  # Current version of the Linzer gem.
5
5
  # @return [String]
6
- VERSION = "0.8.0"
6
+ VERSION = "0.8.1.beta1"
7
7
  end
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 Ed25519.
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.0
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.6.0
209
+ version: 2.7.2
195
210
  required_rubygems_version: !ruby/object:Gem::Requirement
196
211
  requirements:
197
212
  - - ">="