Skip to content

Commit

Permalink
Support for cert_pem and key_pem with ssl_bind DSL (puma#2728)
Browse files Browse the repository at this point in the history
* Fix deprecation warning

DEPRECATED: Use assert_nil if expecting nil from test/test_binder.rb:265. This will fail in Minitest 6.

* Extend MiniSSL with support for cert_pem and key_pem

* Extend Puma ssl_bind DSL with support for cert_pem and cert_key

* Make some variables in binder test more readable
  • Loading branch information
dalibor authored and JuanitoFatas committed Sep 9, 2022
1 parent 37486f3 commit 7d05f56
Show file tree
Hide file tree
Showing 10 changed files with 253 additions and 57 deletions.
38 changes: 33 additions & 5 deletions ext/puma_http11/mini_ssl.c
Expand Up @@ -208,8 +208,11 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
#endif
int ssl_options;
VALUE key, cert, ca, verify_mode, ssl_cipher_filter, no_tlsv1, no_tlsv1_1,
verification_flags, session_id_bytes;
verification_flags, session_id_bytes, cert_pem, key_pem;
DH *dh;
BIO *bio;
X509 *x509;
EVP_PKEY *pkey;

#if OPENSSL_VERSION_NUMBER < 0x10002000L
EC_KEY *ecdh;
Expand All @@ -218,13 +221,15 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {
TypedData_Get_Struct(self, SSL_CTX, &sslctx_type, ctx);

key = rb_funcall(mini_ssl_ctx, rb_intern_const("key"), 0);
StringValue(key);

cert = rb_funcall(mini_ssl_ctx, rb_intern_const("cert"), 0);
StringValue(cert);

ca = rb_funcall(mini_ssl_ctx, rb_intern_const("ca"), 0);

cert_pem = rb_funcall(mini_ssl_ctx, rb_intern_const("cert_pem"), 0);

key_pem = rb_funcall(mini_ssl_ctx, rb_intern_const("key_pem"), 0);

verify_mode = rb_funcall(mini_ssl_ctx, rb_intern_const("verify_mode"), 0);

ssl_cipher_filter = rb_funcall(mini_ssl_ctx, rb_intern_const("ssl_cipher_filter"), 0);
Expand All @@ -233,8 +238,31 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) {

no_tlsv1_1 = rb_funcall(mini_ssl_ctx, rb_intern_const("no_tlsv1_1"), 0);

SSL_CTX_use_certificate_chain_file(ctx, RSTRING_PTR(cert));
SSL_CTX_use_PrivateKey_file(ctx, RSTRING_PTR(key), SSL_FILETYPE_PEM);
if (!NIL_P(cert)) {
StringValue(cert);
SSL_CTX_use_certificate_chain_file(ctx, RSTRING_PTR(cert));
}

if (!NIL_P(key)) {
StringValue(key);
SSL_CTX_use_PrivateKey_file(ctx, RSTRING_PTR(key), SSL_FILETYPE_PEM);
}

if (!NIL_P(cert_pem)) {
bio = BIO_new(BIO_s_mem());
BIO_puts(bio, RSTRING_PTR(cert_pem));
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);

SSL_CTX_use_certificate(ctx, x509);
}

if (!NIL_P(key_pem)) {
bio = BIO_new(BIO_s_mem());
BIO_puts(bio, RSTRING_PTR(key_pem));
pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);

SSL_CTX_use_PrivateKey(ctx, pkey);
}

verification_flags = rb_funcall(mini_ssl_ctx, rb_intern_const("verification_flags"), 0);

Expand Down
13 changes: 12 additions & 1 deletion lib/puma/binder.rb
Expand Up @@ -30,6 +30,7 @@ class Binder

def initialize(events, conf = Configuration.new)
@events = events
@conf = conf
@listeners = []
@inherited_fds = {}
@activated_sockets = {}
Expand Down Expand Up @@ -234,7 +235,17 @@ def parse(binds, logger, log_msg = 'Listening')
# Load localhost authority if not loaded.
ctx = localhost_authority && localhost_authority_context if params.empty?

