Skip to content

Commit cfd0a44

Browse files
committed
Merge branch 'ky/ssl-add-certificate'
Merge GitHub Pull Request #167. * ky/ssl-add-certificate: test/test_ssl: fix test_security_level ssl: add SSLContext#add_certificate test/utils: remove a pointless .public_key call in issue_cert test/envutil: port assert_warning from Ruby trunk
2 parents 42e30d8 + 0b6ac1a commit cfd0a44

File tree

4 files changed

+241
-6
lines changed

4 files changed

+241
-6
lines changed

ext/openssl/ossl_ssl.c

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1213,6 +1213,114 @@ ossl_sslctx_enable_fallback_scsv(VALUE self)
12131213
}
12141214
#endif
12151215

1216+
/*
1217+
* call-seq:
1218+
* ctx.add_certificate(certiticate, pkey [, extra_certs]) -> self
1219+
*
1220+
* Adds a certificate to the context. _pkey_ must be a corresponding private
1221+
* key with _certificate_.
1222+
*
1223+
* Multiple certificates with different public key type can be added by
1224+
* repeated calls of this method, and OpenSSL will choose the most appropriate
1225+
* certificate during the handshake.
1226+
*
1227+
* #cert=, #key=, and #extra_chain_cert= are old accessor methods for setting
1228+
* certificate and internally call this method.
1229+
*
1230+
* === Parameters
1231+
* _certificate_::
1232+
* A certificate. An instance of OpenSSL::X509::Certificate.
1233+
* _pkey_::
1234+
* The private key for _certificate_. An instance of OpenSSL::PKey::PKey.
1235+
* _extra_certs_::
1236+
* Optional. An array of OpenSSL::X509::Certificate. When sending a
1237+
* certificate chain, the certificates specified by this are sent following
1238+
* _certificate_, in the order in the array.
1239+
*
1240+
* === Example
1241+
* rsa_cert = OpenSSL::X509::Certificate.new(...)
1242+
* rsa_pkey = OpenSSL::PKey.read(...)
1243+
* ca_intermediate_cert = OpenSSL::X509::Certificate.new(...)
1244+
* ctx.add_certificate(rsa_cert, rsa_pkey, [ca_intermediate_cert])
1245+
*
1246+
* ecdsa_cert = ...
1247+
* ecdsa_pkey = ...
1248+
* another_ca_cert = ...
1249+
* ctx.add_certificate(ecdsa_cert, ecdsa_pkey, [another_ca_cert])
1250+
*
1251+
* === Note
1252+
* OpenSSL before the version 1.0.2 could handle only one extra chain across
1253+
* all key types. Calling this method discards the chain set previously.
1254+
*/
1255+
static VALUE
1256+
ossl_sslctx_add_certificate(int argc, VALUE *argv, VALUE self)
1257+
{
1258+
VALUE cert, key, extra_chain_ary;
1259+
SSL_CTX *ctx;
1260+
X509 *x509;
1261+
STACK_OF(X509) *extra_chain = NULL;
1262+
EVP_PKEY *pkey, *pub_pkey;
1263+
1264+
GetSSLCTX(self, ctx);
1265+
rb_scan_args(argc, argv, "21", &cert, &key, &extra_chain_ary);
1266+
rb_check_frozen(self);
1267+
x509 = GetX509CertPtr(cert);
1268+
pkey = GetPrivPKeyPtr(key);
1269+
1270+
/*
1271+
* The reference counter is bumped, and decremented immediately.
1272+
* X509_get0_pubkey() is only available in OpenSSL >= 1.1.0.
1273+
*/
1274+
pub_pkey = X509_get_pubkey(x509);
1275+
EVP_PKEY_free(pub_pkey);
1276+
if (!pub_pkey)
1277+
rb_raise(rb_eArgError, "certificate does not contain public key");
1278+
if (EVP_PKEY_cmp(pub_pkey, pkey) != 1)
1279+
rb_raise(rb_eArgError, "public key mismatch");
1280+
1281+
if (argc >= 3)
1282+
extra_chain = ossl_x509_ary2sk(extra_chain_ary);
1283+
1284+
if (!SSL_CTX_use_certificate(ctx, x509)) {
1285+
sk_X509_pop_free(extra_chain, X509_free);
1286+
ossl_raise(eSSLError, "SSL_CTX_use_certificate");
1287+
}
1288+
if (!SSL_CTX_use_PrivateKey(ctx, pkey)) {
1289+
sk_X509_pop_free(extra_chain, X509_free);
1290+
ossl_raise(eSSLError, "SSL_CTX_use_PrivateKey");
1291+
}
1292+
1293+
if (extra_chain) {
1294+
#if OPENSSL_VERSION_NUMBER >= 0x10002000 && !defined(LIBRESSL_VERSION_NUMBER)
1295+
if (!SSL_CTX_set0_chain(ctx, extra_chain)) {
1296+
sk_X509_pop_free(extra_chain, X509_free);
1297+
ossl_raise(eSSLError, "SSL_CTX_set0_chain");
1298+
}
1299+
#else
1300+
STACK_OF(X509) *orig_extra_chain;
1301+
X509 *x509_tmp;
1302+
1303+
/* First, clear the existing chain */
1304+
SSL_CTX_get_extra_chain_certs(ctx, &orig_extra_chain);
1305+
if (orig_extra_chain && sk_X509_num(orig_extra_chain)) {
1306+
rb_warning("SSL_CTX_set0_chain() is not available; " \
1307+
"clearing previously set certificate chain");
1308+
SSL_CTX_clear_extra_chain_certs(ctx);
1309+
}
1310+
while ((x509_tmp = sk_X509_shift(extra_chain))) {
1311+
/* Transfers ownership */
1312+
if (!SSL_CTX_add_extra_chain_cert(ctx, x509_tmp)) {
1313+
X509_free(x509_tmp);
1314+
sk_X509_pop_free(extra_chain, X509_free);
1315+
ossl_raise(eSSLError, "SSL_CTX_add_extra_chain_cert");
1316+
}
1317+
}
1318+
sk_X509_free(extra_chain);
1319+
#endif
1320+
}
1321+
return self;
1322+
}
1323+
12161324
/*
12171325
* call-seq:
12181326
* ctx.session_add(session) -> true | false
@@ -2345,11 +2453,17 @@ Init_ossl_ssl(void)
23452453

23462454
/*
23472455
* Context certificate
2456+
*
2457+
* The _cert_, _key_, and _extra_chain_cert_ attributes are deprecated.
2458+
* It is recommended to use #add_certificate instead.
23482459
*/
23492460
rb_attr(cSSLContext, rb_intern("cert"), 1, 1, Qfalse);
23502461

