Module: Linzer::MLDSA

Defined in:
lib/linzer/ml_dsa.rb,
lib/linzer/ml_dsa/gem_key.rb,
lib/linzer/ml_dsa/openssl_key.rb

Overview

ML-DSA support for HTTP Message Signatures as specified by https://c2sp.org/httpsig-pq

Two independent backends live under this namespace, both supporting all three FIPS 204 parameter sets (ML-DSA-44/65/87):

  • OpenSSLKey -- backed directly by OpenSSL 3.5+, no extra gem dependency. Always loaded by this file.
  • GemKey -- backed by the ml_dsa gem. Optional: require "linzer/ml_dsa/gem_key" yourself to use it (which requires ml_dsa in turn).

Linzer.generate_ml_dsa_*_key/Linzer.new_ml_dsa_*_key dispatch between them via a backend: keyword (:auto, :openssl, or :ml_dsa), preferring OpenSSL when this build actually supports it.

Defined Under Namespace

Classes: GemKey, OpenSSLKey

Constant Summary collapse

ALGORITHMS =
{
  "ml-dsa-44" => MlDsa::ML_DSA_44,
  "ml-dsa-65" => MlDsa::ML_DSA_65,
  "ml-dsa-87" => MlDsa::ML_DSA_87
}.freeze

Class Method Summary collapse

Class Method Details

.deserialize_raw_or_encoded_key(material, algorithm) ⇒ OpenSSL::PKey::PKey

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Builds an OpenSSL key from ML-DSA material of unknown shape: raw FIPS 204 bytes for algorithm (sniffed by exact byte length, the same approach GemKey uses for the gem backend) or an OpenSSL-encoded PEM/DER key, handled as a fallback. Sniffing is scoped to algorithm's own raw sizes, not all three parameter sets' sizes at once, material sized for a different parameter set than requested falls through to the OpenSSL::PKey.read fallback and fails there (raw bytes aren't valid PEM/DER), rather than silently being accepted and mislabeled.

Parameters:

  • material (String)

    Raw FIPS 204 bytes, or a PEM/DER-encoded key

  • algorithm (String)

    Linzer's lowercase algorithm identifier, e.g. "ml-dsa-44"

Returns:

  • (OpenSSL::PKey::PKey)


214
215
216
217
218
219
220
221
222
223
# File 'lib/linzer/ml_dsa/openssl_key.rb', line 214

def deserialize_raw_or_encoded_key(material, algorithm)
  case material.bytesize
  when RAW_PUBLIC_KEY_BYTES.fetch(algorithm)
    wrap_raw_public_key(material, algorithm)
  when RAW_PRIVATE_KEY_BYTES.fetch(algorithm)
    wrap_raw_private_key(material, algorithm)
  else
    OpenSSL::PKey.read(material)
  end
end

.openssl_supported?(algorithm) ⇒ Boolean

Checks whether this OpenSSL build can actually perform ML-DSA signing/verification for the given algorithm.

Returns true only when the algorithm is both something this OpenSSL-backed implementation has actually implemented (see IMPLEMENTED_ALGORITHMS) and something the underlying OpenSSL library itself supports. This is needed mostly because a version-number check alone isn't reliable and some distributions ship OpenSSL 3.5+ with ML-DSA disabled by crypto policy (see https://github.com/ruby/openssl/issues/1075), so this actually attempts a throwaway key generation rather than inspecting OpenSSL::OPENSSL_VERSION.

Memoized per algorithm after the first check, so repeated calls are free. Unknown or not-yet-implemented algorithm identifiers return false rather than raising.

Parameters:

  • algorithm (String)

    Linzer's lowercase algorithm identifier, e.g. "ml-dsa-44"

Returns:

  • (Boolean)


182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# File 'lib/linzer/ml_dsa/openssl_key.rb', line 182

def openssl_supported?(algorithm)
  cache = (@openssl_supported ||= {})
  return cache[algorithm] if cache.key?(algorithm)

  cache[algorithm] =
    if IMPLEMENTED_ALGORITHMS.include?(algorithm)
      begin
        OpenSSL::PKey.generate_key(algorithm.upcase)
        true
      rescue OpenSSL::PKey::PKeyError
        false
      end
    else
      false
    end
end