diff --git a/examples/puma/encrypted_puma_keypair.pem b/examples/puma/encrypted_puma_keypair.pem new file mode 100644 index 0000000000..0c1bd785bf --- /dev/null +++ b/examples/puma/encrypted_puma_keypair.pem @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: AES-256-CBC,90F30BF02BE2889D8DA5B8168E8BC346 + +QxHwMvAj+xu2Qxe3joiBRrTneQAnby1vpLZb+AnsPbwUWC1TuSeQJdRNs9qYlYwg +Ow4/plNp1cPPcjEY2TQlTNtr2ur/Exy11Bs0TLcPxkiQfm+pT/uok2+Y8Qs8WMDm +3MLqMPw0wiqnOoLa8j/oUDC2VsutItCtIsld04NgJYOyQTOdNhZ284x4WLp6vUUy +atWFOWWfRTDGMzXVKnpY8RQw09D/NcHxRzdmWYelVNQpZMfFAMYygNqofoWDrS3Z +XX04fOSxdQg3QwjPkpwD/XH8XT0hGkQaKaAXl5TbCdOOuPtagcDZgt0f4MQJUhEq +HqR2bHACS4MvLk/N6l1DvoqZErepAt5uVSRclREfxnqxgXhxvhMz9fe7z0uCJzQU +Panj8sg3ytP2oAOAg9kPFYfUr+weUOIBXwioHqEnA3K1j2ADJnZvByIqekQ863vl +2f0iweHMJp23gs6FFGA0brLDY78un2L8EPQC9wgPFSZpjFmXZU7ur05bm8fndnn6 +jGy/RLQ9N+fXBAALmwL+nj0Q5luxdQqx6ouCiDKB2ehz3LO/KMD3QI5ysT7GpY2F +2Out0Ud4e2XdS+5CDs28LCBnFejaxlIvJBxXu5PhtJ+7Plm4g1uIMdRspgO+Wukw +5/eNMgWm3Q7+1NGlA4kg3HW6K7GV/QIITYWKk7ETX06oD4Uj/XUxinrADT48IMYQ +iGcSECrs9uc3MNEGA3AHDqTvmCFsBZwjtx2JZX3HwwgXUxYSX52d07fctJJe3wYX +hQi9b4EEyGb2uOMWH3gMLzJaLDM89sgwRz0YNHlQK2wV7mfkmjjNHVGix6M+dfo6 +kbXZD+rertIqytuaE++uVQEjBHk/yMPK9Yn/jA4TJlHsxNyZwrbYnHYgeGs0//fK +fe+ez1A67pvdS1/xAvFBuicqcI+D/Ib1kY3FIZBcnEpoNOCSBjtdZR06JeCKiD7S +QL5yNLgb0I5LrrO0D0Rr3jjCHpwe6WSEvO0Og6ktzXMKCTldk7YiQP1/Oy4BZBbM +EQh2OyqVLXAQdpgQ+J8xTI62khyqnvDelorcn1xQZbBLH7oS7pturxuFgVTW4RXL +SHq9GLfD4dzzIDo+fSII7PjTYwJiKFYWUZKQGboWkPHhgx3LR3geB6i+4HjuRt70 +mRKA1aitwIZsKKkuiTtlUhBN8gLctGJpoP92yMcmADLDLmysWIDb1G/Ogo1a1qLI +2N5pbJUFDrjZ82j2xLtFiVEt17cQ+7x2mxMdb5DaxliZy05XhM6SbnOETBwhIm04 +qV5IjZCCIqY3eEOsgeLz6vdjiohn2P2eXbgokl1vRQnzSwYaCTo64pIN4spgl+jN +WNsD1kQBgyvQPXoQntiiwi5CLlmVlE9RY32DYZJs0RazxcN5mBC9qP9JAVHOhUGR +JhaQfPl2GVtKyzz1dkvvXYjPFu7TKkt3Qtu59YmRfDu85JSzZhSFbssKu7lzNJv1 +wno95r0fg2uG0dvCMB1MhSaRVBnReyubPvIzycU9PYkaS23I88ZzPsxCGMlJwxCC +A9AlbH46bfzrToF/UgbRTbpX7kNjDcDOFb01E47b2h2Tm8bYg7winVFS1qdWr6CG +-----END RSA PRIVATE KEY----- diff --git a/examples/puma/key_password_command.sh b/examples/puma/key_password_command.sh new file mode 100755 index 0000000000..aa6cfe19f4 --- /dev/null +++ b/examples/puma/key_password_command.sh @@ -0,0 +1,3 @@ +#!/bin/sh + +echo "hello world" diff --git a/ext/puma_http11/mini_ssl.c b/ext/puma_http11/mini_ssl.c index 7e577b738b..c199198a14 100644 --- a/ext/puma_http11/mini_ssl.c +++ b/ext/puma_http11/mini_ssl.c @@ -185,6 +185,18 @@ static int engine_verify_callback(int preverify_ok, X509_STORE_CTX* ctx) { return preverify_ok; } +static int password_callback(char *buf, int size, int rwflag, void *userdata) { + const char *password = (const char *) userdata; + size_t len = strlen(password); + + if (len > (size_t) size) { + return 0; + } + + memcpy(buf, password, len); + return (int) len; +} + static VALUE sslctx_alloc(VALUE klass) { SSL_CTX *ctx; @@ -212,10 +224,12 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) { SSL_CTX* ctx; int ssl_options; VALUE key, cert, ca, verify_mode, ssl_cipher_filter, no_tlsv1, no_tlsv1_1, - verification_flags, session_id_bytes, cert_pem, key_pem; + verification_flags, session_id_bytes, cert_pem, key_pem, key_password_command, decryption_key; BIO *bio; X509 *x509; EVP_PKEY *pkey; + pem_password_cb *password_cb = NULL; + const char *password = NULL; #ifdef HAVE_SSL_CTX_SET_MIN_PROTO_VERSION int min; #endif @@ -235,6 +249,8 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) { key = rb_funcall(mini_ssl_ctx, rb_intern_const("key"), 0); + key_password_command = rb_funcall(mini_ssl_ctx, rb_intern_const("key_password_command"), 0); + cert = rb_funcall(mini_ssl_ctx, rb_intern_const("cert"), 0); ca = rb_funcall(mini_ssl_ctx, rb_intern_const("ca"), 0); @@ -261,6 +277,18 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) { } } + if (!NIL_P(key_password_command)) { + decryption_key = rb_funcall(mini_ssl_ctx, rb_intern_const("key_password"), 0); + + if (!NIL_P(decryption_key)) { + StringValue(decryption_key); + password_cb = password_callback; + password = RSTRING_PTR(decryption_key); + SSL_CTX_set_default_passwd_cb(ctx, password_cb); + SSL_CTX_set_default_passwd_cb_userdata(ctx, (void *) password); + } + } + if (!NIL_P(key)) { StringValue(key); @@ -285,7 +313,7 @@ sslctx_initialize(VALUE self, VALUE mini_ssl_ctx) { 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); + pkey = PEM_read_bio_PrivateKey(bio, NULL, password_cb, (void *) password); if (SSL_CTX_use_PrivateKey(ctx, pkey) != 1) { BIO_free(bio); diff --git a/lib/puma/dsl.rb b/lib/puma/dsl.rb index 429c856c2e..121f90adf6 100644 --- a/lib/puma/dsl.rb +++ b/lib/puma/dsl.rb @@ -89,6 +89,7 @@ def self.ssl_bind_str(host, port, opts) cert_flags = (cert = opts[:cert]) ? "cert=#{Puma::Util.escape(cert)}" : nil key_flags = (key = opts[:key]) ? "&key=#{Puma::Util.escape(key)}" : nil + password_flags = (password_command = opts[:key_password_command]) ? "&key_password_command=#{Puma::Util.escape(password_command)}" : nil reuse_flag = if (reuse = opts[:reuse]) @@ -114,7 +115,7 @@ def self.ssl_bind_str(host, port, opts) nil end - "ssl://#{host}:#{port}?#{cert_flags}#{key_flags}#{ssl_cipher_filter}" \ + "ssl://#{host}:#{port}?#{cert_flags}#{key_flags}#{password_flags}#{ssl_cipher_filter}" \ "#{reuse_flag}&verify_mode=#{verify}#{tls_str}#{ca_additions}#{v_flags}#{backlog_str}#{low_latency_str}" end end diff --git a/lib/puma/minissl.rb b/lib/puma/minissl.rb index 1ac1fe0bf5..f1ef087a8f 100644 --- a/lib/puma/minissl.rb +++ b/lib/puma/minissl.rb @@ -5,6 +5,7 @@ rescue LoadError end +require 'open3' # need for Puma::MiniSSL::OPENSSL constants used in `HAS_TLS1_3` # use require, see https://github.com/puma/puma/pull/2381 require 'puma/puma_http11' @@ -277,6 +278,7 @@ def check else # non-jruby Context properties attr_reader :key + attr_reader :key_password_command attr_reader :cert attr_reader :ca attr_reader :cert_pem @@ -291,6 +293,10 @@ def key=(key) @key = key end + def key_password_command=(key_password_command) + @key_password_command = key_password_command + end + def cert=(cert) check_file cert, 'Cert' @cert = cert @@ -316,6 +322,17 @@ def check raise "Cert not configured" if @cert.nil? && @cert_pem.nil? end + # Executes the command to return the password needed to decrypt the key. + def key_password + raise "Key password command not configured" if @key_password_command.nil? + + stdout_str, stderr_str, status = Open3.capture3(key_password_command) + + return stdout_str.chomp if status.success? + + raise "Key password failed with code #{status.exitstatus}: #{stderr_str}" + end + # Controls session reuse. Allowed values are as follows: # * 'off' - matches the behavior of Puma 5.6 and earlier. This is included # in case reuse 'on' is made the default in future Puma versions. diff --git a/lib/puma/minissl/context_builder.rb b/lib/puma/minissl/context_builder.rb index f8b7807c54..dcca1b3114 100644 --- a/lib/puma/minissl/context_builder.rb +++ b/lib/puma/minissl/context_builder.rb @@ -38,6 +38,7 @@ def context ctx.key = params['key'] if params['key'] ctx.key_pem = params['key_pem'] if params['key_pem'] + ctx.key_password_command = params['key_password_command'] if params['key_password_command'] if params['cert'].nil? && params['cert_pem'].nil? log_writer.error "Please specify the SSL cert via 'cert=' or 'cert_pem='" diff --git a/test/test_integration_ssl.rb b/test/test_integration_ssl.rb index 9e06efecb3..6413270f43 100644 --- a/test/test_integration_ssl.rb +++ b/test/test_integration_ssl.rb @@ -174,6 +174,70 @@ def test_ssl_run_with_localhost_authority activate_control_app 'tcp://#{HOST}:#{control_tcp_port}', { auth_token: '#{TOKEN}' } +app do |env| + [200, {}, [env['rack.url_scheme']]] +end +RUBY + + with_server(config) do |http| + body = nil + http.start do + req = Net::HTTP::Get.new '/', {} + http.request(req) { |resp| body = resp.body } + end + assert_equal 'https', body + end + end + + def test_ssl_run_with_encrypted_key + skip_if :jruby + + config = <