23512462
/*
23522463
* Context private key
2464+
*
2465+
* The _cert_, _key_, and _extra_chain_cert_ attributes are deprecated.
2466+
* It is recommended to use #add_certificate instead.
23532467
*/
23542468
rb_attr(cSSLContext, rb_intern("key"), 1, 1, Qfalse);
23552469

@@ -2423,6 +2537,9 @@ Init_ossl_ssl(void)
24232537
/*
24242538
* An Array of extra X509 certificates to be added to the certificate
24252539
* chain.
2540+
*
2541+
* The _cert_, _key_, and _extra_chain_cert_ attributes are deprecated.
2542+
* It is recommended to use #add_certificate instead.
24262543
*/
24272544
rb_attr(cSSLContext, rb_intern("extra_chain_cert"), 1, 1, Qfalse);
24282545

@@ -2581,6 +2698,7 @@ Init_ossl_ssl(void)
25812698
#ifdef SSL_MODE_SEND_FALLBACK_SCSV
25822699
rb_define_method(cSSLContext, "enable_fallback_scsv", ossl_sslctx_enable_fallback_scsv, 0);
25832700
#endif
2701+
rb_define_method(cSSLContext, "add_certificate", ossl_sslctx_add_certificate, -1);
25842702

25852703
rb_define_method(cSSLContext, "setup", ossl_sslctx_setup, 0);
25862704
rb_define_alias(cSSLContext, "freeze", "setup");

