Skip to content

Commit 9a43535

Browse files
hsbtclaude
andcommitted
Gate ML-DSA tests on what they actually need
The signing tests were gated on the PQC handshake probe, which needs SSLContext#groups=. On OpenSSL >= 3.5 with Ruby OpenSSL < 4.0 all 23 were omitted even though ML-DSA keys work there, so the core of this PR went unverified. They now gate on ML-DSA key support, and the five that build a certificate gate on nil-digest X509 signing, which Ruby OpenSSL only accepts from 3.3 on. The algorithm assertions read the SubjectPublicKeyInfo instead of PKey#inspect, whose type_name field only exists from Ruby OpenSSL 4.0. The handshake probe also returned nil on its early exits, so its memoization never took and it rebound a TCP socket per call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent 62a5df0 commit 9a43535

7 files changed

Lines changed: 74 additions & 28 deletions

test/rubygems/helper.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1692,6 +1692,16 @@ def omit_unless_support_pqc
16921692
end
16931693
end
16941694

1695+
def omit_unless_support_ml_dsa_key
1696+
omit "OpenSSL does not support ML-DSA" unless
1697+
Gem::PQCUtilities.support_ml_dsa_key?
1698+
end
1699+
1700+
def omit_unless_support_ml_dsa_cert
1701+
omit "Ruby OpenSSL cannot sign a certificate with an ML-DSA key" unless
1702+
Gem::PQCUtilities.support_ml_dsa_cert?
1703+
end
1704+
16951705
def omit_if_support_ml_dsa_key
16961706
omit "OpenSSL supports ML-DSA" if Gem::PQCUtilities.support_ml_dsa_key?
16971707
end

test/rubygems/pqc_utilities.rb

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,41 @@ def self.support_ml_dsa_key?
5252
end
5353
end
5454

55+
##
56+
# Returns whether the runtime can sign an X.509 certificate with an ML-DSA
57+
# key. Ruby OpenSSL rejects the nil digest that needs before 3.3, so
58+
# support_ml_dsa_key? alone does not cover certificate building.
59+
60+
def self.support_ml_dsa_cert?
61+
return @support_ml_dsa_cert unless @support_ml_dsa_cert.nil?
62+
63+
@support_ml_dsa_cert =
64+
begin
65+
key = OpenSSL::PKey.generate_key("ML-DSA-65")
66+
cert = OpenSSL::X509::Certificate.new
67+
cert.subject = cert.issuer = OpenSSL::X509::Name.new([["CN", "probe"]])
68+
cert.public_key = OpenSSL::PKey.read(key.public_to_pem)
69+
cert.not_before = Time.now
70+
cert.not_after = Time.now + 60
71+
cert.sign(key, nil)
72+
true
73+
# NoMethodError: JRuby's Ruby OpenSSL lacks generate_key.
74+
# TypeError: Ruby OpenSSL < 3.3 rejects a nil digest here.
75+
rescue OpenSSL::PKey::PKeyError, OpenSSL::X509::CertificateError,
76+
NoMethodError, TypeError
77+
false
78+
end
79+
end
80+
81+
##
82+
# Returns the algorithm named in the SubjectPublicKeyInfo of +key+, such as
83+
# "ML-DSA-65". OpenSSL::PKey::PKey#inspect only names the algorithm on Ruby
84+
# OpenSSL >= 4.0, and #oid raises for the provider-backed keys ML-DSA uses.
85+
86+
def self.key_algorithm_name(key)
87+
OpenSSL::ASN1.decode(key.public_to_der).value.first.value.first.ln
88+
end
89+
5590
# Probe an actual PQC handshake between a forced-PQC server and a
5691
# default-configured client, mirroring what the integration tests exercise.
5792
# Memoized so the probe runs at most once per process.
@@ -68,10 +103,10 @@ def self.probe_pqc_handshake
68103
ctx.key = Gem::PEMUtilities::MLDSA65_SSL_KEY
69104
# ctx.key is nil when unsupported ML-DSA-65 algorithm's file is read with
70105
# old OpenSSL versions.
71-
return nil unless ctx.key
106+
return false unless ctx.key
72107

73108
# ctx.groups (OpenSSL::SSL::SSLContext#groups) requires Ruby OpenSSL >= 4.0.
74-
return nil unless ctx.respond_to?(:groups=)
109+
return false unless ctx.respond_to?(:groups=)
75110

76111
ctx.groups = "X25519MLKEM768"
77112
ssl_server = OpenSSL::SSL::SSLServer.new(server, ctx)

