Skip to content

ssl: add SSLContext#add_certificate #167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 22, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions ext/openssl/ossl_ssl.c
Original file line number Diff line number Diff line change
Expand Up @@ -1193,6 +1193,114 @@ ossl_sslctx_set_security_level(VALUE self, VALUE value)
return value;
}

/*
* call-seq:
* ctx.add_certificate(certiticate, pkey [, extra_certs]) -> self
*
* Adds a certificate to the context. _pkey_ must be a corresponding private
* key with _certificate_.
*
* Multiple certificates with different public key type can be added by
* repeated calls of this method, and OpenSSL will choose the most appropriate
* certificate during the handshake.
*
* #cert=, #key=, and #extra_chain_cert= are old accessor methods for setting
* certificate and internally call this method.
*
* === Parameters
* _certificate_::
* A certificate. An instance of OpenSSL::X509::Certificate.
* _pkey_::
* The private key for _certificate_. An instance of OpenSSL::PKey::PKey.
* _extra_certs_::
* Optional. An array of OpenSSL::X509::Certificate. When sending a
* certificate chain, the certificates specified by this are sent following
* _certificate_, in the order in the array.
*
* === Example
* rsa_cert = OpenSSL::X509::Certificate.new(...)
* rsa_pkey = OpenSSL::PKey.read(...)
* ca_intermediate_cert = OpenSSL::X509::Certificate.new(...)
* ctx.add_certificate(rsa_cert, rsa_pkey, [ca_intermediate_cert])
*
* ecdsa_cert = ...
* ecdsa_pkey = ...
* another_ca_cert = ...
* ctx.add_certificate(ecdsa_cert, ecdsa_pkey, [another_ca_cert])
*
* === Note
* OpenSSL before the version 1.0.2 could handle only one extra chain across
* all key types. Calling this method discards the chain set previously.
*/
static VALUE
ossl_sslctx_add_certificate(int argc, VALUE *argv, VALUE self)
{
VALUE cert, key, extra_chain_ary;
SSL_CTX *ctx;
X509 *x509;
STACK_OF(X509) *extra_chain = NULL;
EVP_PKEY *pkey, *pub_pkey;

GetSSLCTX(self, ctx);
rb_scan_args(argc, argv, "21", &cert, &key, &extra_chain_ary);
rb_check_frozen(self);
x509 = GetX509CertPtr(cert);
pkey = GetPrivPKeyPtr(key);

/*
* The reference counter is bumped, and decremented immediately.
* X509_get0_pubkey() is only available in OpenSSL >= 1.1.0.
*/
pub_pkey = X509_get_pubkey(x509);
EVP_PKEY_free(pub_pkey);
if (!pub_pkey)
rb_raise(rb_eArgError, "certificate does not contain public key");
if (EVP_PKEY_cmp(pub_pkey, pkey) != 1)
rb_raise(rb_eArgError, "public key mismatch");

if (argc >= 3)
extra_chain = ossl_x509_ary2sk(extra_chain_ary);

if (!SSL_CTX_use_certificate(ctx, x509)) {
sk_X509_pop_free(extra_chain, X509_free);
ossl_raise(eSSLError, "SSL_CTX_use_certificate");
}
if (!SSL_CTX_use_PrivateKey(ctx, pkey)) {
sk_X509_pop_free(extra_chain, X509_free);
ossl_raise(eSSLError, "SSL_CTX_use_PrivateKey");
}

if (extra_chain) {
#if OPENSSL_VERSION_NUMBER >= 0x10002000 && !defined(LIBRESSL_VERSION_NUMBER)
if (!SSL_CTX_set0_chain(ctx, extra_chain)) {
sk_X509_pop_free(extra_chain, X509_free);
ossl_raise(eSSLError, "SSL_CTX_set0_chain");
}
#else
STACK_OF(X509) *orig_extra_chain;
X509 *x509_tmp;

/* First, clear the existing chain */
SSL_CTX_get_extra_chain_certs(ctx, &orig_extra_chain);
if (orig_extra_chain && sk_X509_num(orig_extra_chain)) {
rb_warning("SSL_CTX_set0_chain() is not available; " \
"clearing previously set certificate chain");
SSL_CTX_clear_extra_chain_certs(ctx);
}
while ((x509_tmp = sk_X509_shift(extra_chain))) {
/* Transfers ownership */
if (!SSL_CTX_add_extra_chain_cert(ctx, x509_tmp)) {
X509_free(x509_tmp);
sk_X509_pop_free(extra_chain, X509_free);
ossl_raise(eSSLError, "SSL_CTX_add_extra_chain_cert");
}
}
sk_X509_free(extra_chain);
#endif
}
return self;
}