test/envutil.rb

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,18 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr =
9292
end
9393
module_function :invoke_ruby
9494

95+
def verbose_warning
96+
class << (stderr = "".dup)
97+
alias write <<
98+
end
99+
stderr, $stderr, verbose, $VERBOSE = $stderr, stderr, $VERBOSE, true
100+
yield stderr
101+
return $stderr
102+
ensure
103+
stderr, $stderr, $VERBOSE = $stderr, stderr, verbose
104+
end
105+
module_function :verbose_warning
106+
95107
def suppress_warning
96108
verbose, $VERBOSE = $VERBOSE, nil
97109
yield
@@ -220,6 +232,17 @@ class Test::Unit::Runner
220232
raise marshal_error if marshal_error
221233
end
222234

235+
def assert_warning(pat, msg = nil)
236+
stderr = EnvUtil.verbose_warning {
237+
yield
238+
}
239+
if Regexp === pat
240+
assert_match pat, stderr, msg
241+
else
242+
assert_equal pat, stderr, msg
243+
end
244+
end
245+
223246
def message msg = nil, ending = ".", &default
224247
proc {
225248
msg = msg.call.chomp(".") if Proc === msg

test/test_ssl.rb

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,87 @@ def test_ssl_with_server_cert
5454
}
5555
end
5656