test/rubygems/test_gem_commands_build_command.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ def test_build_signed_gem
609609
def test_build_signed_gem_ml_dsa_65
610610
pend "openssl is missing" unless Gem::HAVE_OPENSSL && !Gem.java_platform?
611611

612-
omit_unless_support_pqc
612+
omit_unless_support_ml_dsa_key
613613

614614
trust_dir = Gem::Security.trust_dir
615615

test/rubygems/test_gem_commands_cert_command.rb

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ def test_execute_build_key_algorithm_ec_key
158158
end
159159

160160
def test_execute_build_key_algorithm_ml_dsa_65_key
161-
omit_unless_support_pqc
161+
omit_unless_support_ml_dsa_cert
162162

163163
passphrase = "Foo bar"
164164

@@ -193,7 +193,8 @@ def test_execute_build_key_algorithm_ml_dsa_65_key
193193
assert_path_exist cert_path
194194
cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
195195
assert cert.public_key.is_a? OpenSSL::PKey::PKey
196-
assert_match(/type_name=ML-DSA-65/, cert.public_key.inspect)
196+
assert_equal "ML-DSA-65",
197+
Gem::PQCUtilities.key_algorithm_name(cert.public_key)
197198
end
198199

199200
def test_execute_build_key_algorithm_ml_dsa_65_key_without_ml_dsa_support
@@ -390,7 +391,7 @@ def test_execute_build_bad_key
390391
end
391392

392393
def test_execute_build_ml_dsa_65_key
393-
omit_unless_support_pqc
394+
omit_unless_support_ml_dsa_cert
394395

