From 06884af716a43bea9d94d30cea92b20cf9a891a4 Mon Sep 17 00:00:00 2001 From: Matteo Abis <1423701+Enucatl@users.noreply.github.com> Date: Wed, 21 Oct 2026 11:12:43 +0200 Subject: [PATCH] [feature] Add ML-DSA keys and mutual TLS Expose ML-DSA-44, ML-DSA-65, and ML-DSA-87 through the existing PKey API, including pure signatures and X.509 operations. Use BC key specs to accept standard seed encodings on BC 1.80 and emit standard expanded PKCS#8 across supported BC versions. Report the exact parameter-set key type so BCJSSE can select a credential. Select the existing TLS provider path for default SSL contexts with ML-DSA credentials. Let Net::HTTP present the identity without Java-specific application code or an SSL method override. Keep cipher queries and their cache consistent with the selected provider. Preserve classical defaults, callback timing, and explicit provider and protocol policies. Add independent OpenSSL fixtures, regression tests, and TLS acceptance coverage on Java 11 with BCJSSE 1.82 and Java 25 with current BC. Run acceptance through a separate task with the required startup properties. Document BC 1.80 key support, BCJSSE 1.82 TLS support, and explicit signature-scheme policy. Test all three ML-DSA server identities with a default SSL context. Add an optional native OpenSSL task to check JRuby key exports and signatures independently for all three parameter sets. --- .github/workflows/ci-bc-compat.yml | 8 + .github/workflows/ci-test.yml | 5 + BUILDING.md | 25 +++ History.md | 6 + README.md | 73 +++++++ Rakefile | 14 ++ integration/mldsa_http.rb | 205 ++++++++++++++++++ integration/mldsa_openssl.rb | 42 ++++ pom.xml | 1 + src/main/java/org/jruby/ext/openssl/ASN1.java | 4 + src/main/java/org/jruby/ext/openssl/PKey.java | 15 ++ .../java/org/jruby/ext/openssl/PKeyMLDSA.java | 165 ++++++++++++++ .../org/jruby/ext/openssl/SSLContext.java | 35 ++- .../java/org/jruby/ext/openssl/X509CRL.java | 4 +- .../java/org/jruby/ext/openssl/X509Cert.java | 7 +- .../org/jruby/ext/openssl/X509Request.java | 11 +- .../jruby/ext/openssl/impl/PKCS10Request.java | 5 + .../java/org/jruby/ext/openssl/impl/PKey.java | 58 ++++- .../ext/openssl/x509store/PEMInputOutput.java | 6 + .../org/jruby/ext/openssl/SSLContextTest.java | 129 +++++++++++ test/fixtures/mldsa/README.md | 54 +++++ test/fixtures/mldsa/binary.bin | Bin 0 -> 21 bytes test/fixtures/mldsa/empty.bin | 0 test/fixtures/mldsa/generate.sh | 41 ++++ test/fixtures/mldsa/ml-dsa-44-binary.sig | Bin 0 -> 2420 bytes test/fixtures/mldsa/ml-dsa-44-empty.sig | Bin 0 -> 2420 bytes test/fixtures/mldsa/ml-dsa-44-encrypted.pem | 59 +++++ test/fixtures/mldsa/ml-dsa-44-expanded.pem | 56 +++++ test/fixtures/mldsa/ml-dsa-44-public.pem | 30 +++ test/fixtures/mldsa/ml-dsa-44-seed-priv.pem | 57 +++++ test/fixtures/mldsa/ml-dsa-44-seed.pem | 4 + test/fixtures/mldsa/ml-dsa-65-binary.sig | Bin 0 -> 3309 bytes test/fixtures/mldsa/ml-dsa-65-empty.sig | Bin 0 -> 3309 bytes test/fixtures/mldsa/ml-dsa-65-encrypted.pem | 89 ++++++++ test/fixtures/mldsa/ml-dsa-65-expanded.pem | 87 ++++++++ test/fixtures/mldsa/ml-dsa-65-public.pem | 44 ++++ test/fixtures/mldsa/ml-dsa-65-seed-priv.pem | 88 ++++++++ test/fixtures/mldsa/ml-dsa-65-seed.pem | 4 + test/fixtures/mldsa/ml-dsa-87-binary.sig | Bin 0 -> 4627 bytes test/fixtures/mldsa/ml-dsa-87-empty.sig | Bin 0 -> 4627 bytes test/fixtures/mldsa/ml-dsa-87-encrypted.pem | 107 +++++++++ test/fixtures/mldsa/ml-dsa-87-expanded.pem | 105 +++++++++ test/fixtures/mldsa/ml-dsa-87-public.pem | 57 +++++ test/fixtures/mldsa/ml-dsa-87-seed-priv.pem | 106 +++++++++ test/fixtures/mldsa/ml-dsa-87-seed.pem | 4 + test/pkey/test_pkey_mldsa.rb | 179 +++++++++++++++ test/test_x509_mldsa.rb | 131 +++++++++++ 47 files changed, 2106 insertions(+), 14 deletions(-) create mode 100644 integration/mldsa_http.rb create mode 100644 integration/mldsa_openssl.rb create mode 100644 src/main/java/org/jruby/ext/openssl/PKeyMLDSA.java create mode 100644 src/test/java/org/jruby/ext/openssl/SSLContextTest.java create mode 100644 test/fixtures/mldsa/README.md create mode 100644 test/fixtures/mldsa/binary.bin create mode 100644 test/fixtures/mldsa/empty.bin create mode 100755 test/fixtures/mldsa/generate.sh create mode 100644 test/fixtures/mldsa/ml-dsa-44-binary.sig create mode 100644 test/fixtures/mldsa/ml-dsa-44-empty.sig create mode 100644 test/fixtures/mldsa/ml-dsa-44-encrypted.pem create mode 100644 test/fixtures/mldsa/ml-dsa-44-expanded.pem create mode 100644 test/fixtures/mldsa/ml-dsa-44-public.pem create mode 100644 test/fixtures/mldsa/ml-dsa-44-seed-priv.pem create mode 100644 test/fixtures/mldsa/ml-dsa-44-seed.pem create mode 100644 test/fixtures/mldsa/ml-dsa-65-binary.sig create mode 100644 test/fixtures/mldsa/ml-dsa-65-empty.sig create mode 100644 test/fixtures/mldsa/ml-dsa-65-encrypted.pem create mode 100644 test/fixtures/mldsa/ml-dsa-65-expanded.pem create mode 100644 test/fixtures/mldsa/ml-dsa-65-public.pem create mode 100644 test/fixtures/mldsa/ml-dsa-65-seed-priv.pem create mode 100644 test/fixtures/mldsa/ml-dsa-65-seed.pem create mode 100644 test/fixtures/mldsa/ml-dsa-87-binary.sig create mode 100644 test/fixtures/mldsa/ml-dsa-87-empty.sig create mode 100644 test/fixtures/mldsa/ml-dsa-87-encrypted.pem create mode 100644 test/fixtures/mldsa/ml-dsa-87-expanded.pem create mode 100644 test/fixtures/mldsa/ml-dsa-87-public.pem create mode 100644 test/fixtures/mldsa/ml-dsa-87-seed-priv.pem create mode 100644 test/fixtures/mldsa/ml-dsa-87-seed.pem create mode 100644 test/pkey/test_pkey_mldsa.rb create mode 100644 test/test_x509_mldsa.rb diff --git a/.github/workflows/ci-bc-compat.yml b/.github/workflows/ci-bc-compat.yml index 34d7869bb..6a7da4057 100644 --- a/.github/workflows/ci-bc-compat.yml +++ b/.github/workflows/ci-bc-compat.yml @@ -48,3 +48,11 @@ jobs: - name: invoker test (BC ${{ matrix.bc-version }}) run: ./mvnw -ntp test -f src/it/${{ matrix.bc-version }}/pom.xml timeout-minutes: 15 + + - name: ML-DSA Net::HTTP mutual TLS acceptance (BCJSSE 1.82 / Java 11) + if: matrix.bc-version == '1.82' && matrix.java-version == 11 + env: + JAVA_TOOL_OPTIONS: '-Djruby.openssl.load.jars=false' + CLASSPATH: 'src/it/1.82/target/dependency/*' + run: jruby -rbundler/setup -S rake test:mldsa_tls + timeout-minutes: 5 diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index dc1d0aabe..0a60c815e 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -102,3 +102,8 @@ jobs: - name: rake ${{ matrix.test-target }} run: jruby -rbundler/setup -S rake ${{ matrix.test-target }} timeout-minutes: 15 + + - name: ML-DSA Net::HTTP mutual TLS acceptance + if: matrix.runtime.java == 25 && matrix.test-target == 'test' + run: jruby -rbundler/setup -S rake test:mldsa_tls + timeout-minutes: 5 diff --git a/BUILDING.md b/BUILDING.md index 939e63f25..61273c24f 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -32,6 +32,31 @@ git submodule update --init ruby-openssl jruby -rbundler/setup -S rake test:mri ``` +The local ML-DSA mutual TLS acceptance test requires BCJSSE 1.82 or later +with its ML-DSA signature schemes explicitly enabled. This task starts a +separate JRuby process with the existing JSSE properties set before provider +initialization: + +``` +jruby -rbundler/setup -S rake test:mldsa_tls +``` + +It performs actual Net::HTTP GET and POST requests for all three parameter +sets without selecting a client SSL method. It checks the server's authenticated +peer certificate and TLS version, and exercises invalid server and client +identities. It also checks ML-DSA server identities with a default server context. +CI runs it on Java 25 with the default BC version and on Java 11 +with BCJSSE 1.82; the task does not skip the deployment acceptance test. +The key and X.509 regressions also run with the regular unit suite. + +An optional interoperability test checks that native OpenSSL accepts keys and +signatures produced by JRuby for all three ML-DSA parameter sets. It requires +`openssl` 3.5 or later on `PATH` and runs separately from the regular suite: + +``` +jruby -rbundler/setup -S rake test:mldsa_openssl +``` + ### Releasing * fill in [History.md](History.md) change-log entries for release diff --git a/History.md b/History.md index fb8a91903..5cb16a672 100644 --- a/History.md +++ b/History.md @@ -1,3 +1,9 @@ +## 0.19.4 (unreleased) + +- [feat] add ML-DSA-44/65/87 keys, signatures and X.509 operations with BC 1.80+. + Mutual TLS through Net::HTTP requires BCJSSE 1.82+ and explicitly enabled + ML-DSA signature schemes; Java requirements remain unchanged + ## 0.19.3 Broader OpenSSL compatibility across PKCS7, X.509, key handling and big numbers, diff --git a/README.md b/README.md index a317e16c0..88bd7900c 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,79 @@ For older versions of the gem compatibility wasn't reported: | ~>0.15.x | 9.2.x-10.0.x | Java 8-25 | 1.78-1.83 | | ~>0.16.x | 9.3.x-10.1.x | Java 8-25 | 1.83-1.86 | +### ML-DSA + +| Feature | Minimum provider version | +|---------|--------------------------| +| ML-DSA keys, signatures, certificates, CSRs and CRLs | BC 1.80 | +| ML-DSA mutual TLS | BCJSSE 1.82 | + +Java requirements remain those of the selected JRuby version; Java 25 is not +required. BCJSSE's ML-DSA signature schemes must be explicitly enabled before +provider initialization. + +Pure ML-DSA-44, ML-DSA-65, and ML-DSA-87 keys use the existing generic +`OpenSSL::PKey::PKey` API: + +```ruby +key = OpenSSL::PKey.generate_key('ML-DSA-65') +public_key = OpenSSL::PKey.read(key.public_to_pem) +signature = key.sign(nil, message) +public_key.verify(nil, signature, message) +``` + +`OpenSSL::PKey.read` accepts PEM/DER SubjectPublicKeyInfo and PKCS#8, including +password-encrypted PKCS#8. Private keys derive their public key through BC. +`public_to_pem` / `public_to_der` export SubjectPublicKeyInfo; +`private_to_pem` / `private_to_der` export unencrypted expanded-only PKCS#8. `oid` reports the +OpenSSL short name (`id-ml-dsa-65`); certificate, CSR, and CRL signature names +are `ML-DSA-65`. All three parameter sets support certificate, CSR, and CRL +signing with a `nil` digest, verification, and PEM/DER serialization. + +Signatures use pure ML-DSA with an empty context. Explicit digests, HashML-DSA, +OpenSSL signature/key-generation options, raw-key operations, key agreement, +and encrypted private-key export are outside this implementation. PKCS#8 import +supports standard seed-only, seed-and-expanded, and expanded-only keys. +Prestandard bare encodings emitted by BC 1.80 are not accepted. Inconsistent +seed/expanded or private/public components are rejected; unlike OpenSSL's +default, contradictory expanded data is not ignored in favor of the seed. See the +independent [test fixtures](test/fixtures/mldsa/README.md). + +For Net::HTTP client authentication, a default SSL context with an ML-DSA +credential automatically uses the existing TLS provider selection, normally +BCJSSE. No Java-specific application code or `ssl_version` setting is needed. +BCJSSE disables its ML-DSA signature schemes by default; enable them before +provider initialization. For example, start JRuby with the existing JSSE +properties (the classical schemes below allow an RSA server certificate): + +```sh +JRUBY_OPTS='-J-Djdk.tls.client.SignatureSchemes=mldsa44,mldsa65,mldsa87,rsa_pss_rsae_sha256,rsa_pkcs1_sha256' jruby client.rb +``` + +```ruby +require 'net/http' +require 'openssl' + +http = Net::HTTP.new('vault.example', 443) +http.use_ssl = true +http.min_version = http.max_version = OpenSSL::SSL::TLS1_3_VERSION +http.verify_mode = OpenSSL::SSL::VERIFY_PEER +http.verify_hostname = true +http.ca_file = 'ca.pem' +http.cert = OpenSSL::X509::Certificate.new(File.read('client.pem')) +http.key = OpenSSL::PKey.read(File.read('client-key.pem')) +response = http.get('/v1/example') +``` + +The peer must also enable the corresponding ML-DSA client-authentication +signature schemes. A JRuby BCJSSE server uses the existing +`jdk.tls.server.SignatureSchemes` property and `SSLContext.new(:TLS)`. +Contexts without an ML-DSA credential retain their existing provider selection. +Explicit SSL methods, protocol bounds, and the `jruby.openssl.provider.ssl` +property remain effective. TLS support depends on both peers and their policy. +ML-DSA support in the separately distributed FIPS variant is not established +by these regular-BC tests. + ## Security JRuby-OpenSSL is an essential part of [JRuby](https://www.jruby.org/), please report security vulnerabilities to diff --git a/Rakefile b/Rakefile index c1dbd1cd6..2356862c2 100644 --- a/Rakefile +++ b/Rakefile @@ -100,6 +100,20 @@ define_mri_test_task require_relative 'tasks/provider_tests' namespace :test do + desc 'Check JRuby ML-DSA keys and signatures with native OpenSSL 3.5 or later' + task :mldsa_openssl => 'lib/jopenssl.jar' do + ruby '-rbundler/setup -Ilib:test integration/mldsa_openssl.rb' + end + + desc 'Run local ML-DSA Net::HTTP TLS 1.3 mutual authentication tests with BCJSSE' + task :mldsa_tls => 'lib/jopenssl.jar' do + # BCJSSE disables standalone PQ signature schemes by default. + schemes = 'mldsa44,mldsa65,mldsa87,rsa_pss_rsae_sha256,rsa_pkcs1_sha256' + ruby "-J-Djdk.tls.client.SignatureSchemes=#{schemes} " \ + "-J-Djdk.tls.server.SignatureSchemes=#{schemes} " \ + '-rbundler/setup -Ilib:test integration/mldsa_http.rb' + end + namespace :provider do define_provider_test_task :bc_all, description: 'Run tests with BC providers registered', diff --git a/integration/mldsa_http.rb b/integration/mldsa_http.rb new file mode 100644 index 000000000..de2821147 --- /dev/null +++ b/integration/mldsa_http.rb @@ -0,0 +1,205 @@ +require_relative '../test/test_helper' +require 'net/http' +require 'socket' +require 'timeout' + +class TestMLDSAHTTP < TestCase + def setup + super + @ca_key = Fixtures.pkey('rsa2048') + @ca = issue_cert(distinguished_name('trusted CA'), @ca_key, 1, + [['basicConstraints', 'CA:TRUE', true], + ['keyUsage', 'keyCertSign,cRLSign', true]], nil, nil) + @server_key = Fixtures.pkey('rsa2048') + @server_cert = server_certificate + end + + def test_get_and_post_authenticate_each_mldsa_parameter_set + [44, 65, 87].each do |parameter| + # Exercise imported keys as Net::HTTP users do, including public derivation. + path = File.expand_path("../test/fixtures/mldsa/ml-dsa-#{parameter}-seed.pem", File.dirname(__FILE__)) + key = OpenSSL::PKey.read(File.binread(path)) + cert = client_certificate(key) + [Net::HTTP::Get.new('/vault'), Net::HTTP::Post.new('/vault')].each do |request| + request.body = "binary\0payload\xff".b if request.is_a?(Net::HTTP::Post) + with_server do |port, result| + response = http(port, cert, key).start { |connection| connection.request(request) } + assert_equal '200', response.code + peer_der, protocol, method, body = Timeout.timeout(15) { result.pop } + raise peer_der if peer_der.is_a?(Exception) + assert_equal cert.to_der, peer_der, 'server must authenticate the intended certificate' + assert_equal 'TLSv1.3', protocol + assert_equal request.method, method + assert_equal request.body.to_s, body + assert_equal cert.subject.to_s, response.body + end + end + end + end + + def test_rejects_untrusted_server_ca + key = OpenSSL::PKey.generate_key('ML-DSA-44') + with_server do |port, result| + connection = http(port, client_certificate(key), key) + connection.cert_store = OpenSSL::X509::Store.new + assert_raise(OpenSSL::SSL::SSLError) { connection.get('/vault') } + assert_server_rejected(result) + end + end + + def test_mldsa_server_identity_with_default_context + client_key = OpenSSL::PKey.generate_key('ML-DSA-44') + client_cert = client_certificate(client_key) + [44, 65, 87].each do |parameter| + @server_key = OpenSSL::PKey.generate_key("ML-DSA-#{parameter}") + @server_cert = server_certificate + with_server(context: OpenSSL::SSL::SSLContext.new) do |port, result| + http(port, client_cert, client_key).start do |connection| + assert_equal @server_cert.to_der, connection.peer_cert.to_der + assert_equal '200', connection.get('/vault').code + end + peer_der, protocol = Timeout.timeout(15) { result.pop } + raise peer_der if peer_der.is_a?(Exception) + assert_equal client_cert.to_der, peer_der + assert_equal 'TLSv1.3', protocol + end + end + end + + def test_rejects_wrong_hostname + key = OpenSSL::PKey.generate_key('ML-DSA-44') + with_server(server_certificate('wrong.example')) do |port, result| + assert_raise(OpenSSL::SSL::SSLError) { http(port, client_certificate(key), key).get('/vault') } + assert_server_rejected(result) + end + end + + def test_rejects_expired_server_certificate + key = OpenSSL::PKey.generate_key('ML-DSA-44') + with_server(server_certificate('localhost', expired: true)) do |port, result| + assert_raise(OpenSSL::SSL::SSLError) { http(port, client_certificate(key), key).get('/vault') } + assert_server_rejected(result) + end + end + + def test_rejects_absent_client_identity + with_server do |port, result| + assert_raise(OpenSSL::SSL::SSLError) { http(port, nil, nil).get('/vault') } + assert_server_rejected(result) + end + end + + def test_rejects_untrusted_client_identity + key = OpenSSL::PKey.generate_key('ML-DSA-44') + cert = issue_cert(distinguished_name('untrusted client'), key, 99, + [['keyUsage', 'digitalSignature', true]], nil, nil, digest: nil) + with_server do |port, result| + assert_raise(OpenSSL::SSL::SSLError) { http(port, cert, key).get('/vault') } + assert_server_rejected(result) + end + end + + def test_rejects_expired_client_identity + key = OpenSSL::PKey.generate_key('ML-DSA-44') + cert = client_certificate(key, expired: true) + store = trust_store + assert_equal false, store.verify(cert) + assert_equal OpenSSL::X509::V_ERR_CERT_HAS_EXPIRED, store.error + with_server do |port, result| + assert_raise(OpenSSL::SSL::SSLError, Errno::ECONNRESET) { http(port, cert, key).get('/vault') } + assert_server_rejected(result) + end + end + + def test_rejects_certificate_private_key_mismatch + key = OpenSSL::PKey.generate_key('ML-DSA-44') + other = OpenSSL::PKey.generate_key('ML-DSA-44') + with_server do |port, result| + assert_raise(OpenSSL::SSL::SSLError, Errno::ECONNRESET) { http(port, client_certificate(key), other).get('/vault') } + assert_server_rejected(result) + end + end + + private + + def distinguished_name(value) + OpenSSL::X509::Name.new([['CN', value]]) + end + + def server_certificate(host = 'localhost', expired: false) + issue_cert(distinguished_name(host), @server_key, 2, + [['keyUsage', 'digitalSignature', true], + ['extendedKeyUsage', 'serverAuth', false], + ['subjectAltName', "DNS:#{host}", false]], @ca, @ca_key, + not_after: expired ? Time.now - 60 : Time.now + 3600) + end + + def client_certificate(key, expired: false) + issue_cert(distinguished_name("client #{key.oid}"), key, 3, + [['keyUsage', 'digitalSignature', true], + ['extendedKeyUsage', 'clientAuth', false]], @ca, @ca_key, + not_after: expired ? Time.now - 60 : Time.now + 3600) + end + + def trust_store + OpenSSL::X509::Store.new.tap { |store| store.add_cert(@ca) } + end + + def http(port, cert, key) + Net::HTTP.new('localhost', port, nil).tap do |connection| + connection.use_ssl = true + connection.min_version = connection.max_version = OpenSSL::SSL::TLS1_3_VERSION + connection.verify_mode = OpenSSL::SSL::VERIFY_PEER + connection.verify_hostname = true + connection.cert_store = trust_store + connection.cert = cert + connection.key = key + connection.open_timeout = connection.read_timeout = 10 + connection.max_retries = 0 + end + end + + def with_server(cert = @server_cert, context: OpenSSL::SSL::SSLContext.new(:TLS)) + context.cert = cert + context.key = @server_key + context.cert_store = trust_store + context.verify_mode = OpenSSL::SSL::VERIFY_PEER | OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT + context.min_version = context.max_version = OpenSSL::SSL::TLS1_3_VERSION + tcp = TCPServer.new('127.0.0.1', 0) + server = OpenSSL::SSL::SSLServer.new(tcp, context) + result = Queue.new + thread = Thread.new do + begin + ssl = server.accept + request_line = ssl.gets + raise EOFError, 'client closed before sending an HTTP request' unless request_line + method = request_line.split.first + length = 0 + while (line = ssl.gets) && line != "\r\n" + length = line.split(':', 2).last.to_i if line.downcase.start_with?('content-length:') + end + body = length.zero? ? '' : ssl.read(length) + peer = ssl.peer_cert + result << [peer.to_der, ssl.ssl_version, method, body] + response = peer.subject.to_s + ssl.write("HTTP/1.1 200 OK\r\nContent-Length: #{response.bytesize}\r\nConnection: close\r\n\r\n#{response}") + rescue => error + result << [error] + ensure + ssl.close if ssl + end + end + yield tcp.addr[1], result + ensure + tcp.close if tcp && !tcp.closed? + if thread + thread.kill unless thread.join(15) + thread.join + end + end + + def assert_server_rejected(result) + outcome = Timeout.timeout(15) { result.pop } + assert_kind_of Exception, outcome.first + end +end diff --git a/integration/mldsa_openssl.rb b/integration/mldsa_openssl.rb new file mode 100644 index 000000000..c5845697a --- /dev/null +++ b/integration/mldsa_openssl.rb @@ -0,0 +1,42 @@ +require_relative '../test/test_helper' +require 'open3' +require 'tmpdir' + +class TestMLDSAOpenSSL < TestCase + def test_native_openssl_accepts_jruby_keys_and_signatures + version = openssl('version') + number = version[/\AOpenSSL (\d+\.\d+\.\d+)/, 1] + assert(number && Gem::Version.new(number) >= Gem::Version.new('3.5.0'), + "OpenSSL 3.5 or later is required; found #{version.strip}") + message = File.expand_path('../test/fixtures/mldsa/binary.bin', File.dirname(__FILE__)) + + Dir.mktmpdir('mldsa-openssl') do |directory| + private_path = File.join(directory, 'private.der') + public_path = File.join(directory, 'public.der') + signature_path = File.join(directory, 'signature.bin') + [44, 65, 87].each do |parameter| + key = OpenSSL::PKey.generate_key("ML-DSA-#{parameter}") + File.binwrite(private_path, key.private_to_der) + File.binwrite(public_path, key.public_to_der) + File.binwrite(signature_path, key.sign(nil, File.binread(message))) + + assert_equal key.public_to_der, + openssl('pkey', '-inform', 'DER', '-in', private_path, '-pubout', '-outform', 'DER') + assert_equal key.public_to_der, + openssl('pkey', '-pubin', '-inform', 'DER', '-in', public_path, '-pubout', '-outform', 'DER') + openssl('pkeyutl', '-verify', '-pubin', '-keyform', 'DER', '-inkey', public_path, + '-in', message, '-sigfile', signature_path) + end + end + end + + private + + def openssl(*arguments) + output, error, status = Open3.capture3('openssl', *arguments, binmode: true) + assert status.success?, "openssl #{arguments.join(' ')} failed: #{error}#{output}" + output + rescue Errno::ENOENT + flunk 'OpenSSL 3.5 or later must be installed and available on PATH' + end +end diff --git a/pom.xml b/pom.xml index 04cf8e881..642984147 100644 --- a/pom.xml +++ b/pom.xml @@ -1041,6 +1041,7 @@ DO NOT MODIFY - GENERATED CODE org/jruby/ext/openssl/PKeyEC.java org/jruby/ext/openssl/PKeyEdDSA.java org/jruby/ext/openssl/PKeyHMAC.java + org/jruby/ext/openssl/PKeyMLDSA.java org/jruby/ext/openssl/PKeyRSA.java org/jruby/ext/openssl/PKeyXDH.java org/jruby/ext/openssl/Random.java diff --git a/src/main/java/org/jruby/ext/openssl/ASN1.java b/src/main/java/org/jruby/ext/openssl/ASN1.java index 533439ac8..4518c97bf 100644 --- a/src/main/java/org/jruby/ext/openssl/ASN1.java +++ b/src/main/java/org/jruby/ext/openssl/ASN1.java @@ -297,6 +297,10 @@ private static void defaultObjects(final Ruby runtime) { addObject(runtime, 1087, "ED25519", "ED25519", "1.3.101.112"); addObject(runtime, 1088, "ED448", "ED448", "1.3.101.113"); + addObject(runtime, 1457, "id-ml-dsa-44", "ML-DSA-44", "2.16.840.1.101.3.4.3.17"); + addObject(runtime, 1458, "id-ml-dsa-65", "ML-DSA-65", "2.16.840.1.101.3.4.3.18"); + addObject(runtime, 1459, "id-ml-dsa-87", "ML-DSA-87", "2.16.840.1.101.3.4.3.19"); + addObject(runtime, 660, "street", "streetAddress", "2.5.4.9"); addObject(runtime, 391, "DC", "domainComponent", "0.9.2342.19200300.100.1.25"); //addObject(runtime, 509, null, "generationQualifier", "2.5.4.44"); diff --git a/src/main/java/org/jruby/ext/openssl/PKey.java b/src/main/java/org/jruby/ext/openssl/PKey.java index 25b9008cf..a7685d22c 100644 --- a/src/main/java/org/jruby/ext/openssl/PKey.java +++ b/src/main/java/org/jruby/ext/openssl/PKey.java @@ -122,6 +122,9 @@ public static PKey newInstance(final Ruby runtime, final PublicKey publicKey) { if (PKeyXDH.isXDHKey(publicKey)) { return new PKeyXDH(runtime, publicKey); } + if (PKeyMLDSA.isMLDSAAlgorithm(publicKey.getAlgorithm())) { + return new PKeyMLDSA(runtime, publicKey, null); + } if (publicKey instanceof DHPublicKey) { return new PKeyDH(runtime, (DHPublicKey) publicKey, null); } @@ -203,6 +206,9 @@ private static IRubyObject readImpl(final ThreadContext context, IRubyObject... if ( PKeyXDH.isXDHAlgorithm(alg) ) { return PKeyXDH.newInstance(runtime, keyPair); } + if ( PKeyMLDSA.isMLDSAAlgorithm(alg) ) { + return new PKeyMLDSA(runtime, keyPair.getPublic(), keyPair.getPrivate()); + } if ( "DH".equals(alg) ) { return new PKeyDH(runtime, (DHPublicKey) keyPair.getPublic(), (DHPrivateKey) keyPair.getPrivate()); } @@ -254,6 +260,9 @@ private static IRubyObject readImpl(final ThreadContext context, IRubyObject... if (PKeyXDH.isXDHKey(pubKey)) { return new PKeyXDH(runtime, pubKey); } + if (pubKey != null && PKeyMLDSA.isMLDSAAlgorithm(pubKey.getAlgorithm())) { + return new PKeyMLDSA(runtime, pubKey, null); + } if (pubKey instanceof DHPublicKey) { return new PKeyDH(runtime, (DHPublicKey) pubKey, null); } @@ -294,6 +303,12 @@ public static IRubyObject generate_key(final ThreadContext context, IRubyObject if ( PKeyXDH.isXDHAlgorithm(algorithm) ) { return PKeyXDH.generate(context, algorithm); } + if ( PKeyMLDSA.isMLDSAAlgorithm(algorithm) ) { + if (!options.isNil() && !options.convertToHash().isEmpty()) { + throw newPKeyError(runtime, "ML-DSA key generation options are not supported"); + } + return PKeyMLDSA.generate(context, algorithm); + } if ( "RSA".equalsIgnoreCase(algorithm) ) { return generateRSAKey(context, options); } diff --git a/src/main/java/org/jruby/ext/openssl/PKeyMLDSA.java b/src/main/java/org/jruby/ext/openssl/PKeyMLDSA.java new file mode 100644 index 000000000..37ad5a637 --- /dev/null +++ b/src/main/java/org/jruby/ext/openssl/PKeyMLDSA.java @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2026 JRuby contributors. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + */ +package org.jruby.ext.openssl; + +import java.io.IOException; +import java.io.StringWriter; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.SignatureException; + +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; +import org.bouncycastle.asn1.DEROctetString; +import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; +import org.bouncycastle.jcajce.interfaces.MLDSAPrivateKey; +import org.bouncycastle.jcajce.spec.MLDSAParameterSpec; +import org.jruby.Ruby; +import org.jruby.RubyString; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.ext.openssl.x509store.PEMInputOutput; +import org.jruby.util.ByteList; + +import static org.jruby.ext.openssl.OpenSSL.throwOperationErrorOrOriginal; +import static org.jruby.ext.openssl.util.RubySupport.newString; + +/** Pure ML-DSA keys, exposed as OpenSSL::PKey::PKey. */ +public class PKeyMLDSA extends PKey { + private final PublicKey publicKey; + private final PrivateKey privateKey; + + PKeyMLDSA(Ruby runtime, PublicKey publicKey, PrivateKey privateKey) { + super(runtime, PKey._PKey(runtime).getClass("PKey")); + this.publicKey = publicKey; + this.privateKey = privateKey; + final int length = SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()).getPublicKeyData().getOctets().length; + final int expected = "ML-DSA-44".equals(publicKey.getAlgorithm()) ? 1312 : + "ML-DSA-65".equals(publicKey.getAlgorithm()) ? 1952 : 2592; + if (length != expected) throw newPKeyError(runtime, "invalid ML-DSA public key length"); + } + + static PKeyMLDSA generate(ThreadContext context, String algorithm) { + try { + KeyPairGenerator generator = SecurityHelper.getKeyPairGenerator("ML-DSA"); + generator.initialize(MLDSAParameterSpec.fromName(algorithm), OpenSSL.getSecureRandom(context)); + KeyPair pair = generator.generateKeyPair(); + return new PKeyMLDSA(context.runtime, pair.getPublic(), pair.getPrivate()); + } + catch (GeneralSecurityException | IllegalArgumentException e) { + throw newPKeyError(context.runtime, e); + } + } + + static boolean isMLDSAAlgorithm(String algorithm) { + return "ML-DSA-44".equalsIgnoreCase(algorithm) || + "ML-DSA-65".equalsIgnoreCase(algorithm) || + "ML-DSA-87".equalsIgnoreCase(algorithm); + } + + @Override + public PublicKey getPublicKey() { return publicKey; } + + @Override + public PrivateKey getPrivateKey() { return privateKey; } + + @Override + public String getAlgorithm() { + return privateKey != null ? privateKey.getAlgorithm() : publicKey.getAlgorithm(); + } + + // BCJSSE requests the exact parameter set when selecting a TLS credential. + @Override + public String getKeyType() { return getAlgorithm(); } + + @Override + public RubyString oid() { + return getRuntime().newString("id-ml-dsa-" + getAlgorithm().substring(7)); + } + + @Override + public IRubyObject sign(IRubyObject digest, IRubyObject data) { + final Ruby runtime = getRuntime(); + if (!isPrivateKey()) throw runtime.newArgumentError("Private key is needed."); + if (!digest.isNil()) throw newPKeyError(runtime, "ML-DSA does not support digests"); + try { + return RubyString.newString(runtime, sign(getAlgorithm(), privateKey, data.convertToString().getByteList())); + } + catch (GeneralSecurityException e) { + throw newPKeyError(runtime, e); + } + catch (Throwable e) { + return throwOperationErrorOrOriginal(runtime, e); + } + } + + @Override + public IRubyObject verify(IRubyObject digest, IRubyObject signature, IRubyObject data) { + final Ruby runtime = getRuntime(); + if (!digest.isNil()) throw newPKeyError(runtime, "ML-DSA does not support digests"); + try { + ByteList message = data.convertToString().getByteList(); + ByteList bytes = signature.convertToString().getByteList(); + return runtime.newBoolean(verify(getAlgorithm(), publicKey, message, bytes)); + } + catch (SignatureException e) { + return runtime.getFalse(); + } + catch (GeneralSecurityException e) { + throw newPKeyError(runtime, e); + } + catch (Throwable e) { + return throwOperationErrorOrOriginal(runtime, e); + } + } + + @Override + public RubyString to_der() { + return privateKey != null ? private_to_der(getRuntime().getCurrentContext(), IRubyObject.NULL_ARRAY) : + newString(getRuntime(), publicKey.getEncoded()); + } + + @Override + public RubyString private_to_der(ThreadContext context, IRubyObject[] args) { + if (privateKey == null || args.length > 0) return super.private_to_der(context, args); + try { + // Always emit standard expanded-only PKCS#8, including with BC 1.80's prestandard encoder. + PrivateKeyInfo info = new PrivateKeyInfo( + SubjectPublicKeyInfo.getInstance(publicKey.getEncoded()).getAlgorithm(), + new DEROctetString(((MLDSAPrivateKey) privateKey).getPrivateData())); + return newString(context.runtime, info.getEncoded()); + } + catch (IOException e) { + throw newPKeyError(context.runtime, e); + } + } + + @Override + public RubyString private_to_pem(ThreadContext context, IRubyObject[] args) { + try { + StringWriter writer = new StringWriter(); + PEMInputOutput.writePKCS8PrivateKey(writer, private_to_der(context, args).getBytes()); + return context.runtime.newString(writer.toString()); + } + catch (IOException e) { + throw newPKeyError(context.runtime, e); + } + } + + @Override + public RubyString to_pem(ThreadContext context, IRubyObject[] args) { + return privateKey != null ? private_to_pem(context, args) : public_to_pem(context); + } + + @Override + public RubyString to_text() { + return getRuntime().newString(getAlgorithm() + (privateKey != null ? " Private-Key\n" : " Public-Key\n")); + } +} diff --git a/src/main/java/org/jruby/ext/openssl/SSLContext.java b/src/main/java/org/jruby/ext/openssl/SSLContext.java index 18fbf4157..6fc42d039 100644 --- a/src/main/java/org/jruby/ext/openssl/SSLContext.java +++ b/src/main/java/org/jruby/ext/openssl/SSLContext.java @@ -583,18 +583,19 @@ public RubyArray ciphers(final ThreadContext context) { // SSL_CTX_get_ciphers } private CipherListCache matchedCiphersWithCache(final ThreadContext context, final String ciphers) { + final String contextProtocol = getContextProtocol(); final CipherListCache cache = cipherListCache; - if ( protocol.equals(cache.protocol) && ciphers.equals(cache.ciphers) ) { + if ( contextProtocol.equals(cache.protocol) && ciphers.equals(cache.ciphers) ) { return cache; } - return cipherListCache = matchedCiphers(context, ciphers); + return cipherListCache = matchedCiphers(context, ciphers, contextProtocol); } - private CipherListCache matchedCiphers(final ThreadContext context, final String ciphers) { + private CipherListCache matchedCiphers(final ThreadContext context, final String ciphers, final String contextProtocol) { final Ruby runtime = context.runtime; try { - final String[] supported = getSupportedCipherSuites(protocol); + final String[] supported = getSupportedCipherSuites(contextProtocol); final Collection cipherDefs = CipherStrings.matchingCiphers(ciphers, supported, false); @@ -612,7 +613,7 @@ private CipherListCache matchedCiphers(final ThreadContext context, final String cipherList.add(cipherInfo(context, def).freeze(context)); legacyCipherCount++; } - return new CipherListCache(protocol, ciphers, + return new CipherListCache(contextProtocol, ciphers, runtime.newArrayNoCopy(cipherList.toArray(new IRubyObject[cipherList.size()])), legacyCipherCount); } catch (GeneralSecurityException gse) { @@ -1091,6 +1092,28 @@ private List buildCredentials(final X509Cert cert, final PKey key, return credentials; } + private String getContextProtocol() { + final InternalContext context = internalContext; + if (context != null) return context.sslContext.getProtocol(); + final IRubyObject cert = getInstanceVariable("@cert"); + final IRubyObject key = getInstanceVariable("@key"); + return getContextProtocol(buildCredentials( + cert instanceof X509Cert ? (X509Cert) cert : t_cert, + key instanceof PKey ? (PKey) key : t_key, null)); + } + + private String getContextProtocol(final List credentials) { + if ("SSL".equals(protocol)) { + for (Credential credential : credentials) { + if (PKeyMLDSA.isMLDSAAlgorithm(credential.keyType)) { + // BCJSSE provides ML-DSA credentials through its TLS context. + return "TLS"; + } + } + } + return protocol; + } + private InternalContext initSSLContext(ThreadContext context, final Store store, final List clientCA, final int verifyMode, final IRubyObject verifyCallback, final int verifyDepth, final int timeout, @@ -1140,7 +1163,7 @@ private class InternalContext { this.alpnProtocols = alpnProtocols; this.alpnSelectCallback = alpnSelectCallback; - this.sslContext = SecurityHelper.getSSLContext(protocol); + this.sslContext = SecurityHelper.getSSLContext(getContextProtocol(certsAndKeys)); } void initSSLContext(final ThreadContext context) throws NoSuchAlgorithmException, KeyManagementException { diff --git a/src/main/java/org/jruby/ext/openssl/X509CRL.java b/src/main/java/org/jruby/ext/openssl/X509CRL.java index 59deac273..70c521e12 100644 --- a/src/main/java/org/jruby/ext/openssl/X509CRL.java +++ b/src/main/java/org/jruby/ext/openssl/X509CRL.java @@ -709,8 +709,8 @@ private static String getSignatureAlgorithm(final Ruby runtime, final PKey key, // Have to obey some artificial constraints of the OpenSSL implementation. Stupid. final String keyAlg = key.getAlgorithm(); - // PureEdDSA (Ed25519/Ed448) — no digest needed - if (digest.isNil() && PKeyEdDSA.isEdDSAAlgorithm(keyAlg)) { + // Pure signatures do not take a digest. + if (digest.isNil() && (PKeyEdDSA.isEdDSAAlgorithm(keyAlg) || PKeyMLDSA.isMLDSAAlgorithm(keyAlg))) { return keyAlg; } diff --git a/src/main/java/org/jruby/ext/openssl/X509Cert.java b/src/main/java/org/jruby/ext/openssl/X509Cert.java index beaeac546..d4ebf793a 100644 --- a/src/main/java/org/jruby/ext/openssl/X509Cert.java +++ b/src/main/java/org/jruby/ext/openssl/X509Cert.java @@ -283,6 +283,8 @@ private void initialize(final ThreadContext context, final X509Certificate cert) if ( sigAlgorithm == null ) sigAlgorithm = cert.getSigAlgName(); // e.g. SHA256withRSA else { sigAlgorithm = ASN1.oid2name(runtime, new ASN1ObjectIdentifier(sigAlgorithm), true); + final String longName = ASN1.o2a(runtime, new ASN1ObjectIdentifier(cert.getSigAlgOID()), true); + if (PKeyMLDSA.isMLDSAAlgorithm(longName)) sigAlgorithm = longName; if (sigAlgorithm == null) { sigAlgorithm = "0.0"; // "NULL"; // for some certificates that MRI parses, @@ -746,8 +748,8 @@ public IRubyObject sign(final ThreadContext context, final IRubyObject key, fina // Have to obey some artificial constraints of the OpenSSL implementation. Stupid. final String keyAlg = ((PKey) key).getAlgorithm(); final String digAlg; final String digName; - if (digest.isNil() && PKeyEdDSA.isEdDSAAlgorithm(keyAlg)) { - // PureEdDSA (Ed25519/Ed448) — no digest needed + if (digest.isNil() && (PKeyEdDSA.isEdDSAAlgorithm(keyAlg) || PKeyMLDSA.isMLDSAAlgorithm(keyAlg))) { + // Pure signatures do not take a digest. digAlg = null; digName = null; } else if (digest instanceof Digest) { @@ -809,6 +811,7 @@ else if (digest instanceof RubyString) { } String name = ASN1Registry.o2a(certHolder.getSignatureAlgorithm().getAlgorithm()); + if (name == null) name = ASN1.o2a(runtime, certHolder.getSignatureAlgorithm().getAlgorithm(), true); if ( name == null ) name = cert.getSigAlgOID(); this.sig_alg = runtime.newString(name); this.changed = false; diff --git a/src/main/java/org/jruby/ext/openssl/X509Request.java b/src/main/java/org/jruby/ext/openssl/X509Request.java index cfb068bb5..c432d9e92 100644 --- a/src/main/java/org/jruby/ext/openssl/X509Request.java +++ b/src/main/java/org/jruby/ext/openssl/X509Request.java @@ -361,7 +361,8 @@ public IRubyObject signature_algorithm(final ThreadContext context) { private String getSignatureAlgorithm() { AlgorithmIdentifier signatureAlgId = request == null ? null : request.getSignatureAlgorithm(); if (signatureAlgId == null) return "NULL"; - final String name = ASN1Registry.o2a(signatureAlgId.getAlgorithm()); + String name = ASN1Registry.o2a(signatureAlgId.getAlgorithm()); + if (name == null) name = ASN1.o2a(getRuntime(), signatureAlgId.getAlgorithm(), true); return name == null ? "" : name; } @@ -388,13 +389,17 @@ public IRubyObject sign(final ThreadContext context, final IRubyObject key, fina try { request = null; - if (_digest.isNil() && PKeyEdDSA.isEdDSAAlgorithm(pkey.getAlgorithm())) { - // PureEdDSA (Ed25519/Ed448) — pass algorithm identifier directly + if (_digest.isNil() && (PKeyEdDSA.isEdDSAAlgorithm(pkey.getAlgorithm()) || + PKeyMLDSA.isMLDSAAlgorithm(pkey.getAlgorithm()))) { + // Pure signatures take the algorithm identifier directly. final String keyAlg = pkey.getAlgorithm(); AlgorithmIdentifier algId = new DefaultSignatureAlgorithmIdentifierFinder().find(keyAlg); getRequest().sign( pkey.getPrivateKey(), algId ); } else { + if (PKeyMLDSA.isMLDSAAlgorithm(pkey.getAlgorithm())) { + throw newRequestError(runtime, new IllegalArgumentException("ML-DSA does not support digests")); + } Digest digest = Digest.getDigest(context, _digest); supportedSignatureAlgorithm(runtime, _RequestError(runtime), public_key, digest); getRequest().sign( pkey.getPrivateKey(), digest.getShortAlgorithm() ); diff --git a/src/main/java/org/jruby/ext/openssl/impl/PKCS10Request.java b/src/main/java/org/jruby/ext/openssl/impl/PKCS10Request.java index 5c0528b73..6136e72ec 100644 --- a/src/main/java/org/jruby/ext/openssl/impl/PKCS10Request.java +++ b/src/main/java/org/jruby/ext/openssl/impl/PKCS10Request.java @@ -245,6 +245,11 @@ else if (X9ObjectIdentifiers.id_dsa.equals(algOID)) { else if (X9ObjectIdentifiers.id_ecPublicKey.equals(algOID)) { algorithm = "EC"; } + else if (org.bouncycastle.asn1.nist.NISTObjectIdentifiers.id_ml_dsa_44.equals(algOID) || + org.bouncycastle.asn1.nist.NISTObjectIdentifiers.id_ml_dsa_65.equals(algOID) || + org.bouncycastle.asn1.nist.NISTObjectIdentifiers.id_ml_dsa_87.equals(algOID)) { + algorithm = "ML-DSA"; + } else { throw new NoSuchAlgorithmException("could not generate public key for request, oid: " + algOID); } diff --git a/src/main/java/org/jruby/ext/openssl/impl/PKey.java b/src/main/java/org/jruby/ext/openssl/impl/PKey.java index 81be71976..feead8366 100644 --- a/src/main/java/org/jruby/ext/openssl/impl/PKey.java +++ b/src/main/java/org/jruby/ext/openssl/impl/PKey.java @@ -62,13 +62,16 @@ import org.bouncycastle.asn1.ASN1InputStream; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1ObjectIdentifier; +import org.bouncycastle.asn1.ASN1OctetString; import org.bouncycastle.asn1.ASN1Primitive; import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.ASN1TaggedObject; import org.bouncycastle.asn1.DERBitString; import org.bouncycastle.asn1.DERNull; import org.bouncycastle.asn1.DERSequence; import org.bouncycastle.asn1.DLSequence; import org.bouncycastle.asn1.edec.EdECObjectIdentifiers; +import org.bouncycastle.asn1.nist.NISTObjectIdentifiers; import org.bouncycastle.asn1.pkcs.PKCSObjectIdentifiers; import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; import org.bouncycastle.asn1.x509.AlgorithmIdentifier; @@ -76,7 +79,10 @@ import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo; import org.bouncycastle.asn1.x9.X9ObjectIdentifiers; import org.bouncycastle.jcajce.interfaces.EdDSAPrivateKey; +import org.bouncycastle.jcajce.interfaces.MLDSAPrivateKey; import org.bouncycastle.jcajce.interfaces.XDHPrivateKey; +import org.bouncycastle.jcajce.spec.MLDSAParameterSpec; +import org.bouncycastle.jcajce.spec.MLDSAPrivateKeySpec; import org.bouncycastle.openssl.PEMParser; import org.jruby.ext.openssl.SecurityHelper; @@ -90,7 +96,7 @@ */ public class PKey { - public enum Type { RSA, DSA, EC, EdDSA, XDH, DH; } + public enum Type { RSA, DSA, EC, EdDSA, XDH, DH, MLDSA; } public static KeyPair readPrivateKey(final Type type, final byte[] input) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { @@ -158,6 +164,8 @@ public static KeyPair readPrivateKey(final Type type, final PrivateKeyInfo keyIn return readEdDSAPrivateKey(keyInfo); case XDH: return readXDHPrivateKey(keyInfo); + case MLDSA: + return readMLDSAPrivateKey(keyInfo); case DH: return readDHPrivateKey(keyInfo); default: @@ -210,6 +218,9 @@ private static Type matchPublicKeyType(final AlgorithmIdentifier algId) throws I if (EdECObjectIdentifiers.id_Ed448.equals(algIdentifier)) return Type.EdDSA; if (EdECObjectIdentifiers.id_X25519.equals(algIdentifier)) return Type.XDH; if (EdECObjectIdentifiers.id_X448.equals(algIdentifier)) return Type.XDH; + if (NISTObjectIdentifiers.id_ml_dsa_44.equals(algIdentifier) || + NISTObjectIdentifiers.id_ml_dsa_65.equals(algIdentifier) || + NISTObjectIdentifiers.id_ml_dsa_87.equals(algIdentifier)) return Type.MLDSA; if (PKCSObjectIdentifiers.dhKeyAgreement.equals(algIdentifier)) return Type.DH; throw new IOException("unsupported public key algorithm: " + algIdentifier); @@ -355,6 +366,51 @@ private static KeyPair readDHPrivateKey(final PrivateKeyInfo keyInfo) return new KeyPair(publicKey, privateKey); } + private static KeyPair readMLDSAPrivateKey(final PrivateKeyInfo keyInfo) + throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { + final ASN1ObjectIdentifier oid = keyInfo.getPrivateKeyAlgorithm().getAlgorithm(); + final int parameter = NISTObjectIdentifiers.id_ml_dsa_44.equals(oid) ? 44 : + NISTObjectIdentifiers.id_ml_dsa_65.equals(oid) ? 65 : 87; + final int expandedLength = parameter == 44 ? 2560 : parameter == 65 ? 4032 : 4896; + final ASN1Encodable encoded = keyInfo.parsePrivateKey(); + byte[] seed = null; + byte[] expanded = null; + if (encoded instanceof ASN1TaggedObject) { + ASN1TaggedObject tagged = (ASN1TaggedObject) encoded; + if (!tagged.hasContextTag(0) || tagged.isExplicit()) throw new InvalidKeySpecException("invalid ML-DSA seed tag"); + seed = ASN1OctetString.getInstance(tagged, false).getOctets(); + } + else if (encoded instanceof ASN1Sequence) { + ASN1Sequence both = (ASN1Sequence) encoded; + if (both.size() != 2) throw new InvalidKeySpecException("invalid ML-DSA private key sequence"); + seed = ASN1OctetString.getInstance(both.getObjectAt(0)).getOctets(); + expanded = ASN1OctetString.getInstance(both.getObjectAt(1)).getOctets(); + } + else { + expanded = ASN1OctetString.getInstance(encoded).getOctets(); + } + if (expanded != null && expanded.length != expandedLength) { + throw new InvalidKeySpecException("invalid ML-DSA expanded key length"); + } + final MLDSAParameterSpec parameters = MLDSAParameterSpec.fromName("ML-DSA-" + parameter); + // BC 1.80 misreads standardized seed encodings; its KeySpec works across supported BCs. + final KeySpec spec = seed != null ? new MLDSAPrivateKeySpec(parameters, seed) : + new MLDSAPrivateKeySpec(parameters, expanded, null); + final PrivateKey privateKey = SecurityHelper.getKeyFactory("ML-DSA").generatePrivate(spec); + if (!(privateKey instanceof MLDSAPrivateKey)) { + throw new InvalidKeySpecException("ML-DSA public key derivation requires a BC private key"); + } + final MLDSAPrivateKey key = (MLDSAPrivateKey) privateKey; + if (expanded != null && !org.bouncycastle.util.Arrays.constantTimeAreEqual(expanded, key.getPrivateData())) { + throw new InvalidKeySpecException("ML-DSA seed does not match expanded key"); + } + if (keyInfo.hasPublicKey() && !org.bouncycastle.util.Arrays.constantTimeAreEqual( + keyInfo.getPublicKeyData().getOctets(), key.getPublicKey().getPublicData())) { + throw new InvalidKeySpecException("ML-DSA public key does not match private key"); + } + return new KeyPair(key.getPublicKey(), privateKey); + } + // XDH (X25519 / X448) private static KeyPair readXDHPrivateKey(final PrivateKeyInfo keyInfo) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException { diff --git a/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java b/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java index bcc2ea6f9..0e8451744 100644 --- a/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java +++ b/src/main/java/org/jruby/ext/openssl/x509store/PEMInputOutput.java @@ -1475,6 +1475,12 @@ public static KeyFactory getKeyFactory(final AlgorithmIdentifier algId) private static Type getPrivateKeyType(final AlgorithmIdentifier algId) { final ASN1ObjectIdentifier algIdentifier = algId.getAlgorithm(); + if (org.bouncycastle.asn1.nist.NISTObjectIdentifiers.id_ml_dsa_44.equals(algIdentifier) || + org.bouncycastle.asn1.nist.NISTObjectIdentifiers.id_ml_dsa_65.equals(algIdentifier) || + org.bouncycastle.asn1.nist.NISTObjectIdentifiers.id_ml_dsa_87.equals(algIdentifier)) { + return Type.MLDSA; + } + if (X9ObjectIdentifiers.id_ecPublicKey.equals(algIdentifier)) { return Type.EC; } diff --git a/src/test/java/org/jruby/ext/openssl/SSLContextTest.java b/src/test/java/org/jruby/ext/openssl/SSLContextTest.java new file mode 100644 index 000000000..88a4676c9 --- /dev/null +++ b/src/test/java/org/jruby/ext/openssl/SSLContextTest.java @@ -0,0 +1,129 @@ +package org.jruby.ext.openssl; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; + +import org.jruby.RubyArray; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +public class SSLContextTest extends OpenSSLHelper { + @BeforeEach + public void setUp() throws Exception { + setUpRuntime(); + } + + @AfterEach + public void tearDown() { + tearDownRuntime(); + } + + @Test + public void mldsaCredentialSourcesUseTlsProvider() throws Exception { + for (int parameter : new int[] {44, 65, 87}) { + loadMLDSAIdentity(parameter); + for (String credentials : new String[] { + "ctx.cert = $cert; ctx.key = $key", + "ctx.add_certificate($cert, $key)", + "ctx.client_cert_cb = proc { [$cert, $key] }"}) { + SSLContext context = context("ctx = OpenSSL::SSL::SSLContext.new; " + credentials + "; ctx"); + assertEquals(engineClass("TLS"), context.createSSLEngine(null, 0).getClass()); + assertEquals("SSL", context.getProtocol(), "provider selection must not change the SSL method"); + } + } + } + + @Test + public void cipherQueriesFollowMldsaCredentials() { + loadMLDSAIdentity(44); + RubyArray tlsCiphers = (RubyArray) runtime.evalScriptlet("OpenSSL::SSL::SSLContext.new(:TLS).ciphers"); + RubyArray sslCiphers = (RubyArray) runtime.evalScriptlet("OpenSSL::SSL::SSLContext.new.ciphers"); + for (String credentials : new String[] { + "$ctx.cert = $cert; $ctx.key = $key", + "$ctx.add_certificate($cert, $key)", + "$ctx.client_cert_cb = proc { $calls += 1; [$cert, $key] }"}) { + SSLContext context = (SSLContext) runtime.evalScriptlet( + "$calls = 0; $ctx = OpenSSL::SSL::SSLContext.new; $ctx.ciphers; " + credentials + "; $ctx"); + boolean callback = credentials.contains("client_cert_cb"); + assertEquals(callback ? sslCiphers : tlsCiphers, context.ciphers(currentContext())); + assertEquals("0", runtime.evalScriptlet("$calls").toString(), "cipher queries must not call client_cert_cb"); + context.doSetup(currentContext()); + assertEquals(tlsCiphers, context.ciphers(currentContext())); + } + + runtime.evalScriptlet("$ctx = OpenSSL::SSL::SSLContext.new; $ctx.cert = $cert; $ctx.key = $key"); + assertEquals(tlsCiphers, runtime.evalScriptlet("$ctx.ciphers")); + assertEquals(sslCiphers, runtime.evalScriptlet("$ctx.key = nil; $ctx.ciphers")); + } + + @Test + public void emptyAndClassicalContextsKeepDefaultProvider() throws Exception { + SSLContext empty = context("OpenSSL::SSL::SSLContext.new"); + assertEquals(engineClass("SSL"), empty.createSSLEngine(null, 0).getClass()); + SSLContext rsa = context( + "ctx = OpenSSL::SSL::SSLContext.new; " + + "ctx.cert = OpenSSL::X509::Certificate.new(File.read('test/ssl/client.crt')); " + + "ctx.key = OpenSSL::PKey.read(File.read('test/ssl/client.key')); ctx"); + assertEquals(engineClass("SSL"), rsa.createSSLEngine(null, 0).getClass()); + } + + @Test + public void mixedCredentialsUseTlsProvider() throws Exception { + loadMLDSAIdentity(44); + SSLContext context = context( + "ctx = OpenSSL::SSL::SSLContext.new; " + + "ctx.cert = OpenSSL::X509::Certificate.new(File.read('test/ssl/client.crt')); " + + "ctx.key = OpenSSL::PKey.read(File.read('test/ssl/client.key')); " + + "ctx.add_certificate($cert, $key); ctx"); + assertEquals(engineClass("TLS"), context.createSSLEngine(null, 0).getClass()); + } + + @Test + public void mldsaCredentialsPreserveExplicitMethods() throws Exception { + loadMLDSAIdentity(44); + for (String method : new String[] {"TLSv1_2", "SSLv3"}) { + SSLContext context = context("ctx = OpenSSL::SSL::SSLContext.new(:" + method + "); " + + "ctx.cert = $cert; ctx.key = $key; ctx"); + assertEquals(engineClass(context.getProtocol()), context.createSSLEngine(null, 0).getClass()); + } + } + + @Test + public void mldsaCredentialsPreserveProtocolBoundsAndOptions() { + loadMLDSAIdentity(44); + SSLContext bounded = context("ctx = OpenSSL::SSL::SSLContext.new; " + + "ctx.cert = $cert; ctx.key = $key; " + + "ctx.min_version = ctx.max_version = OpenSSL::SSL::TLS1_3_VERSION; ctx"); + assertArrayEquals(new String[] {"TLSv1.3"}, bounded.createSSLEngine(null, 0).getEnabledProtocols()); + SSLContext disabled = context("ctx = OpenSSL::SSL::SSLContext.new; " + + "ctx.cert = $cert; ctx.key = $key; " + + "ctx.min_version = OpenSSL::SSL::TLS1_2_VERSION; " + + "ctx.max_version = OpenSSL::SSL::TLS1_3_VERSION; " + + "ctx.options |= OpenSSL::SSL::OP_NO_TLSv1_3; ctx"); + assertArrayEquals(new String[] {"TLSv1.2"}, disabled.createSSLEngine(null, 0).getEnabledProtocols()); + } + + private void loadMLDSAIdentity(int parameter) { + runtime.evalScriptlet( + "$key = OpenSSL::PKey.read(File.read('test/fixtures/mldsa/ml-dsa-" + parameter + "-seed.pem')); " + + "$cert = OpenSSL::X509::Certificate.new; $cert.version = 2; $cert.serial = 1; " + + "$cert.subject = $cert.issuer = OpenSSL::X509::Name.parse('/CN=ML-DSA test'); " + + "$cert.public_key = $key; " + + "$cert.not_before = Time.now - 60; $cert.not_after = Time.now + 3600; $cert.sign($key, nil)"); + } + + private SSLContext context(String script) { + SSLContext context = (SSLContext) runtime.evalScriptlet(script); + context.doSetup(currentContext()); + return context; + } + + private Class engineClass(String protocol) throws Exception { + javax.net.ssl.SSLContext context = SecurityHelper.getSSLContext(protocol); + context.init(null, new TrustManager[0], null); + return context.createSSLEngine().getClass(); + } +} diff --git a/test/fixtures/mldsa/README.md b/test/fixtures/mldsa/README.md new file mode 100644 index 000000000..dd1dd7ce3 --- /dev/null +++ b/test/fixtures/mldsa/README.md @@ -0,0 +1,54 @@ +These are disposable ML-DSA test identities, generated independently by OpenSSL +3.5.7 (9 Jun 2026), Debian trixie amd64, with its default provider. They must never +be used as real credentials. + +Run `./generate.sh` in this directory with OpenSSL 3.5+ and native Ruby linked to +that OpenSSL to regenerate them. Generation is random, so key and signature +bytes change. The script verifies signatures and compares the public keys +derived from every private-key encoding. `empty.bin` is empty; `binary.bin` +contains NUL and bytes outside ASCII. + +For each parameter set, `seed.pem`, `seed-priv.pem`, and `expanded.pem` are PKCS#8 +with seed-only, seed-and-expanded, and expanded-only private keys, respectively. +`public.pem` is SubjectPublicKeyInfo. `encrypted.pem` is expanded-only encrypted +PKCS#8 using PBES2, PBKDF2-HMAC-SHA256 (2048 iterations), and AES-256-CBC; the +password is `mldsa-test-password`. Tests decode PEM to exercise the equivalent +DER inputs rather than retaining duplicate fixtures. Expanded-only encryption +keeps encrypted-key import testable with BC 1.80, whose decoder does not accept +the newer tagged seed-only encoding. jruby-openssl imports standardized seed +encodings through BC's seed key-spec API, including on BC 1.80. It exports +standard expanded-only PKCS#8 on every BC version. The ambiguous prestandard +bare encodings emitted directly by BC 1.80 are outside this import contract. + +`binary.sig` was generated by `openssl pkeyutl`. OpenSSL 3.5.7's CLI rejects an +empty file with "Could not allocate 0 bytes for oneshot sign/verify buffer", so +`empty.sig` uses native Ruby's `OpenSSL::PKey#sign(nil, "")` against the same +libcrypto. Both signatures use pure ML-DSA with an empty context and no external +digest. + +Run `jruby -rbundler/setup -S rake test:mldsa_openssl` from the repository root +to check the reverse direction with native OpenSSL 3.5 or later on `PATH`. +This optional test generates JRuby keys for all three parameter sets, checks +their exported private and public keys with OpenSSL, and verifies their +signatures over `binary.bin`. It uses temporary files and leaves these fixtures +unchanged. + +To check a JRuby-produced signature using independent OpenSSL (replace `44` as +needed), write the signature to `/tmp/jruby-binary.sig`, then run: + +```sh +openssl pkeyutl -verify -pubin -inkey ml-dsa-44-public.pem \ + -in binary.bin -sigfile /tmp/jruby-binary.sig +``` + +For empty messages, use native Ruby linked to OpenSSL 3.5+: + +```sh +ruby -ropenssl -e 'key = OpenSSL::PKey.read(File.binread(ARGV[0])); + abort "invalid signature" unless key.verify(nil, File.binread(ARGV[1]), "")' \ + ml-dsa-44-public.pem /tmp/jruby-empty.sig +``` + +The selected encodings and pure signature options are documented by OpenSSL: +[EVP_PKEY-ML-DSA](https://docs.openssl.org/3.5/man7/EVP_PKEY-ML-DSA/) and +[openssl-pkeyutl](https://docs.openssl.org/3.5/man1/openssl-pkeyutl/). diff --git a/test/fixtures/mldsa/binary.bin b/test/fixtures/mldsa/binary.bin new file mode 100644 index 0000000000000000000000000000000000000000..00dd26933e1cd6326e9955e84e554437f0ad5890 GIT binary patch literal 21 ccmZQzWU6oY_unh1G^vuozaTX)IM{~^08}FfdH?_b literal 0 HcmV?d00001 diff --git a/test/fixtures/mldsa/empty.bin b/test/fixtures/mldsa/empty.bin new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixtures/mldsa/generate.sh b/test/fixtures/mldsa/generate.sh new file mode 100755 index 000000000..c22d01523 --- /dev/null +++ b/test/fixtures/mldsa/generate.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# Regenerate independent test fixtures with OpenSSL 3.5 or later. +set -eu +cd "$(dirname "$0")" +openssl version +: > empty.bin +printf '\000\001\002\177\200\376\377JRuby\000OpenSSL\n' > binary.bin +for parameter in 44 65 87; do + name="ml-dsa-$parameter" + openssl genpkey -algorithm "$name" -out "$name-seed-priv.pem" + openssl pkey -provparam ml-dsa.output_formats=seed-only \ + -in "$name-seed-priv.pem" -out "$name-seed.pem" + openssl pkey -provparam ml-dsa.output_formats=priv-only \ + -in "$name-seed-priv.pem" -out "$name-expanded.pem" + openssl pkey -in "$name-seed.pem" -pubout -out "$name-public.pem" + openssl pkcs8 -provparam ml-dsa.output_formats=priv-only -topk8 -in "$name-expanded.pem" -v2 aes-256-cbc -iter 2048 \ + -passout pass:mldsa-test-password -out "$name-encrypted.pem" + for message in binary; do + openssl pkeyutl -sign -inkey "$name-seed.pem" -in "$message.bin" \ + -out "$name-$message.sig" + openssl pkeyutl -verify -pubin -inkey "$name-public.pem" \ + -in "$message.bin" -sigfile "$name-$message.sig" + done + # pkeyutl 3.5.7 cannot allocate its buffer for an empty file; use libcrypto. + ruby -ropenssl -e ' + abort "native Ruby is required for independent fixtures" if defined?(JRUBY_VERSION) + name = ARGV.fetch(0) + key = OpenSSL::PKey.read(File.binread("#{name}-seed.pem")) + signature = key.sign(nil, "") + File.binwrite("#{name}-empty.sig", signature) + abort "invalid empty-message signature" unless key.verify(nil, signature, "") + ' "$name" + # Verify every private encoding derives the same public key. + for encoding in seed seed-priv expanded; do + openssl pkey -in "$name-$encoding.pem" -pubout | cmp - "$name-public.pem" + done + openssl pkey -in "$name-encrypted.pem" -passin pass:mldsa-test-password \ + -pubout | cmp - "$name-public.pem" +done + +chmod 644 *.pem *.bin *.sig diff --git a/test/fixtures/mldsa/ml-dsa-44-binary.sig b/test/fixtures/mldsa/ml-dsa-44-binary.sig new file mode 100644 index 0000000000000000000000000000000000000000..ca171567670fe39ced371ea7c07c1396c752ca4b GIT binary patch literal 2420 zcmV-)35)jK3^o?C!4pQA8QBu-?a+S7W|D_7hPG=$&D$yrrAxHbLzlRQNL$gYB5)|U zo&ywHAZLrAgUfZ2zh^5OeE83r^xzHh)PgD$iU_nBm2~l7TmY)Xz=REzz5E3t2#GJ# zQ+`Mt3=LGHpdjH?eS*R3V$62+tbT`&{1L3#+=beJnLW>0?9RN-u6vpF0y$lR;ey(_!US)%#p6$0bIk zF%uYjvES{vdzJuJWJ!l=D~`$JO!OKbBD7X9A&dA`%frJVN8(3e#JvrDLnLoOw9Wh#! zBCPE_Hg_e}GbxU_7~BMz)Ddi0r}{e55NNiRo=h-hshWEqD32i2T)hOraz?T7Cw}J# zE=OqUV@ST6jAJZ&aT+>mREUK~8?!crIu%KUAzXF#%&yXd0Xjjy$ULEdt@SLde&00r`i(dg5-vXv; z=AJt*q4jEyRgDSiu_}0FnJ}_btdrg-$grBB2~&<&8*lWCU#R_|4UOr+9K(|@NIqUvR~{5Wns zD*4~Nu5!MyD0@xB=447C;fU#_!n(y>i_mOI!~3KTwzwMi_N_VpoGGsxk+A=X1e;~Q z^kwUTmO^VmRVQ>WmON@*1*yk2lWki$y8x7ceI?J!%0zabWxyAh5zhp<9Y?*v3=>rH zQhr}%1vDa@zJ9ZMITaTcTBU52zGzP^zR2P_pe8n7k0|>Hj&8p1mM_ zA3Q%mebL*JgfSeyXg~JT+qoy~ox3Mo;#kDz_EOCZhpy(ILawxHW*PbA<}DZWKXttg z=W!SttPL&tZ=R(*GlhF(qG$zBq%N6Z7bxb7^3`y@0)Rha@d-$!CRZB}h^{c;G98={ z{9Jue2%_r?S}WUg6W=8qBU7|hL(^X;;OX<55l9W@>Refg!$KBCr8J+d|<@RF37;*yEfMP&qGT6Je}{9E4Ub6&lP|E%55pY4qlGeER6m09Pv1 z`F)dfM?`*QD4z0e7$Mp49~GVmKhRjWk6Is7Y-i4f85Z5TkxW^F0V-HvKxvf%i^Lj$bz{5D7(O~(T6fRc|PJdW6GN0vd%YO zQ=R}{L)s>t0Blsj9uppg?NGiIK)HqLn-8)vKgdfEDe-%7Ff(n#<4%HLrAM;Sxsz}7 z!n9tpKc1(E&9OB6;)rCPB#Z13t?2w!LPioU!{HsXViUeQOc!LWw6XL|AbOX5`u>zU zPHX_aerQ&yy3h*|pW9H7Q`f_UFpOIXmRX2{6E>Y+sTpwmT6PXR?&f!zTJB9ck>secUUq>wdpD94nDnN&ba)yyKAD zh+(!hB23f_JP?Mrbrh~h4byOYoTVoAeJ)rSFec1UbOV^X|I9UY9W%64%2qFHN&7nU zih>WHkU`u@L3cEO&4Q4X$nbKb;aBZ7O-UeChPFWHVVKkKY_ejlOUs=5M4+Ay zu7p8Ln=S|+)a9mJ97QbBMLMx7`0RqMl{7|d1m9=hH`U3O+jyE$M39=r9*(4hM4Y;Ez8X_y z(88~3$sX|S#S&y+8*T~kWC}F8sWF;}TS9go&$ed{Fyh)%q3${ht3AFl2#2Cnloob->Z~Jps=VBRfd4AhkqC1-=ZsK&3!v7KQ*=4;y-@= zTwz6MG=ZJkA?ork+XUra-$|={NxXykBG3kiN+&eOBCHm`YYmF@-7F$oCBi&*Fdy4n z@Di=^#=+!6>DtLlQL)-%!kt=f5KUuyeYaL?^?hY$&5%Y8WJUgWn;&Qp@ z4N68PGC4XsJWr06pR~8e*5ULDA2M8Pfryl#q|MaX?ClE}A|y&-WOcT?01pu$Jy&6Y ms;#fM&Ex0p`ThU^000000000000000000000000GA1OO1TBD%= literal 0 HcmV?d00001 diff --git a/test/fixtures/mldsa/ml-dsa-44-empty.sig b/test/fixtures/mldsa/ml-dsa-44-empty.sig new file mode 100644 index 0000000000000000000000000000000000000000..e98cdaf2336dad6baa20556dafa5b9632ee2ceee GIT binary patch literal 2420 zcmV-)35)hMKrfn3a!6$GuDtpIn>@~ox5QE<2y?FIaKP1#ct=zJEg7Os=MK#-SdZ7# zkMP*Nz-iCwUr8JC?vKPN4-0D+3o2Y zkZB5cPu70qjpI|RkX&{Yg^H?*(u`r7OEbni7#NyN@BKHNZT=VEr$oSZe6l>2;rDHf zgtlz!q&{EhOUT&Lf~QK+5W-r9e?#tC)o|F?A%JjXqT;0*p%z{ zaRu6{WJ;M^BoQ;R1L8F_yaEkH#GW=iucyoD$W{50_be7{-U{Jw=sw?gfLok?`0h)* zcD5<{K<^Sj_S|Yo_VPo!xZ!Rkl-|6;DL7Y%pmZvcy2_f zsN@iJqIB-N(kSKy{=HM03<|+r&GAg55G_6}uI-?Tz`Do@J>Z_Mg@!d%v{C=*T3jh3 zr5-Zv96Oyj92@9HGS(FYy8%Y>OI?ohfJ#Yh0-lYRY}swPIlHYhO%859zU2kOSbS)> zw_gC9UkS7=leZ$Pl#xqbMgNeF=e>xXGt7>|Hm6#xh8il~BsfATA((nDb-RTBbeW0$ zhzwq3Mr6pT?DB8tWbLR<;G>iNAlF%e12;)scpJkkV96?`^t=S1xUd3Uijs7Mdk&_C zbuHyLgtJ(K8p#@;z)QXJop(52SQE}Hgy6BZ#7V{*OOix?I9B8x<}HY;yUGyNdQ`VZYi%D0Z_l9hF? zV1A-PL8S5|8uMHf&0+|Zt=lnw&42=`8PqWf!JC-<$PQFncI@c9ntDpMQDij@sW~vO z{d&V0f&-M@9YZ?SQ@R^3=6ERs4jtrY$oM!yb#6rWva0H zOX(IvVDiMLIf~p)D=Ab-}*0rY~zsCV?7_S@Ma#}QH@7#g@jB%b7PiZa+hBQt`bXF zDA%|B96T8k*r|nH{v{NIAj9Xj#*1vc!*KF=0TC@7-g<+(HH9)Gq#DrxYNTCx6w-I$ zv34$W+nrR6ANh(fa>!6FfYZ_e7!6t9Zs&7S+~Md9k0tFfw9Go$-a9?)^bUD1IY|I- zWD7~T`<#uB^2`fx2K>YV!}EIyF=o02u|Wa0nuM(-O~tv6$+^cW#=`cXxY)GurM0BU z4INb>rATgmF~0YBP!)d-TP!!->+Y-XVEt!C?c3 z{dh}b3LI3&EbK~vCR>P)cwPuCbI|5wrq_}6W_-r0+)LM}*+a1H@tv<9*>a~dH31`f z3G--}ljWVtWKWE?LD_UB^Q+ngXb$aNomVcIFSbz&qA%*B+gY_&b98NPpdoPkj$;63p&8@8sEgC3 zrBJe-UPecOZaeKQezKOR^C`IvLV=26**VDuxJx0A#)2lEn*@jo52=7Y5~BcoMC{$P zkf%U7pY~qy^RvCimi;czp{6SfVERoo`och#lc-j%3|O+uI0%^riwJ=OeI!LV(F>N3 zKGFT9m}4xDc z8Wl(?gSJ%xV3&vbkz{+`0yt=>t&CQG%V!a+KfX_Na!Cjd0cPXZ z3Sm9rC^qfBzq+U&_qB8qQ>n{1W^q)f^)o@{8Xud%lj5b!JJ3l`ASn}y)b!b%>3ic~ zy;^iDuay|(lOz$pZk5%qAxIsyy8+_4`SD+C0m3ZXdhEiYx7~RrJ1y zB4JeUNf)USj8J5@51#A{O6#;184}Mg71OWs68Tgn9C9$kaqp8Xj-t_`mVQOeJrfMh zYFFni7)tH0@=jS}&N*Vxo#HnxE4+!aI)PygQjbIg?#eBZV+vy<-e^(NabqvQrTh?uwry!I;A(Ij z>;rl%xVZxej1GAIV+*rt&r2fw=qoXm)mpju*n-3n_T@-!PxtUm|7X$+Wzl%_OYDV? zZvs~^mNKcGVt3!$J`E$r4RBxtv9i|>*gYTG@Do)#RIfHgN39T2^Neh2dbG1m%wHzx zyDDCIMv)Kk(1t{^Azv#Pbf(RToInHYYG!%r>AodpVhYXn8NFvj0>-cj*3_Hrk1(ew zv!FL8DsqB~?eF~m41_ m{{R30000000000000000000000000000000000098YMF1xwBCK literal 0 HcmV?d00001 diff --git a/test/fixtures/mldsa/ml-dsa-44-encrypted.pem b/test/fixtures/mldsa/ml-dsa-44-encrypted.pem new file mode 100644 index 000000000..4930942e2 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-44-encrypted.pem @@ -0,0 +1,59 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +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 +KU6SxIowLZzM +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-44-expanded.pem b/test/fixtures/mldsa/ml-dsa-44-expanded.pem new file mode 100644 index 000000000..c0ac30292 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-44-expanded.pem @@ -0,0 +1,56 @@ +-----BEGIN PRIVATE KEY----- +MIIKGAIBADALBglghkgBZQMEAxEEggoEBIIKADmw28QI8U0qHfpntj7jflsKpqMP +ojKp1k9RshNEIdVT68ViQeNc8dookn29hhmgoYv5ReV7d2aZLe80u02qHEdXCRma +S0laIxZSpszwj0vshb+JzKzorei5kra2CUmM1iMH6KzBYGTbevqZ1+H1EldUJ82B +Qh66wENzNSQpPDAslLJR2iJOgAJSgDQhQoBp0YIJI0EuAiUJkQKJkkQoALUlUBBK +AKQw4IJJETZljIgkiEKQTKiE0zYOEoRtWJIIwEJB0DCMA6iIY8YISiIKykaCoQgy +C4RxCMBtmaItyAYIDEeKG4ZgxLiIiAIFZDhuDEcRFAMyHDlNSJZQXAgiEMJMyiKB +iUQuGQgwQAACoZaAEAAsmLggoUgC1KRkoDZRykQxUrgpCJiFCECK2cZICcAo0BZR +URCAW4Qw4gIM2xBK2yIS0oIRIIWECqeJgBRyoRIiksKNEiMtSDSESoRwU8aBVDYo +FCVA2KYl1IZkEyIiEQGMYgQy0kgIIEaGI6KNEBmF2yZKIMWQJBFlQzAmiICJYxRu +0AZK4hhFQ8YhhEIGA7BsHBEC4EIuELRgwQKQk8iEYCAuGzMOSKAAQziB2KhpJDVh +YsiNgUhCQahs4EYyQaQAmkBkWkIJG5QEibhAEIGRwKQJJDQQHIAADLVsQLAwYzKR +CzZtUbZNDIhEYxQpAEYIiKZECUQEjJYMShgBGKORCANljCJOIyNKSqAtUMQt5III +4EIl0DQtIrIIBBUMSSRsGEBl5EZsCUgwI0kmFLJFmLQoGxRhFDIl3EhsmpCBi7Yt +0pSIC4cAGsMlYkaEg7CJYpRNCDWJUsAxBMUFhEBOQSBg2TRRmIAwYCJK3MJxATIg +IyQBW0AmILIAC6CFEaKQiAguEBYMGERCC4RhAplhSiaAkTiJoEZGGUJoSDAqo8Ah +EhdtlIAoCxZi4oBE0jiNUUJBBCkJA7ltBDaGAoiQILkwIEeGCbaRGDSJSCJJEQdw +2jJSooIpCZCJIgCRUIYA4UQsUqJBhBYB2bSFGIIskZiNIzgBWSZpSBAwCEeB47hg +IiIEgEQpWcIkGaIkmBAwA0YNkEQNCiAtE5SBSAQtooaMCBVJJJaNGYFBJIANBAcQ +ozImEjFki8ZlA6dFm4Rw0jiKY6hBFEci0zYxA0KBWZggQwJlE5dJAAABEpmIU0Zx +07SByDZABDRFXCSEwSPh8O+7ku6RLFvpD6M0TlHkrWx4TTFlT1A7mtiZXBmtuRt6 +ZYqERezT75fJ2pWiofuKuqKVpXAyhzfyFzWh0xp86ubcfONTBruHUwRRKzQG+2HU +p/JIe2Pogb2ofkpUfDidp+UktyNQQPVoMiRR2HrQao6WBJnaj1/5rqR80poQi7dK +c3tn1VWnnknwiG17D1x4XOBD0pszZUSJ7oeJ1ZZ4I3Rbser3lCxJxgxIQOEyrShQ +hWZYbjzKxHlCuQZPmQWcf9RpEAMgyE1JmYOHBgfy3N5UvqOhKDjklFdNXtoDjmrB +Am2YWf7LlkIdQ0MdQ+vup7Vty5WHgH0uY4ArcAcHDp49BsJa7xqzHYy7horoZE39 ++D+hqo1C2Gj6fzSh1Enfapb7k7hiH4Kij+u0ztVBAP3+R/9k0IbNXaDVoifP90QK +dLh2SzzhZ+kP3JQoOuKHoDNChA/ELwh2DMgVlzT8UtmMq0H8jfmnLT643d3f0Tke ++4tKG3YnUy5WAnWfOW+0oqYptkRdh1e2M7zy+uYTK2qfyiZdvuaodfmOfljkT8w3 +1tNcIjggfg803+X7dl5z3mR/x5NIx3gBQVb+dAxqfuC51YsX0tubH92LTbg7Wpj/ +Cfj0vwPN3RlZyxGf62z1DJyLOLmX6SnoRNBKvfXouzmHk1ns4eksvba+Y1Jhyijl +j0YoplHApgWVSBugYdQI2uXKWnu7Jpu5DIl7QzR0+DotKxBi5b1AiRSljCxm/Qvh +g3oEuwbG1ZoR5FeVjsTCxsp0ZT14jL8fn4L+mXr+Cyd8LXRaiwapI/BWjA1T/k5D +3dj8Xs5I9rfu2qJwfArV4mOUEMZJEEtitH2SfORm2qFtx+zyew6JSGbEOkvK+YEn +Qg9qSr+vb0CrO9qPFn57nD7kOZtsa+o+qeKNjqjKTYqntiXM+0zz4io4059FO5qG +i+mN4W4lXzrEpsCoMlQ2raNH9pa4WiRS1PGvlCABioUzycBkkiIBWiDBodFTsv3g +PF9wm8S3+MJQf8WEZ4qwSiiHSvoHtNjxK09vi0CTmYWwADTWyejSEV359Wc3I+qw +v19nP9H4BZfrUOZ8hkJOLKuwYYjx8n3CQsXegC4Fztm+/HoiBHI7YzHVgUCe0AXq +iXjg79a85Y283Ms5pHKsYgsLn0TI9prpjVqRjoseHdhAlegeuLajnrny0nTEN/8U +pRKKCW/zhncCw7nVbbgjKH+th7mZtgKAZBGe71wuEbMW4vK9/GWc58f0bNTOPD39 +OrfHJKZ/aV94PwrUaNPNpCQaGBUXh2ZV2Df+5hrhg5WEpmwm5luF+oUVo5BPppU0 +gdKd3nfjrZkvuDpYTln/rw+4SFOAsbYAFVFdJpdcDPStmWySIHNvDziJMOmSbfkl +hi7+6bZDsqE/NYTPJxI9LHgqS6KvlnDfpirUFQOL/gZ588A5FFlY/Lw6oPEJBmq6 +QFxYOC1xGJfomLfl/7Ztu37k/+RrxxewYr9AuG0NhqNO38/y6hHgs8azWOktTffE +Q+9/83W/Xr7f0Sdyvv1WqUP6WKqjURcj3lG2NiO70BELe/5jZCCGUkpNi2yJbCeh +iC70M3YpElF3OcReeothY+nONdN5ZwJeLLBRKZZniLdPZsi2S07ikz1nWt5THeko +uP3a0linyyfHF4f7Aqp9eAk7B8NL1vae467CwZH5HWZiXZKnBM7pu0EqVhGkIn7i +Kyt2NiPJRzMqNp0p237R/sywDtGzb726Jg1Bg84NF4wUjKu3NB3G2q+B1bTRiGom +jopLkQRYM4rQKbtdvt1zNB2OgATpCpkutIC0rmVtzpj9Kxmg5snAZcWMSRoSj1ZD +A0o+NrFZVQrWbOQ/3wM/QYB3N/TjY5nmdxNTx1Sotgblzn3BclRS0cG4oAcb60o/ +0tA/4u4PIioJq5JiPFMLC1pCLAk+ZxBPB5ZXeAIGqwgNv+HMsevjzNLyXWUdjrto +5wQZAFkTLclOh1XtZZW1+sedN7To1uCPr0BVpTdOt1HBcqlFoe2Ir6y+Y3j2jcHf +xz89F0q1BLlD24XkuyHTstBbbeZKjioXZu+QjpsK4MJ3u1zPsdsyUlQ1z0xvLajn +IyLrfNG/NzWKTiiFdNXEx1o7URT8rs3IbouxwabrQDZ/U6QAGn6hpflA0imWnpAe +t3fhRP9ZFez2K3UrUaMZa3sIRTLJCrTOfmygH6kVY8EgXuA3JZXDhHl+WM0= +-----END PRIVATE KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-44-public.pem b/test/fixtures/mldsa/ml-dsa-44-public.pem new file mode 100644 index 000000000..6ff70b1ff --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-44-public.pem @@ -0,0 +1,30 @@ +-----BEGIN PUBLIC KEY----- +MIIFMjALBglghkgBZQMEAxEDggUhADmw28QI8U0qHfpntj7jflsKpqMPojKp1k9R +shNEIdVTfmzgJ8HVz80H+Astue/R3NRBP5C9TJozRCK3iLbXA3V1Cgx3MLMM4wpB +thH17x59V4/taDLJt7dacE2iAfzkkELUqm08rCNBn7yZLZnYlKWT5X0BT/YlnJMH +bmHkAOo7mlre3eIQULrttLZ4JfB1tOstbbG7gu4SLyQfquKXnB71Vuw60MLwEcJH +Hh2vKHkIp8JH0Cpp4mqOAhEerjmwk0bV4k1oan58yk8oSzQlkwYNs852b+joYXmg +Y+EuYpj2RxtI0kx8rLDtbzxsrcHfyL+Gq7AHIgsEBIi8pmitUOUoYoHtg4E98n6L +VpNOl4HYYRJIhBR4VvUlyS+RoxM4DzGjQJlv1Ou64a73nynDpndDasn80IUAmtKF +sqmoHDAC03x7u9yryctgUDAg2io8F6UQCJo7eBbwTDtM+E8OmoXwpUdfnlr3R6Mq +G1fNAFdllwTlqeBjh3HphEaz7lmLeiRoEathpeuAe/aUkJNhzFY1uniuiJrta2Sf +A/OtgdA2b0UM/x+SLNW1Y4XeHnURFnQHA8i+s6SmS5kBN+wYWO3lmo31Klgczw3g +PZEGZc8KelOBHoqImWYkvHHVN/REsjh1l7a0Ei9Sd2mrqU8EFyUqGCoia0ktkkj/ ++zegALjl2T7URZ4tJdN7nZTYA1T+U5pZKQSgoOIxq7ICtvButM5XyB354Cfws8IX +BJXc6Uge20bzHP09hWzoZPnpvXXlwX2qXkICiReZfo53Hmm0WVCJxZ7gbJZnS2qR +qO/sJtw6LTwCIVzqdg5DJjp0dyIY/pJCoqZOfPYOGapjJPcAafSu32t0UDCf9OLs +ei/BXb2ZZuAvRJdEcThpmfNuJEjKAssnNpJ0E+mDEICwifWtmtRZZr8PorvpeGNj +Il32FYh9YpEvAXpbD0gro3bScbBVBI7aCp9rKqHzKSHf+sEObCwHkY3OpFdDPJVr +4qsQu7p9jlU4wevSczc9AFEgiAWNQHgTUmuG4zK8RyEUTOG3UBbkfziL6V9086EE +wDun00OW+9gbOQ4CAS8tEQYE7KHPETzXHtUwfo0i4x56wcb/uunWa5PMcAh4tKf5 +d4kD/ugNHquOoduRPq2rSVf8oDAFfVbBHbCq4NfsveRrREP1mXbK4etPqOMPYRk/ +ynpiaM/VEzvoq3fCx/fzbtd6FR/AbQSJi7Wtfc7xNdKPieI2anoHaRoGihhXfql3 +NAQYvDq7jJCO61e6bfjgjt2CDHnps6TcoYCHIzDCDSxj+xiGq6e/y0V0AurAt0j0 +PY/HHMQI8mCsvPeiAloY0FkP38Uu+feZrm+DaqXnoD8yCtnBjLzf+2GaZvuNwrH2 +080gSxF5FCrUpaOEsdcDM18m2ebIMyuOcnSdHpc0aCjN7TYfahrzWdklUdCAiEDN +q4DSlXHGE4RFp5/KdHDYPuLRnAfc1hN3dP7l3neS9AqsgScLxGv8s2GBL9ZN5+Xo +x/1l9lXyN35bNp/4ZmSw/NS84X+ND4GgYVwmErOOq86GniibxGZbjepUEdzdtoaS +69JxJlZrq4aBqN0gswSYvXxWlN8pvObxxuq2/o7sylJgHCvZWem2+C3IsRdfPSUu +zy0xN84fwPFdHuMljgGhHfBLrifr/WReM0Znn+Sfizgu0ODhpP3jwuxXSVQQbcri +0UIX8P9au6HW1tT5IYerTq97nKWL59ejtfrlOm1YPiDd/ALFx8I= +-----END PUBLIC KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-44-seed-priv.pem b/test/fixtures/mldsa/ml-dsa-44-seed-priv.pem new file mode 100644 index 000000000..0d64bc9e5 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-44-seed-priv.pem @@ -0,0 +1,57 @@ +-----BEGIN PRIVATE KEY----- +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 +-----END PRIVATE KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-44-seed.pem b/test/fixtures/mldsa/ml-dsa-44-seed.pem new file mode 100644 index 000000000..e665e8b59 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-44-seed.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MDQCAQAwCwYJYIZIAWUDBAMRBCKAIAPG5lOUUKGxUIy2tZNDwXdxDE587itIDH6o +L7K8Mvhx +-----END PRIVATE KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-65-binary.sig b/test/fixtures/mldsa/ml-dsa-65-binary.sig new file mode 100644 index 0000000000000000000000000000000000000000..220af8708e80fc93ad3a94c19d5b7936b6ee9ada GIT binary patch literal 3309 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 B{#qTSqTg~DNqkt%rlz`w`p}Aood(;=@{DtL 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 JA}B65Iz#aI9qa%A literal 0 HcmV?d00001 diff --git a/test/fixtures/mldsa/ml-dsa-87-empty.sig b/test/fixtures/mldsa/ml-dsa-87-empty.sig new file mode 100644 index 0000000000000000000000000000000000000000..9eaa71712493938f2fbfe74c58c499f2c7531b11 GIT binary patch literal 4627 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 J7#|`hEj8lu1$F=c literal 0 HcmV?d00001 diff --git a/test/fixtures/mldsa/ml-dsa-87-encrypted.pem b/test/fixtures/mldsa/ml-dsa-87-encrypted.pem new file mode 100644 index 000000000..5b6ab96f5 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-87-encrypted.pem @@ -0,0 +1,107 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +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 +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-87-expanded.pem b/test/fixtures/mldsa/ml-dsa-87-expanded.pem new file mode 100644 index 000000000..ec7b21952 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-87-expanded.pem @@ -0,0 +1,105 @@ +-----BEGIN PRIVATE KEY----- +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 +0BUoI5X5ViSHiU0yww/5Lq2nEytsGGjdg/IbGg== +-----END PRIVATE KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-87-public.pem b/test/fixtures/mldsa/ml-dsa-87-public.pem new file mode 100644 index 000000000..4cae60067 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-87-public.pem @@ -0,0 +1,57 @@ +-----BEGIN PUBLIC KEY----- +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 +jLF4EgA4S33VertB8j9GV7RGAxpmLQ== +-----END PUBLIC KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-87-seed-priv.pem b/test/fixtures/mldsa/ml-dsa-87-seed-priv.pem new file mode 100644 index 000000000..ba3e5588d --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-87-seed-priv.pem @@ -0,0 +1,106 @@ +-----BEGIN PRIVATE KEY----- +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 +TTLDD/kuracTK2wYaN2D8hsa +-----END PRIVATE KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-87-seed.pem b/test/fixtures/mldsa/ml-dsa-87-seed.pem new file mode 100644 index 000000000..e65b8d0c0 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-87-seed.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MDQCAQAwCwYJYIZIAWUDBAMTBCKAIJAWOdONYe+HC2RSDaU/fxEUftnHtZf+CnEp +7oU8nkOG +-----END PRIVATE KEY----- diff --git a/test/pkey/test_pkey_mldsa.rb b/test/pkey/test_pkey_mldsa.rb new file mode 100644 index 000000000..3caf84e0a --- /dev/null +++ b/test/pkey/test_pkey_mldsa.rb @@ -0,0 +1,179 @@ +require File.expand_path('../test_helper', File.dirname(__FILE__)) +require 'base64' + +class TestPKeyMLDSA < TestCase + PARAMETERS = [44, 65, 87].freeze + FIXTURES = File.expand_path('../fixtures/mldsa', File.dirname(__FILE__)) + + def setup + super + if fips? + begin + OpenSSL::PKey.generate_key('ML-DSA-44') + rescue OpenSSL::PKey::PKeyError => error + skip("ML-DSA unavailable from the FIPS provider: #{error.message}") + end + end + end + + def test_generate_and_sign + PARAMETERS.each do |parameter| + algorithm = "ML-DSA-#{parameter}" + key = OpenSSL::PKey.generate_key(algorithm) + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.generate_key(algorithm, 'seed' => 'ignored') } + assert_equal key.oid, OpenSSL::PKey.generate_key(algorithm, {}).oid + public_key = OpenSSL::PKey.read(key.public_to_der) + other = OpenSSL::PKey.generate_key(algorithm) + assert_instance_of OpenSSL::PKey::PKey, key + assert_equal "id-ml-dsa-#{parameter}", key.oid + assert_match(/type_name=#{algorithm}/, key.inspect) + assert_equal true, key.private? + assert_equal true, key.public? + assert_equal false, public_key.private? + exported = OpenSSL::ASN1.decode(key.private_to_der) + expanded = OpenSSL::ASN1.decode(exported.value[2].value) + assert_instance_of OpenSSL::ASN1::OctetString, expanded + assert_equal(parameter == 44 ? 2560 : parameter == 65 ? 4032 : 4896, expanded.value.bytesize) + + ['', fixture('binary.bin'), 'prefix-message-suffix'[7, 7]].each do |message| + signature = key.sign(nil, message) + assert_equal true, public_key.verify(nil, signature, message) + assert_equal false, public_key.verify(nil, signature, message + "\0") + assert_equal false, other.verify(nil, signature, message) + ['', signature[0, signature.bytesize - 1], signature + "\0", + "\xff".b * signature.bytesize].each do |bad_signature| + assert_equal false, public_key.verify(nil, bad_signature, message) + end + end + assert_raise(ArgumentError) { public_key.sign(nil, 'message') } + ['SHA256', OpenSSL::Digest::SHA512.new].each do |digest| + assert_raise(OpenSSL::PKey::PKeyError) { key.sign(digest, 'message') } + assert_raise(OpenSSL::PKey::PKeyError) { public_key.verify(digest, '', 'message') } + end + assert_raise(OpenSSL::PKey::PKeyError) { key.derive(public_key) } + assert_raise(OpenSSL::PKey::PKeyError) { public_key.private_to_der } + assert_raise(OpenSSL::PKey::PKeyError) { key.private_to_pem(OpenSSL::Cipher.new('aes-256-cbc'), 'password') } + end + end + + %w[seed seed-priv expanded].each do |encoding| + define_method("test_independent_openssl_#{encoding.tr('-', '_')}_keys_and_signatures") do + PARAMETERS.each do |parameter| + public_key = OpenSSL::PKey.read(fixture("ml-dsa-#{parameter}-public.pem")) + pem = fixture("ml-dsa-#{parameter}-#{encoding}.pem") + [pem, pem_der(pem)].each do |input| + key = OpenSSL::PKey.read(input) + assert_equal public_key.public_to_der, key.public_to_der + assert_equal true, key.private? + assert_equal "id-ml-dsa-#{parameter}", key.oid + %w[empty binary].each do |message| + data = fixture("#{message}.bin") + signature = fixture("ml-dsa-#{parameter}-#{message}.sig") + assert_equal true, key.verify(nil, signature, data) + assert_equal true, public_key.verify(nil, key.sign(nil, data), data) + end + [key.private_to_der, key.private_to_pem].each do |export| + assert_equal public_key.public_to_der, OpenSSL::PKey.read(export).public_to_der + end + end + [public_key.public_to_der, public_key.public_to_pem].each do |export| + assert_equal public_key.public_to_der, OpenSSL::PKey.read(export).public_to_der + end + spki = OpenSSL::ASN1.decode(public_key.public_to_der) + assert_equal "2.16.840.1.101.3.4.3.#{parameter == 44 ? 17 : parameter == 65 ? 18 : 19}", spki.value[0].value[0].oid + assert_equal "id-ml-dsa-#{parameter}", spki.value[0].value[0].sn + assert_equal "ML-DSA-#{parameter}", spki.value[0].value[0].ln + end + end + end + + def test_encrypted_pkcs8_import + PARAMETERS.each do |parameter| + pem = fixture("ml-dsa-#{parameter}-encrypted.pem") + expected = OpenSSL::PKey.read(fixture("ml-dsa-#{parameter}-public.pem")) + [pem, pem_der(pem)].each do |input| + key = OpenSSL::PKey.read(input, 'mldsa-test-password') + assert_equal expected.public_to_der, key.public_to_der + assert_equal true, expected.verify(nil, key.sign(nil, ''), '') + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(input, 'wrong-password') } + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(input) } + end + end + end + + def test_malformed_keys + ['', 'not a key', "\x30\x03\x02\x01\x00".b].each do |input| + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(input) } + end + PARAMETERS.each do |parameter| + public_der = pem_der(fixture("ml-dsa-#{parameter}-public.pem")) + [:short, :long].each do |variant| + spki = OpenSSL::ASN1.decode(public_der) + bytes = spki.value[1].value + spki.value[1].value = variant == :short ? bytes[0, bytes.bytesize - 1] : bytes + "\0" + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(spki.to_der) } + end + %w[seed expanded public].each do |encoding| + der = pem_der(fixture("ml-dsa-#{parameter}-#{encoding}.pem")) + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(der[0, der.bytesize - 1]) } + end + end + end + + def test_invalid_private_key_components + PARAMETERS.each do |parameter| + original = pem_der(fixture("ml-dsa-#{parameter}-seed-priv.pem")) + [31, 33].each do |length| + info = OpenSSL::ASN1.decode(original) + both = OpenSSL::ASN1.decode(info.value[2].value) + both.value[0].value = "\0" * length + info.value[2].value = both.to_der + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(info.to_der) } + end + [:short, :long, :inconsistent].each do |variant| + info = OpenSSL::ASN1.decode(original) + both = OpenSSL::ASN1.decode(info.value[2].value) + expanded = both.value[1].value + both.value[1].value = case variant + when :short then expanded[0, expanded.bytesize - 1] + when :long then expanded + "\0" + else "\0" * expanded.bytesize + end + info.value[2].value = both.to_der + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(info.to_der) } + end + info = OpenSSL::ASN1.decode(pem_der(fixture("ml-dsa-#{parameter}-seed.pem"))) + seed = OpenSSL::ASN1.decode(info.value[2].value) + seed.tag = 1 + info.value[2].value = seed.to_der + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(info.to_der) } + + # These malformed standard encodings have the length of a legacy bare key. + info.value[2].value = OpenSSL::ASN1::OctetString.new("\0" * 30, 0, :IMPLICIT, :CONTEXT_SPECIFIC).to_der + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(info.to_der) } + info = OpenSSL::ASN1.decode(pem_der(fixture("ml-dsa-#{parameter}-expanded.pem"))) + expanded = OpenSSL::ASN1.decode(info.value[2].value) + expanded.value = expanded.value[0, expanded.value.bytesize - 4] + info.value[2].value = expanded.to_der + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(info.to_der) } + + info = OpenSSL::ASN1.decode(original) + info.value[0].value = 1 + public_key = OpenSSL::ASN1.decode(pem_der(fixture("ml-dsa-#{parameter}-public.pem"))).value[1].value + info.value << OpenSSL::ASN1::BitString.new(public_key, 1, :IMPLICIT, :CONTEXT_SPECIFIC) + assert_equal public_key, OpenSSL::ASN1.decode(OpenSSL::PKey.read(info.to_der).public_to_der).value[1].value + info.value.last.value = "\0" * public_key.bytesize + assert_raise(OpenSSL::PKey::PKeyError) { OpenSSL::PKey.read(info.to_der) } + end + end + + private + + def fixture(name) + File.binread(File.join(FIXTURES, name)) + end + + def pem_der(pem) + Base64.decode64(pem.lines.reject { |line| line.start_with?('-----') }.join) + end +end diff --git a/test/test_x509_mldsa.rb b/test/test_x509_mldsa.rb new file mode 100644 index 000000000..f9ce40b34 --- /dev/null +++ b/test/test_x509_mldsa.rb @@ -0,0 +1,131 @@ +require_relative 'test_helper' + +class TestX509MLDSA < TestCase + ALGORITHMS = %w[ML-DSA-44 ML-DSA-65 ML-DSA-87].freeze + CA_EXTENSIONS = [ + ['basicConstraints', 'CA:TRUE', true], + ['keyUsage', 'keyCertSign,cRLSign', true] + ].freeze + + def test_certificate_public_key_and_roundtrip + ALGORITHMS.each do |algorithm| + key = generate_key(algorithm) + other = generate_key(algorithm) + public_key = OpenSSL::PKey.read(key.public_to_der) + cert = issue_cert(distinguished_name(algorithm), public_key, 1, [], nil, key, digest: nil) + + [cert, OpenSSL::X509::Certificate.new(cert.to_der), + OpenSSL::X509::Certificate.new(cert.to_pem)].each do |copy| + assert_equal algorithm, copy.signature_algorithm + assert_equal "id-ml-dsa-#{algorithm.split('-').last}", copy.public_key.oid + assert_equal key.public_to_der, copy.public_key.public_to_der + assert_equal false, copy.public_key.private? + assert_equal true, copy.check_private_key(key) + assert_equal false, copy.check_private_key(other) + assert_equal true, copy.verify(copy.public_key) + assert_equal false, copy.verify(other) + assert_equal cert.to_der, copy.to_der + end + end + end + + def test_request_sign_verify_and_roundtrip + ALGORITHMS.each do |algorithm| + key = generate_key(algorithm) + other = generate_key(algorithm) + request = OpenSSL::X509::Request.new + request.version = 0 + request.subject = distinguished_name(algorithm) + request.public_key = OpenSSL::PKey.read(key.public_to_der) + assert_same request, request.sign(key, nil) + + [request, OpenSSL::X509::Request.new(request.to_der), + OpenSSL::X509::Request.new(request.to_pem)].each do |copy| + assert_equal algorithm, copy.signature_algorithm + assert_equal "id-ml-dsa-#{algorithm.split('-').last}", copy.public_key.oid + assert_equal key.public_to_der, copy.public_key.public_to_der + assert_equal false, copy.public_key.private? + assert_equal true, copy.verify(copy.public_key) + assert_equal false, copy.verify(other) + assert_equal request.to_der, copy.to_der + end + + request.subject = distinguished_name('modified') + assert_equal false, request.verify(key) + end + end + + def test_crl_sign_verify_and_roundtrip + ALGORITHMS.each do |algorithm| + key = generate_key(algorithm) + other = generate_key(algorithm) + ca = issue_cert(distinguished_name(algorithm), key, 1, CA_EXTENSIONS, nil, nil, digest: nil) + now = Time.now + crl = issue_crl([[2, now - 60, 1]], 1, now - 60, now + 3600, [], ca, key, nil) + + [crl, OpenSSL::X509::CRL.new(crl.to_der), + OpenSSL::X509::CRL.new(crl.to_pem)].each do |copy| + assert_equal algorithm, copy.signature_algorithm + assert_equal true, copy.verify(ca.public_key) + assert_equal false, copy.verify(other) + assert_equal 2, copy.revoked.first.serial.to_i + assert_equal crl.to_der, copy.to_der + end + end + end + + def test_mixed_classical_and_mldsa_chains + rsa = Fixtures.pkey('rsa2048') + ALGORITHMS.each do |algorithm| + key = generate_key(algorithm) + [[rsa, key, rsa], [key, rsa, key]].each do |root_key, issuer_key, leaf_key| + root_digest = root_key.equal?(rsa) ? 'SHA256' : nil + issuer_digest = issuer_key.equal?(rsa) ? 'SHA256' : nil + root = issue_cert(distinguished_name('root'), root_key, 1, CA_EXTENSIONS, + nil, nil, digest: root_digest) + issuer = issue_cert(distinguished_name('intermediate'), issuer_key, 2, CA_EXTENSIONS, + root, root_key, digest: root_digest) + leaf = issue_cert(distinguished_name('leaf'), leaf_key, 3, + [['keyUsage', 'digitalSignature', true]], + issuer, issuer_key, digest: issuer_digest) + store = OpenSSL::X509::Store.new + store.add_cert(OpenSSL::X509::Certificate.new(root.to_der)) + assert_equal true, store.verify(OpenSSL::X509::Certificate.new(leaf.to_pem), + [OpenSSL::X509::Certificate.new(issuer.to_der)]), store.error_string + assert_equal true, issuer.verify(root.public_key) + assert_equal true, leaf.verify(issuer.public_key) + end + end + end + + def test_signing_rejects_digests + ALGORITHMS.each do |algorithm| + key = generate_key(algorithm) + cert = issue_cert(distinguished_name(algorithm), key, 1, [], nil, nil, digest: nil) + request = OpenSSL::X509::Request.new + request.version = 0 + request.subject = distinguished_name(algorithm) + request.public_key = OpenSSL::PKey.read(key.public_to_der) + crl = issue_crl([], 1, Time.now - 60, Time.now + 3600, [], cert, key, nil) + + ['SHA256', OpenSSL::Digest::SHA512.new].each do |digest| + assert_raise(OpenSSL::X509::CertificateError) { cert.sign(key, digest) } + assert_raise(OpenSSL::X509::RequestError) { request.sign(key, digest) } + assert_raise(OpenSSL::X509::CRLError) { crl.sign(key, digest) } + end + end + end + + private + + def generate_key(algorithm) + OpenSSL::PKey.generate_key(algorithm) + rescue OpenSSL::PKey::PKeyError => error + raise unless fips? && error.message.include?('unsupported algorithm') + skip("#{algorithm} unavailable from the FIPS provider: #{error.message}") + end + + def distinguished_name(common_name) + OpenSSL::X509::Name.new([['CN', common_name]]) + end +end