/*
* call-seq:
* ctx.session_add(session) -> true | false
Expand Down Expand Up @@ -2324,11 +2432,17 @@ Init_ossl_ssl(void)

/*
* Context certificate
*
* The _cert_, _key_, and _extra_chain_cert_ attributes are deprecated.
* It is recommended to use #add_certificate instead.
*/
rb_attr(cSSLContext, rb_intern("cert"), 1, 1, Qfalse);

/*
* Context private key
*
* The _cert_, _key_, and _extra_chain_cert_ attributes are deprecated.
* It is recommended to use #add_certificate instead.
*/
rb_attr(cSSLContext, rb_intern("key"), 1, 1, Qfalse);

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

Expand Down Expand Up @@ -2557,6 +2674,7 @@ Init_ossl_ssl(void)
rb_define_method(cSSLContext, "ecdh_curves=", ossl_sslctx_set_ecdh_curves, 1);
rb_define_method(cSSLContext, "security_level", ossl_sslctx_get_security_level, 0);
rb_define_method(cSSLContext, "security_level=", ossl_sslctx_set_security_level, 1);
rb_define_method(cSSLContext, "add_certificate", ossl_sslctx_add_certificate, -1);

rb_define_method(cSSLContext, "setup", ossl_sslctx_setup, 0);
rb_define_alias(cSSLContext, "freeze", "setup");
Expand Down
23 changes: 23 additions & 0 deletions test/envutil.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr =
end
module_function :invoke_ruby

def verbose_warning
class << (stderr = "".dup)
alias write <<
end
stderr, $stderr, verbose, $VERBOSE = $stderr, stderr, $VERBOSE, true
yield stderr
return $stderr
ensure
stderr, $stderr, $VERBOSE = $stderr, stderr, verbose
end
module_function :verbose_warning

def suppress_warning
verbose, $VERBOSE = $VERBOSE, nil
yield
Expand Down Expand Up @@ -220,6 +232,17 @@ class Test::Unit::Runner
raise marshal_error if marshal_error
end

def assert_warning(pat, msg = nil)
stderr = EnvUtil.verbose_warning {
yield
}
if Regexp === pat
assert_match pat, stderr, msg
else
assert_equal pat, stderr, msg
end
end

def message msg = nil, ending = ".", &default
proc {
msg = msg.call.chomp(".") if Proc === msg
Expand Down
104 changes: 99 additions & 5 deletions test/test_ssl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,87 @@ def test_ssl_with_server_cert
}
end

def test_add_certificate
ctx_proc = -> ctx {
# Unset values set by start_server
ctx.cert = ctx.key = ctx.extra_chain_cert = nil
ctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA
}
start_server(ctx_proc: ctx_proc) do |port|
server_connect(port) { |ssl|
assert_equal @svr_cert.subject, ssl.peer_cert.subject
assert_equal [@svr_cert.subject, @ca_cert.subject],
ssl.peer_cert_chain.map(&:subject)
}
end
end

def test_add_certificate_multiple_certs
pend "EC is not supported" unless defined?(OpenSSL::PKey::EC)
pend "TLS 1.2 is not supported" unless tls12_supported?

# SSL_CTX_set0_chain() is needed for setting multiple certificate chains
add0_chain_supported = openssl?(1, 0, 2)

if add0_chain_supported
ca2_key = Fixtures.pkey("rsa1024")
ca2_exts = [
["basicConstraints", "CA:TRUE", true],
["keyUsage", "cRLSign, keyCertSign", true],
]
ca2_dn = OpenSSL::X509::Name.parse_rfc2253("CN=CA2")
ca2_cert = issue_cert(ca2_dn, ca2_key, 123, ca2_exts, nil, nil)
else
# Use the same CA as @svr_cert
ca2_key = @ca_key; ca2_cert = @ca_cert
end