395396
@cmd.handle_options %W[
396397
--build nobody@example.com
@@ -414,7 +415,7 @@ def test_execute_build_ml_dsa_65_key
414415
end
415416

416417
def test_execute_build_encrypted_ml_dsa_65_key
417-
omit_unless_support_pqc
418+
omit_unless_support_ml_dsa_cert
418419

419420
@cmd.handle_options %W[
420421
--build nobody@example.com
@@ -448,7 +449,7 @@ def test_execute_certificate
448449
end
449450

450451
def test_execute_certificate_ml_dsa_65
451-
omit_unless_support_pqc
452+
omit_unless_support_ml_dsa_key
452453

453454
use_ui @ui do
454455
@cmd.handle_options %W[--certificate #{ML_DSA_65_PUBLIC_CERT_FILE}]
@@ -512,7 +513,7 @@ def test_execute_encrypted_private_key
512513
end
513514

514515
def test_execute_private_ml_dsa_65_key
515-
omit_unless_support_pqc
516+
omit_unless_support_ml_dsa_key
516517

517518
use_ui @ui do
518519
@cmd.send :handle_options, %W[--private-key #{ML_DSA_65_PRIVATE_KEY_FILE}]
@@ -538,7 +539,7 @@ def test_execute_private_ml_dsa_65_key_without_ml_dsa_support
538539
end
539540

540541
def test_execute_encrypted_private_ml_dsa_65_key
541-
omit_unless_support_pqc
542+
omit_unless_support_ml_dsa_key
542543

543544
use_ui @ui do
544545
@cmd.send :handle_options,

test/rubygems/test_gem_security.rb

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -104,30 +104,30 @@ def test_class_create_key_ec
104104
end
105105

106106
def test_class_create_key_ml_dsa_44
107-
omit_unless_support_pqc
107+
omit_unless_support_ml_dsa_key
108108

109109
key = Gem::Security.create_key "ml-dsa-44"
110110

111111
assert_kind_of OpenSSL::PKey::PKey, key
112-
assert_match(/type_name=ML-DSA-44/, key.inspect)
112+
assert_equal "ML-DSA-44", Gem::PQCUtilities.key_algorithm_name(key)
113113
end
114114

115115
def test_class_create_key_ml_dsa_65
116-
omit_unless_support_pqc
116+
omit_unless_support_ml_dsa_key
117117

118118
key = Gem::Security.create_key "ml-dsa-65"
119119

120120
assert_kind_of OpenSSL::PKey::PKey, key
121-
assert_match(/type_name=ML-DSA-65/, key.inspect)
121+
assert_equal "ML-DSA-65", Gem::PQCUtilities.key_algorithm_name(key)
122122
end
123123

124124
def test_class_create_key_ml_dsa_87
125-
omit_unless_support_pqc
125+
omit_unless_support_ml_dsa_key
126126

127127
key = Gem::Security.create_key "ml-dsa-87"
128128

129129
assert_kind_of OpenSSL::PKey::PKey, key
130-
assert_match(/type_name=ML-DSA-87/, key.inspect)
130+
assert_equal "ML-DSA-87", Gem::PQCUtilities.key_algorithm_name(key)
131131
end
132132

133133
def test_class_create_key_ml_dsa_44_without_ml_dsa_support
@@ -207,7 +207,7 @@ def test_class_digest_required_raises_unsupported_algorithm
207207
end
208208

209209
def test_class_digest_required_ml_dsa_65
210-
omit_unless_support_pqc
210+
omit_unless_support_ml_dsa_key
211211

212212
refute Gem::Security.digest_required?(Gem::Security.create_key("ml-dsa-65"))
213213
end
@@ -225,7 +225,7 @@ def test_class_get_public_key_ec
225225
end
226226

227227
def test_class_get_public_key_ml_dsa_65
228-
omit_unless_support_pqc
228+
omit_unless_support_ml_dsa_key
229229

230230
pkey = Gem::Security.get_public_key(ML_DSA_65_PRIVATE_KEY)
231231

@@ -334,7 +334,7 @@ def test_class_sign_AltName
334334
end
335335

336336
def test_class_sign_ml_dsa_65
337-
omit_unless_support_pqc
337+
omit_unless_support_ml_dsa_cert
338338

339339
assert_sign ML_DSA_65_PUBLIC_CERT, ML_DSA_65_PRIVATE_KEY
340340
end
@@ -362,7 +362,7 @@ def test_class_write_private_key
362362
end
363363

364364
def test_class_write_private_key_ml_dsa_65
365-
omit_unless_support_pqc
365+
omit_unless_support_ml_dsa_key
366366

367367
key = Gem::Security.create_key "ml-dsa-65"
368368

@@ -394,7 +394,7 @@ def test_class_write_private_key_encrypted
394394
end
395395

396396
def test_class_write_private_key_encrypted_ml_dsa_65
397-
omit_unless_support_pqc
397+
omit_unless_support_ml_dsa_key
398398

399399
key = Gem::Security.create_key "ml-dsa-65"
400400

@@ -433,7 +433,7 @@ def test_class_write_private_key_encrypted_cipher
433433
end
434434

435435
def test_class_write_private_key_encrypted_cipher_ml_dsa_65
436-
omit_unless_support_pqc
436+
omit_unless_support_ml_dsa_key
437437

438438
key = Gem::Security.create_key "ml-dsa-65"
439439

test/rubygems/test_gem_security_policy.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ def test_check_data
5454
end
5555

5656
def test_check_data_ml_dsa_65
57-
omit_unless_support_pqc
57+
omit_unless_support_ml_dsa_key
5858

5959
data = digest "hello"
6060

@@ -78,7 +78,7 @@ def test_check_data_invalid
7878
end
7979

8080
def test_check_data_invalid_ml_dsa_65
81-
omit_unless_support_pqc
81+
omit_unless_support_ml_dsa_key
8282

8383
data = digest "hello"
8484

@@ -249,7 +249,7 @@ def test_check_trust
249249
end
250250

251251
def test_check_trust_ml_dsa_65
252-
omit_unless_support_pqc
252+
omit_unless_support_ml_dsa_key
253253

254254
Gem::Security.trust_dir.trust_cert ML_DSA_65_PUBLIC_CERT
255255

test/rubygems/test_gem_security_signer.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ def test_initialize_key_path
7070
end
7171

7272
def test_initialize_key_path_ml_dsa_65
73-
omit_unless_support_pqc
73+
omit_unless_support_ml_dsa_key
7474

7575
key_file = ML_DSA_65_PRIVATE_KEY_FILE
7676

@@ -103,7 +103,7 @@ def test_initialize_encrypted_key_path
103103
end
104104

105105
def test_initialize_encrypted_key_path_ml_dsa_65
106-
omit_unless_support_pqc
106+
omit_unless_support_ml_dsa_key
107107

108108
key_file = ML_DSA_65_ENCRYPTED_PRIVATE_KEY_FILE
109109

@@ -153,7 +153,7 @@ def test_sign
153153
end
154154

155155
def test_sign_ml_dsa_65
156-
omit_unless_support_pqc
156+
omit_unless_support_ml_dsa_key
157157

158158
signer = Gem::Security::Signer.new ML_DSA_65_PRIVATE_KEY, [ML_DSA_65_PUBLIC_CERT]
159159

0 commit comments

Comments
 (0)