diff --git a/.github/workflows/ci-bc-compat.yml b/.github/workflows/ci-bc-compat.yml index 34d7869b..6a7da405 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 dc1d0aab..0a60c815 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 939e63f2..61273c24 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 fb8a9190..5cb16a67 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 a317e16c..88bd7900 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 c1dbd1cd..2356862c 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 00000000..de282114 --- /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 00000000..c5845697 --- /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 04cf8e88..64298414 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 533439ac..4518c97b 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 25b9008c..a7685d22 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 00000000..37ad5a63 --- /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 18fbf415..6fc42d03 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 59deac27..70c521e1 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 beaeac54..d4ebf793 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 cfb068bb..c432d9e9 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 5c0528b7..6136e72e 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 81be7197..feead836 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 bcc2ea6f..0e845174 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 00000000..88a4676c --- /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 00000000..dd1dd7ce --- /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 00000000..00dd2693 Binary files /dev/null and b/test/fixtures/mldsa/binary.bin differ diff --git a/test/fixtures/mldsa/empty.bin b/test/fixtures/mldsa/empty.bin new file mode 100644 index 00000000..e69de29b diff --git a/test/fixtures/mldsa/generate.sh b/test/fixtures/mldsa/generate.sh new file mode 100755 index 00000000..c22d0152 --- /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 00000000..ca171567 Binary files /dev/null and b/test/fixtures/mldsa/ml-dsa-44-binary.sig differ 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 00000000..e98cdaf2 Binary files /dev/null and b/test/fixtures/mldsa/ml-dsa-44-empty.sig differ 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 00000000..4930942e --- /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 00000000..c0ac3029 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-44-expanded.pem @@ -0,0 +1,56 @@ +-----BEGIN PRIVATE KEY----- +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 +-----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 00000000..6ff70b1f --- /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 00000000..0d64bc9e --- /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 00000000..e665e8b5 --- /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 00000000..220af870 Binary files /dev/null and b/test/fixtures/mldsa/ml-dsa-65-binary.sig differ diff --git a/test/fixtures/mldsa/ml-dsa-65-empty.sig b/test/fixtures/mldsa/ml-dsa-65-empty.sig new file mode 100644 index 00000000..9cd7c403 Binary files /dev/null and b/test/fixtures/mldsa/ml-dsa-65-empty.sig differ diff --git a/test/fixtures/mldsa/ml-dsa-65-encrypted.pem b/test/fixtures/mldsa/ml-dsa-65-encrypted.pem new file mode 100644 index 00000000..c4626d95 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-65-encrypted.pem @@ -0,0 +1,89 @@ +-----BEGIN ENCRYPTED PRIVATE KEY----- +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 +-----END ENCRYPTED PRIVATE KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-65-expanded.pem b/test/fixtures/mldsa/ml-dsa-65-expanded.pem new file mode 100644 index 00000000..592ddcb3 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-65-expanded.pem @@ -0,0 +1,87 @@ +-----BEGIN PRIVATE KEY----- +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 +fN8MZs19cDRzbeSF4Vvt/2W0YieuHADFtZd2cg== +-----END PRIVATE KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-65-public.pem b/test/fixtures/mldsa/ml-dsa-65-public.pem new file mode 100644 index 00000000..f20855e4 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-65-public.pem @@ -0,0 +1,44 @@ +-----BEGIN PUBLIC KEY----- +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 +wuXBRFpR +-----END PUBLIC KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-65-seed-priv.pem b/test/fixtures/mldsa/ml-dsa-65-seed-priv.pem new file mode 100644 index 00000000..1bde6a07 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-65-seed-priv.pem @@ -0,0 +1,88 @@ +-----BEGIN PRIVATE KEY----- +MIIP/gIBADALBglghkgBZQMEAxIEgg/qMIIP5gQgkCqqIU9sbRkdO9xFNNf8cm6l +lQpEU1hgmI11LSXKTU4Egg/AkAMj8ku9p1Vp5PjvkWyu6HsdUrmZb+8O3vGdOChU +viamBM7eSkY0AVjLi2rugbuvPVsuPYvZQtXntK/5mZFeZAWlyhadlEh9M7fHP6vp +lVgeg7LOEAX6Dm0L4zynKzTO+2kAyovcQdtm10pSwKH9ERkE/zNOslvn5yRfhRG2 +V9ZRdBOIYVcXgWQnUxUHQBMWEUgTAjZgWIJTU0BWYXFFYncQhGGFhxMSI4eAdnNE +gXNIAAeBFVRYhUNiBGJIVzYwhjIwNDRiZzd4BDh2gCNRJhUiMHJQhCUiIxMjdEaB +UDMAMncSBYVjdCRVRnMxcSiBUUBIGFJTYQMHYDECQQglRCUIRUFnBCKCeGAgUYCD +hEWIUwVVN3ZFJIdHQXaHdwEgUgdBFhACQnRQdhETgWEidEJUhmVEJlWANxd4QzJI +EWF0YYgmYUcBU2YweFN0gTBlhQcRAWhyBGMXaEVVNINTUWiFAAA1NkJGcThVNGhC +FhWGdWZjclZiCGcwIjFDInCIdUVSZ3hVKFhlFkhTADJIUnQWBkEzFiWGQ4IygHAY +R2IHQEFWKINAgjdmOBUChCCCB0cXFVMRNiA2FAYoESIWRnADZ2I1eCFmJhNCFyIn +ZTAjBEYHJHGCQUJjc1R3hVASURNYaHUiEnQFCFUUVBNRg4N0ZAQ4YDiGFHU4cIQS +VhRSMQWFWCNEQTYCgXBSMjOCV0QAEodoNjdYYGcGEjJwdiWEUGM4SCBhg3QTRHUn +RyJmEXMmhRZEeHJUNoRFKCh4YEGANCMSWIgSZhMyUkN0B0WBEkECQEhohYE4VoGE +aFR0dxZFI4EDF3IWUjARMjIEF0BCBUdIRYMhYWGDAkNQIQYFNzcFc2MHc1WHdRKC +cWWIdgMmAkUFZINhJ3ZDYEOBhmJwV0RCJSiGNGQCYgSDQGgREmIAhwIYWCJIFQhn +AGNTeFMDJmeCRgKCYgZVQoZHJQKDMmFlJIhYFIAyIgU3eIVAAXhiQEIzhyc0coJo +WIRWcSWAJRBCJ0c2GBJ2E3InMyAyCIQxZgMQITAwdUSHQUBCBhCHBRMFJjU4QIg4 +QxFXQ0IXQRBTAwEIYCd0QwQxMgaHYDAWNoBER0g1IxIoNCOCESRIBUQ0iEBDQTJh +QCUTB0AUVIMzhYcUZicSJwZYiChiFwQ2IXWDYnBIEWeIMUJmAwcxgoVGFiVGOGiF +FjKCaFdiEwRIMYJEZ2hlVoViCGRQA1NiYyZCAIAnhRdEcgiGBFV2EHIScSQSM0IW +FhMWgHdCKEVQiCR1aBVXUxiBdUiEiFYngwVlQnhjdGEQdzR1EoFESDQxEmZiNYYD +IYh0RzcgQAMUgDNDRxSHR0ZVJGU2B4SGcRcHKBhjI2A0FVZCBkJwgAJBh1c2KDB4 +KFaBFVZEeHdgN4NwUgRzaBRVAgcVEzQWYFVyRhiCUBNShQJjADc3ckhXaCSAdjUE +BVYmhUQYWAJyABgWdDA2SIJlZ4clIzRFWCJoIDYoglIgJ0dFRlFDEjYYBlNUYhAl +J4MIQmCDcRMDVFB0JiBSIwOCMHNFEwIGZkM2dFI1AANGgTZXVSNnggUxhFR3VTVI +dyQWBSRTRRGEB2JnGIZ3h2CIVRAnMTAjUChxRENzRQMSU0CHhnU0ZgRTgjhVFANG +VRZ3gEZggHOHeGZwYFKBglU2QmYYchQhdYQDKHYXMlcTgig0IoIWBDUHRVNyWCNx +QoCCUUCGEwgXM2BSFSUIGIdGZmZiFCJzVUIwQgYzUocYBDg3hINGU0V0dCdiGGUm +gQdnRlVWYWIYYXaGJEUoUxEEWIZTd3NkQiQndwckJjKENCYCaGdUUGFYY3iFchCH +QjBICEgjh2GDZyJSI2EVWGd0Z3SDAReIJlGBRhIVdERSYIRyQzVQWHQSIQNUgWQ3 +cIEDFUFwABcEFiIjJGQ2hlUFBUh1YzUYFTU1B0NzcDdTdlFWFzZkQ0YVE3OBhAAH +JCQmEkE0A4OIYXFWiIYkMkeCMyKHU0VweCBSAEFmUQNAhgECCBJ3RwhBIxgzMRFS +F3NicSZxFnUQhzhAQnVXQCYwoJMztqTS3ornqSyWzqKb5H2eS7NFHRadEW9/1Sw2 +cp5uUA616JiKEi927yGJWADz9EVirkSZDDhJfKofmeH2vv4WyAwaWbBhnv3QGw7H +w/j+ihVcaMTauX3BM3NHUAJMrP+5NtacxS4ya54DQxl0U0gmDavrClKri5jsS4pS +mrgiDtFd+3adiFbl3m4Z+FBbOXsYf9VAZ2QMY8ebt0Y0i9HOwWrsz5jeyaBpyyAX +GbcsLC135SCj3171deRP2S8KGCxtuIAYVeCSp156yV9bOGShTyWaAXTnH3Ih0gJh +TiOtYba02pdazNq/yyr+LgSUdLCAI+YMZfjCanPGkIOrQWGSMuq/JfgFE5e/ylht +bLYamVTGijOR4xHmr2uMh2NE0/ILUziT3P9VP+GFwcfjTypePybBnrWLVYmCUg4C +jGTSUX92ZS6DCiilFMDk5SFwTKl/zHyvFNn9d3m7cED46YulN0f/oFCvEPmsnd9s +Ysl6HD4ZzvcTtvq2AmAblNukUpJO1eRqHdL6AvlanCpWbu9AmDpfB/GDfBhiIDfO +2IRL9JyK5u7vMG7Bo9NfCC4WQkkS97LVTyA1AmAIvp5C1k6dD9wli0po1XxvNfDt +86C1kDwhk7H2gfK2omcZNt1HitCuDzKH/VR23FkX4+IdSoW0ko+Hvyxb3O1tgyyd +NHGBJ3hCBhnH85rXIZ7T6q0b9Dq+Fa4LbB/eMAbXpDdrEPwf/k6BAEC0ch2j9wR9 +9vkzh2BIavIx6aXXVPDcDgd3xkMkD5DOsX9RTPXYcslS0u9GmAJ2UmBuZSFoo7zj +wU7s1c44pKoAan0s65MpgRUYXvdXyQ4rri2e2TjysJn5rG4VAbKkIW/6K8W7cvoD +0yF3RUVTSP8Rn4px34gxStX6cjUrpCa0azU/MIrUM/o/C4HFkUefjtoM/fs49l1J +BbdVgX1tftfHWGLZN/YxvwXRIPVLr1lArlNDBkd1ah13fN6znvzJ8L3p2+28BlWF +Icve8hzC4DjbXBJrMEBv9VeRuGhcsa9KRcmsANaNPjCGa77W1S2LgFcp8mK9cSW5 +HgOpTtnr0/fuwVMCvuSneBPJFdOLVrCQyrwDZYPxXdbbZwrOJ+jpPoiBE7DOcwwM +JD7wBkdeTXHNjjyazgUe8yn13zsP8vUeaazL9mQz0neWhvyjAbgSWZFDlVc4amxl +8WP8MyVbYBmorzpJhChl0usynsDOXC/Kd2sAiu2qdCBCNfPfXUe2BtCrZ8P8GySy +RnO8T1/P2Ha89bxjC8K4Uor5xX+SofZOFHOgh/hFH0BUPyodTXlg/FXmHUd1JEJm +5J/3DXJD/P0kkAdOFsndc5bffaxf9fNKZhCQk8gcizls2UH6IvJXZx12iK2U4mYB +jis2LC7rHiayywK5cC0x9EDYwjdsJT26bQiVU4Hmh99Q+kniCnv3I9OyyQ2yAz7S +pHoAX4dZK1EWwi3ED7TjK9jlJmZqMF+5Qeqcc5ZVnphSV9jY5tZ0hvhy6jcFDFa3 +inNX4B36nTj11G1BQRyiRLkhqTQSmE5eg6PxK2NywXoyhs4yul1fqUbvkfJbQfp0 +34nX4/keeErfxuXLJo8JvjQWByQk4xUxJtDnxo3VZcqvSQDwPBqk2j7OLVZ4Gj1Z +jqFDK3XR/DiClYaKqlN9cFYi6SKa5B/hlnh+hnOdq8YsC/YEIx/6TUiqh3HmnDa5 +CVou8bXj3+kGgsxc7RJH2rJ6MKwe36sXfOOxQcXe2knqVuTrYb0xGLaJXQHWC5iR +DQVjNqlQ0dARnmjISRBk103ZwbD0jVIKaDS1H3qb1fygqnigak+2nl3TDG5DyVEv +gaUaV2vHplfd6g4iArr16f3WClJoauiwbXw5HNyyTiYzC0okPS8teFZkG3mf/Pa7 +aINSUOyL+3TeBQA97j2XArMh03749V3p3NpeSj5reMY8liTxSjNrStaYK9jfeUfV +xzuh3+m2RJD1hDI9sHl5yJwiGoQ7gRfnCQaHlnrsst/BUHyF7v8T4N57O9o3PayK +/4GAWDyc7bzoEbOLUCbuWG/I/5PVgS3GblVcRTT3f3hAJMhTS3fVmQ9MpM/ipXFo +CZ3u2lcY+sdBfDVwO1C8CMQWsSFaFgD9m3LPPKmyaEokjlyqCmqdYWmEvOcUBVqP +qyUE1a7JTuBTME1xSFlSSyKyZiKuiy7FksjGCyXDceUXP1oBj0CnSdIhqht1nes7 +jrvCVr8xpro4AdmIqzFEHXy88tdQrDWmVWJXEWU7Nl/IcBffdFQbglDALXzBu5/7 +c9m8CXrcyfHSQLj9tJmv7WenQ663b2OhGpb3YH6q3Km3jiGYTgSUhIyr4XL+rxmR +aEMiUaEDH9R2zJL9XRrnF8vJvJdd0qLJw0q4E5GhyjodrQhTRmaVYK14bMmxWrMp +6n9wsdgSt/to423CSMFwiTGFBJ0E1W9ATdn2AJkv00xAHF1YkZNs27MsVo4dgzFq +6oXg595MgY0ngOrDeA+OscAxY/LwhH/iTs7KwUxd+rXy3n27t6X2M92zHxPiOdT9 +jhZ4E7TyydUbcGWkjQjbg7BjcVOGgT87VbhtLm8fZuFu7WtigsixiY0tqtuV/dUV +DFS1GEMa8LsBtWgd2zXEFAetf7PPrpI7ME/PBiUlpdGF0QjdfDq+miquUxO3D8Rt +rBUIOfvCqY2VM6LHgW7jiJlFeYbw36qr8wnuE5bAWmhC51XVyUH1Hg6BAiU8hIRw +eoglniyGkjLvhMB08fuEaEzFN0M5JEP9Xwg/9tzaeBHtcxmDvUaSYivSsp7HPtjc +y6a1lOcKwqY5oCoEaaIl48b2jQlK7yuR/3tdvTJBAdSlqTN8SLQZ/0aoY54GMql2 +SKMnlmAqHFtaG+itzn4uqFcZIUX61jWi6ZhFaSHY9uYau8Sd2/WVr4AYerL1AkYE +LpJQcbVGw1ga1xrA6ace9ikWkaWhbvuo/haTojXJ4eefUq8L85zht4c7bUUHnYWf +JokGBdFQdcFk9X039NbHw+zEoK7hLMfR056j+zNw3WEoKCPTSy//9NVMPr09HMK+ +gpzrhArLImI8f8LtswQrq2d2ub/iVeXqtXdChP7h4kPFfRKIedg1+oRwIb4gz+np +2jLSOj0m665fTtz+OkjltP2eC/5um6W7Stks5Y1O8jO/JoxMglExC801Q7TRyUX7 +yfpqNXXi8dFLAJK02hgZytpdW+qzChi2Gt5dG6u3jwUdR0k+CzwjICgC36kTS1op +6WA4LaSpKDYto0DxN2gU+OhTzQ7sJP2iITX+s0ppICi4/4gsT9d83wxmzX1wNHNt +5IXhW+3/ZbRiJ64cAMW1l3Zy +-----END PRIVATE KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-65-seed.pem b/test/fixtures/mldsa/ml-dsa-65-seed.pem new file mode 100644 index 00000000..21d6fcf8 --- /dev/null +++ b/test/fixtures/mldsa/ml-dsa-65-seed.pem @@ -0,0 +1,4 @@ +-----BEGIN PRIVATE KEY----- +MDQCAQAwCwYJYIZIAWUDBAMSBCKAIJAqqiFPbG0ZHTvcRTTX/HJupZUKRFNYYJiN +dS0lyk1O +-----END PRIVATE KEY----- diff --git a/test/fixtures/mldsa/ml-dsa-87-binary.sig b/test/fixtures/mldsa/ml-dsa-87-binary.sig new file mode 100644 index 00000000..8284db5d Binary files /dev/null and b/test/fixtures/mldsa/ml-dsa-87-binary.sig differ 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 00000000..9eaa7171 Binary files /dev/null and b/test/fixtures/mldsa/ml-dsa-87-empty.sig differ 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 00000000..5b6ab96f --- /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 00000000..ec7b2195 --- /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 00000000..4cae6006 --- /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 00000000..ba3e5588 --- /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 00000000..e65b8d0c --- /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 00000000..3caf84e0 --- /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 00000000..f9ce40b3 --- /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