57+
def test_add_certificate
58+
ctx_proc = -> ctx {
59+
# Unset values set by start_server
60+
ctx.cert = ctx.key = ctx.extra_chain_cert = nil
61+
ctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA
62+
}
63+
start_server(ctx_proc: ctx_proc) do |port|
64+
server_connect(port) { |ssl|
65+
assert_equal @svr_cert.subject, ssl.peer_cert.subject
66+
assert_equal [@svr_cert.subject, @ca_cert.subject],
67+
ssl.peer_cert_chain.map(&:subject)
68+
}
69+
end
70+
end
71+
72+
def test_add_certificate_multiple_certs
73+
pend "EC is not supported" unless defined?(OpenSSL::PKey::EC)
74+
pend "TLS 1.2 is not supported" unless tls12_supported?
75+
76+
# SSL_CTX_set0_chain() is needed for setting multiple certificate chains
77+
add0_chain_supported = openssl?(1, 0, 2)
78+
79+
if add0_chain_supported
80+
ca2_key = Fixtures.pkey("rsa1024")
81+
ca2_exts = [
82+
["basicConstraints", "CA:TRUE", true],
83+
["keyUsage", "cRLSign, keyCertSign", true],
84+
]
85+
ca2_dn = OpenSSL::X509::Name.parse_rfc2253("CN=CA2")
86+
ca2_cert = issue_cert(ca2_dn, ca2_key, 123, ca2_exts, nil, nil)
87+
else
88+
# Use the same CA as @svr_cert
89+
ca2_key = @ca_key; ca2_cert = @ca_cert
90+
end
91+
92+
ecdsa_key = Fixtures.pkey("p256")
93+
exts = [
94+
["keyUsage", "digitalSignature", false],
95+
]
96+
ecdsa_dn = OpenSSL::X509::Name.parse_rfc2253("CN=localhost2")
97+
ecdsa_cert = issue_cert(ecdsa_dn, ecdsa_key, 456, exts, ca2_cert, ca2_key)
98+
99+
if !add0_chain_supported
100+
# Testing the warning emitted when 'extra' chain is replaced
101+
tctx = OpenSSL::SSL::SSLContext.new
102+
tctx.add_certificate(@svr_cert, @svr_key, [@ca_cert])
103+
assert_warning(/set0_chain/) {
104+
tctx.add_certificate(ecdsa_cert, ecdsa_key, [ca2_cert])
105+
}
106+
end
107+
108+
ctx_proc = -> ctx {
109+
# Unset values set by start_server
110+
ctx.cert = ctx.key = ctx.extra_chain_cert = nil
111+
ctx.ecdh_curves = "P-256" unless openssl?(1, 0, 2)
112+
ctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA
113+
EnvUtil.suppress_warning do # !add0_chain_supported
114+
ctx.add_certificate(ecdsa_cert, ecdsa_key, [ca2_cert])
115+
end
116+
}
117+
start_server(ctx_proc: ctx_proc) do |port|
118+
ctx = OpenSSL::SSL::SSLContext.new
119+
ctx.max_version = :TLS1_2 # TODO: We need this to force certificate type
120+
ctx.ciphers = "aECDSA"
121+
server_connect(port, ctx) { |ssl|
122+
assert_equal ecdsa_cert.subject, ssl.peer_cert.subject
123+
assert_equal [ecdsa_cert.subject, ca2_cert.subject],
124+
ssl.peer_cert_chain.map(&:subject)
125+
}
126+
127+
ctx = OpenSSL::SSL::SSLContext.new
128+
ctx.max_version = :TLS1_2
129+
ctx.ciphers = "aRSA"
130+
server_connect(port, ctx) { |ssl|
131+
assert_equal @svr_cert.subject, ssl.peer_cert.subject
132+
assert_equal [@svr_cert.subject, @ca_cert.subject],
133+
ssl.peer_cert_chain.map(&:subject)
134+
}
135+
end
136+
end
137+
57138
def test_sysread_and_syswrite
58139
start_server { |port|
59140
server_connect(port) { |ssl|
@@ -1389,11 +1470,24 @@ def test_security_level
13891470
return
13901471
end
13911472
assert_equal(1, ctx.security_level)
1392-
# assert_raise(OpenSSL::SSL::SSLError) { ctx.key = Fixtures.pkey("dsa512") }
1393-
# ctx.key = Fixtures.pkey("rsa1024")
1394-
# ctx.security_level = 2
1395-
# assert_raise(OpenSSL::SSL::SSLError) { ctx.key = Fixtures.pkey("rsa1024") }
1396-
pend "FIXME: SSLContext#key= currently does not raise because SSL_CTX_use_certificate() is delayed"
1473+
1474+
dsa512 = Fixtures.pkey("dsa512")
1475+
dsa512_cert = issue_cert(@svr, dsa512, 50, [], @ca_cert, @ca_key)
1476+
rsa1024 = Fixtures.pkey("rsa1024")
1477+
rsa1024_cert = issue_cert(@svr, rsa1024, 51, [], @ca_cert, @ca_key)
1478+
1479+
assert_raise(OpenSSL::SSL::SSLError) {
1480+
# 512 bit DSA key is rejected because it offers < 80 bits of security
1481+
ctx.add_certificate(dsa512_cert, dsa512)
1482+
}
1483+
assert_nothing_raised {
1484+
ctx.add_certificate(rsa1024_cert, rsa1024)
1485+
}
1486+
ctx.security_level = 2
1487+
assert_raise(OpenSSL::SSL::SSLError) {
1488+
# < 112 bits of security
1489+
ctx.add_certificate(rsa1024_cert, rsa1024)
1490+
}
13971491
end
13981492

13991493
def test_dup

test/utils.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def issue_cert(dn, key, serial, extensions, issuer, issuer_key,
6767
cert.serial = serial
6868
cert.subject = dn
6969
cert.issuer = issuer.subject
70-
cert.public_key = key.public_key
70+
cert.public_key = key
7171
now = Time.now
7272
cert.not_before = not_before || now - 3600
7373
cert.not_after = not_after || now + 3600

0 commit comments

Comments
 (0)