ctx ||= MiniSSL::ContextBuilder.new(params, @events).context
ctx ||=
begin
# Extract cert_pem and key_pem from options[:store] if present
['cert', 'key'].each do |v|
if params[v] && params[v].start_with?('store:')
index = Integer(params.delete(v).split('store:').last)
params["#{v}_pem"] = @conf.options[:store][index]
end
end
MiniSSL::ContextBuilder.new(params, @events).context
end

if fd = @inherited_fds.delete(str)
logger.log "* Inherited #{str}"
Expand Down
29 changes: 29 additions & 0 deletions lib/puma/dsl.rb
Expand Up @@ -447,6 +447,14 @@ def threads(min, max)
# verify_mode: verify_mode, # default 'none'
# verification_flags: flags, # optional, not supported by JRuby
# }
#
# Alternatively, you can provide the cert_pem and key_pem:
# @example
# ssl_bind '127.0.0.1', '9292', {
# cert_pem: File.read(path_to_cert),
# key_pem: File.read(path_to_key),
# }
#
# @example For JRuby, two keys are required: keystore & keystore_pass.
# ssl_bind '127.0.0.1', '9292', {
# keystore: path_to_keystore,
Expand All @@ -455,6 +463,7 @@ def threads(min, max)
# verify_mode: verify_mode # default 'none'
# }
def ssl_bind(host, port, opts)
add_pem_values_to_options_store(opts)
bind self.class.ssl_bind_str(host, port, opts)
end

Expand Down Expand Up @@ -927,5 +936,25 @@ def io_selector_backend(backend)
def mutate_stdout_and_stderr_to_sync_on_write(enabled=true)
@options[:mutate_stdout_and_stderr_to_sync_on_write] = enabled
end

private

# To avoid adding cert_pem and key_pem as URI params, we store them on the
# options[:store] from where Puma binder knows how to find and extract them.
def add_pem_values_to_options_store(opts)
return if defined?(JRUBY_VERSION)

@options[:store] ||= []

# Store cert_pem and key_pem to options[:store] if present
[:cert, :key].each do |v|
opt_key = :"#{v}_pem"
if opts[opt_key]
index = @options[:store].length
@options[:store] << opts[opt_key]
opts[v] = "store:#{index}"
end
end
end
end
end
20 changes: 18 additions & 2 deletions lib/puma/minissl.rb
Expand Up @@ -208,6 +208,10 @@ class Context
def initialize
@no_tlsv1 = false
@no_tlsv1_1 = false
@key = nil
@cert = nil
@key_pem = nil
@cert_pem = nil
end

if IS_JRUBY
Expand All @@ -230,6 +234,8 @@ def check
attr_reader :key
attr_reader :cert
attr_reader :ca
attr_reader :cert_pem
attr_reader :key_pem
attr_accessor :ssl_cipher_filter
attr_accessor :verification_flags

Expand All @@ -248,9 +254,19 @@ def ca=(ca)
@ca = ca
end

def cert_pem=(cert_pem)
raise ArgumentError, "'cert_pem' is not a String" unless cert_pem.is_a? String
@cert_pem = cert_pem
end

def key_pem=(key_pem)
raise ArgumentError, "'key_pem' is not a String" unless key_pem.is_a? String
@key_pem = key_pem
end

def check
raise "Key not configured" unless @key
raise "Cert not configured" unless @cert
raise "Key not configured" if @key.nil? && @key_pem.nil?
raise "Cert not configured" if @cert.nil? && @cert_pem.nil?
end
end