ecdsa_key = Fixtures.pkey("p256")
exts = [
["keyUsage", "digitalSignature", false],
]
ecdsa_dn = OpenSSL::X509::Name.parse_rfc2253("CN=localhost2")
ecdsa_cert = issue_cert(ecdsa_dn, ecdsa_key, 456, exts, ca2_cert, ca2_key)

if !add0_chain_supported
# Testing the warning emitted when 'extra' chain is replaced
tctx = OpenSSL::SSL::SSLContext.new
tctx.add_certificate(@svr_cert, @svr_key, [@ca_cert])
assert_warning(/set0_chain/) {
tctx.add_certificate(ecdsa_cert, ecdsa_key, [ca2_cert])
}
end

ctx_proc = -> ctx {
# Unset values set by start_server
ctx.cert = ctx.key = ctx.extra_chain_cert = nil
ctx.ecdh_curves = "P-256" unless openssl?(1, 0, 2)
ctx.add_certificate(@svr_cert, @svr_key, [@ca_cert]) # RSA
EnvUtil.suppress_warning do # !add0_chain_supported
ctx.add_certificate(ecdsa_cert, ecdsa_key, [ca2_cert])
end
}
start_server(ctx_proc: ctx_proc) do |port|
ctx = OpenSSL::SSL::SSLContext.new
ctx.max_version = :TLS1_2 # TODO: We need this to force certificate type
ctx.ciphers = "aECDSA"
server_connect(port, ctx) { |ssl|
assert_equal ecdsa_cert.subject, ssl.peer_cert.subject
assert_equal [ecdsa_cert.subject, ca2_cert.subject],
ssl.peer_cert_chain.map(&:subject)
}

ctx = OpenSSL::SSL::SSLContext.new
ctx.max_version = :TLS1_2
ctx.ciphers = "aRSA"
server_connect(port, ctx) { |ssl|
assert_equal @svr_cert.subject, ssl.peer_cert.subject
assert_equal [@svr_cert.subject, @ca_cert.subject],
ssl.peer_cert_chain.map(&:subject)
}
end
end

def test_sysread_and_syswrite
start_server { |port|
server_connect(port) { |ssl|
Expand Down Expand Up @@ -1318,11 +1399,24 @@ def test_security_level
return
end
assert_equal(1, ctx.security_level)
# assert_raise(OpenSSL::SSL::SSLError) { ctx.key = Fixtures.pkey("dsa512") }
# ctx.key = Fixtures.pkey("rsa1024")
# ctx.security_level = 2
# assert_raise(OpenSSL::SSL::SSLError) { ctx.key = Fixtures.pkey("rsa1024") }
pend "FIXME: SSLContext#key= currently does not raise because SSL_CTX_use_certificate() is delayed"

dsa512 = Fixtures.pkey("dsa512")
dsa512_cert = issue_cert(@svr, dsa512, 50, [], @ca_cert, @ca_key)
rsa1024 = Fixtures.pkey("rsa1024")
rsa1024_cert = issue_cert(@svr, rsa1024, 51, [], @ca_cert, @ca_key)

assert_raise(OpenSSL::SSL::SSLError) {
# 512 bit DSA key is rejected because it offers < 80 bits of security
ctx.add_certificate(dsa512_cert, dsa512)
}
assert_nothing_raised {
ctx.add_certificate(rsa1024_cert, rsa1024)
}
ctx.security_level = 2
assert_raise(OpenSSL::SSL::SSLError) {
# < 112 bits of security
ctx.add_certificate(rsa1024_cert, rsa1024)
}
end

def test_dup
Expand Down
2 changes: 1 addition & 1 deletion test/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def issue_cert(dn, key, serial, extensions, issuer, issuer_key,
cert.serial = serial
cert.subject = dn
cert.issuer = issuer.subject
cert.public_key = key.public_key
cert.public_key = key
now = Time.now
cert.not_before = not_before || now - 3600
cert.not_after = not_after || now + 3600
Expand Down