Skip to content

Commit 6ce04c9

Browse files
committed
ssl: add SSLContext#add_certificate
Add a new method to add a certificate, a corresponding private key, and extra CA certificates at once. This has two advantages over the existing {cert,key,extra_cert_chain} attributes: 1. We can notice the problem with the certificate and/or the private key. Since the existing attributes are simple instance variables, they aren't set to the SSL_CTX until #setup which usually happens on the first connection. 2. For the same reason, existing attributes allowed only one certificate for a context, even though OpenSSL itself is capable of handling multiple certificates and selecting the most appropriate one according to the cipher suite selected. The documentation for the existing attributes are updated to recommend using #add_certificate.
1 parent 125ab88 commit 6ce04c9

File tree

2 files changed

+199
-0
lines changed

2 files changed

+199
-0
lines changed

ext/openssl/ossl_ssl.c

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,114 @@ ossl_sslctx_set_security_level(VALUE self, VALUE value)
11931193
return value;
11941194
}
11951195

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

23252433
/*
23262434
* Context certificate
2435+
*
2436+
* The _cert_, _key_, and _extra_chain_cert_ attributes are deprecated.
2437+
* It is recommended to use #add_certificate instead.
23272438
*/
23282439
rb_attr(cSSLContext, rb_intern("cert"), 1, 1, Qfalse);
23292440

23302441
/*
23312442
* Context private key
2443+
*
2444+
* The _cert_, _key_, and _extra_chain_cert_ attributes are deprecated.
2445+
* It is recommended to use #add_certificate instead.
23322446
*/
23332447
rb_attr(cSSLContext, rb_intern("key"), 1, 1, Qfalse);
23342448

@@ -2402,6 +2516,9 @@ Init_ossl_ssl(void)
24022516
/*
24032517
* An Array of extra X509 certificates to be added to the certificate
24042518
* chain.
2519+
*
2520+
* The _cert_, _key_, and _extra_chain_cert_ attributes are deprecated.
2521+
* It is recommended to use #add_certificate instead.
24052522
*/
24062523
rb_attr(cSSLContext, rb_intern("extra_chain_cert"), 1, 1, Qfalse);
24072524

@@ -2557,6 +2674,7 @@ Init_ossl_ssl(void)
25572674
rb_define_method(cSSLContext, "ecdh_curves=", ossl_sslctx_set_ecdh_curves, 1);
25582675
rb_define_method(cSSLContext, "security_level", ossl_sslctx_get_security_level, 0);
25592676
rb_define_method(cSSLContext, "security_level=", ossl_sslctx_set_security_level, 1);
2677+
rb_define_method(cSSLContext, "add_certificate", ossl_sslctx_add_certificate, -1);
25602678

25612679
rb_define_method(cSSLContext, "setup", ossl_sslctx_setup, 0);
25622680
rb_define_alias(cSSLContext, "freeze", "setup");

test/test_ssl.rb

Lines changed: 81 additions & 0 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|

0 commit comments

Comments
 (0)