Expand Down
14 changes: 8 additions & 6 deletions lib/puma/minissl/context_builder.rb
Expand Up @@ -23,17 +23,19 @@ def context
ctx.keystore_pass = params['keystore-pass']
ctx.ssl_cipher_list = params['ssl_cipher_list'] if params['ssl_cipher_list']
else
unless params['key']
events.error "Please specify the SSL key via 'key='"
if params['key'].nil? && params['key_pem'].nil?
events.error "Please specify the SSL key via 'key=' or 'key_pem='"
end

ctx.key = params['key']
ctx.key = params['key'] if params['key']
ctx.key_pem = params['key_pem'] if params['key_pem']

unless params['cert']
events.error "Please specify the SSL cert via 'cert='"
if params['cert'].nil? && params['cert_pem'].nil?
events.error "Please specify the SSL cert via 'cert=' or 'cert_pem='"
end

ctx.cert = params['cert']
ctx.cert = params['cert'] if params['cert']
ctx.cert_pem = params['cert_pem'] if params['cert_pem']

if ['peer', 'force_peer'].include?(params['verify_mode'])
unless params['ca']
Expand Down
14 changes: 7 additions & 7 deletions test/test_binder.rb
Expand Up @@ -262,7 +262,7 @@ def test_env_contains_protoenv
env_hash = @binder.envs[@binder.ios.first]

@binder.proto_env.each do |k,v|
assert_equal env_hash[k], v
assert env_hash[k] == v
end
end

Expand Down Expand Up @@ -308,11 +308,11 @@ def test_redirects_for_restart_env
def test_close_listeners_closes_ios
@binder.parse ["tcp://127.0.0.1:#{UniquePort.call}"], @events

refute @binder.listeners.any? { |u, l| l.closed? }
refute @binder.listeners.any? { |_l, io| io.closed? }

@binder.close_listeners

assert @binder.listeners.all? { |u, l| l.closed? }
assert @binder.listeners.all? { |_l, io| io.closed? }
end

def test_close_listeners_closes_ios_unless_closed?
Expand All @@ -322,11 +322,11 @@ def test_close_listeners_closes_ios_unless_closed?
bomb.close
def bomb.close; raise "Boom!"; end # the bomb has been planted

assert @binder.listeners.any? { |u, l| l.closed? }
assert @binder.listeners.any? { |_l, io| io.closed? }

@binder.close_listeners

assert @binder.listeners.all? { |u, l| l.closed? }
assert @binder.listeners.all? { |_l, io| io.closed? }
end

def test_listeners_file_unlink_if_unix_listener
Expand All @@ -344,8 +344,8 @@ def test_import_from_env_listen_inherit
@binder.parse ["tcp://127.0.0.1:0"], @events
removals = @binder.create_inherited_fds(@binder.redirects_for_restart_env)

@binder.listeners.each do |url, io|
assert_equal io.to_i, @binder.inherited_fds[url]
@binder.listeners.each do |l, io|
assert_equal io.to_i, @binder.inherited_fds[l]
end
assert_includes removals, "PUMA_INHERIT_0"
end
Expand Down
22 changes: 22 additions & 0 deletions test/test_config.rb
Expand Up @@ -77,6 +77,28 @@ def test_ssl_bind
assert_equal [ssl_binding], conf.options[:binds]
end

def test_ssl_bind_with_cert_and_key_pem
skip_if :jruby
skip_unless :ssl

cert_path = File.expand_path "../examples/puma/client-certs", __dir__
cert_pem = File.read("#{cert_path}/server.crt")
key_pem = File.read("#{cert_path}/server.key")

conf = Puma::Configuration.new do |c|
c.ssl_bind "0.0.0.0", "9292", {
cert_pem: cert_pem,
key_pem: key_pem,
verify_mode: "the_verify_mode",
}
end

conf.load

ssl_binding = "ssl://0.0.0.0:9292?cert=store:0&key=store:1&verify_mode=the_verify_mode"
assert_equal [ssl_binding], conf.options[:binds]
end

def test_ssl_bind_jruby
skip_unless :jruby
skip_unless :ssl
Expand Down

0 comments on commit 7d05f56

Please sign in to comment.