From af605f352e40905247efe115389c7cdcb1039005 Mon Sep 17 00:00:00 2001 From: Kyle Drake Date: Tue, 2 Sep 2014 18:36:17 -0700 Subject: [PATCH] allow adding of ssl certs for domain --- app.rb | 54 ++++++++ migrations/040_add_ssl.rb | 12 ++ migrations/041_add_intermediate_ssl.rb | 9 ++ models/site.rb | 8 +- tests/acceptance/settings_tests.rb | 154 +++++++++++++++++++++++ tests/files/ssl/derpie.com-encrypted.key | 30 +++++ views/custom_domain.erb | 54 ++++++-- 7 files changed, 307 insertions(+), 14 deletions(-) create mode 100644 migrations/040_add_ssl.rb create mode 100644 migrations/041_add_intermediate_ssl.rb create mode 100644 tests/files/ssl/derpie.com-encrypted.key diff --git a/app.rb b/app.rb index dae4afed..6ffb430b 100644 --- a/app.rb +++ b/app.rb @@ -486,6 +486,60 @@ post '/settings/profile' do redirect '/settings' end +post '/settings/ssl' do + require_login + + unless params[:key] && params[:cert] && params[:cert_intermediate] + flash[:error] = 'SSL key, certificate, and intermediate certificate are required to continue.' + redirect '/custom_domain' + end + + begin + key = OpenSSL::PKey::RSA.new params[:key][:tempfile].read, '' + rescue => e + flash[:error] = 'Could not process SSL key, file may be incorrect, damaged, or passworded (you need to remove the password).' + redirect '/custom_domain' + end + + if !key.private? + flash[:error] = 'SSL Key file does not have private key data.' + redirect '/custom_domain' + end + + begin + cert = OpenSSL::X509::Certificate.new params[:cert][:tempfile].read + rescue => e + flash[:error] = 'Could not process SSL certificate, file may be incorrect or damaged.' + redirect '/custom_domain' + end + + if cert.not_after < Time.now + flash[:error] = 'SSL Certificate is expired, please create a new one.' + redirect '/custom_domain' + end + + cert_cn = cert.subject.to_a.select {|a| a.first == 'CN'}.flatten[1] + if !cert_cn.match(current_site.domain) + flash[:error] = "The certificate CN (common name) #{cert_cn} does not match your domain: #{current_site.domain}" + redirect '/custom_domain' + end + + begin + cert_intermediate = OpenSSL::X509::Certificate.new params[:cert_intermediate][:tempfile].read + rescue => e + flash[:error] = 'Could not process intermediate SSL certificate, file may be incorrect or damaged.' + redirect '/custom_domain' + end + + current_site.ssl_key = key.to_pem + current_site.ssl_cert = cert.to_pem + current_site.ssl_cert_intermediate = cert_intermediate.to_pem + current_site.save + + flash[:success] = 'Updated SSL key/certificate.' + redirect '/custom_domain' +end + post '/signin' do dashboard_if_signed_in diff --git a/migrations/040_add_ssl.rb b/migrations/040_add_ssl.rb new file mode 100644 index 00000000..87b010f8 --- /dev/null +++ b/migrations/040_add_ssl.rb @@ -0,0 +1,12 @@ +Sequel.migration do + up { + DB.add_column :sites, :ssl_key, :text + DB.add_column :sites, :ssl_cert, :text + + } + + down { + DB.drop_column :sites, :ssl_key + DB.drop_column :sites, :ssl_cert + } +end diff --git a/migrations/041_add_intermediate_ssl.rb b/migrations/041_add_intermediate_ssl.rb new file mode 100644 index 00000000..8f1d76a3 --- /dev/null +++ b/migrations/041_add_intermediate_ssl.rb @@ -0,0 +1,9 @@ +Sequel.migration do + up { + DB.add_column :sites, :ssl_cert_intermediate, :text + } + + down { + DB.drop_column :sites, :ssl_cert_intermediate + } +end \ No newline at end of file diff --git a/models/site.rb b/models/site.rb index 598c8ea9..42c4652d 100644 --- a/models/site.rb +++ b/models/site.rb @@ -798,6 +798,10 @@ class Site < Sequel::Model "#{THUMBNAILS_URL_ROOT}/#{values[:username]}/#{path}.#{resolution}.#{ext}" end + def ssl_installed? + ssl_key && ssl_cert && ssl_cert_intermediate + end + def to_rss RSS::Maker.make("atom") do |maker| maker.channel.title = title @@ -808,8 +812,8 @@ class Site < Sequel::Model latest_events.each do |event| if event.site_change_id maker.items.new_item do |item| - item.link = "http://#{username}.neocities.org" - item.title = "#{username}.neocities.org has been updated" + item.link = "https://#{host}" + item.title = "#{title} has been updated" item.updated = event.site_change.created_at end end diff --git a/tests/acceptance/settings_tests.rb b/tests/acceptance/settings_tests.rb index a425ee74..18b1d722 100644 --- a/tests/acceptance/settings_tests.rb +++ b/tests/acceptance/settings_tests.rb @@ -1,6 +1,160 @@ require_relative './environment.rb' describe 'site/settings' do + describe 'ssl' do + include Capybara::DSL + + before do + # https://github.com/kyledrake/ruby-openssl-cheat-sheet/blob/master/certificate_authority.rb + # TODO make ca generation run only once + ca_keypair = OpenSSL::PKey::RSA.new(2048) + ca_cert = OpenSSL::X509::Certificate.new + ca_cert.not_before = Time.now + ca_cert.subject = OpenSSL::X509::Name.new([ + ["C", "US"], + ["ST", "Oregon"], + ["L", "Portland"], + ["CN", "Neocities CA"] + ]) + ca_cert.issuer = ca_cert.subject + # All issued certs will be unusuable after this time. + ca_cert.not_after = Time.now + 1000000000 # 40 or so years + ca_cert.serial = 1 + ca_cert.public_key = ca_keypair.public_key + ef = OpenSSL::X509::ExtensionFactory.new + ef.subject_certificate = ca_cert + ef.issuer_certificate = ca_cert + # Read more about the various extensions here: http://www.openssl.org/docs/apps/x509v3_config.html + ca_cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true)) + ca_cert.add_extension(ef.create_extension("keyUsage","keyCertSign, cRLSign", true)) + ca_cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false)) + ca_cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false)) + ca_cert.sign(ca_keypair, OpenSSL::Digest::SHA256.new) + @ca_cert = ca_cert + @ca_keypair = ca_keypair + + @domain = SecureRandom.uuid.gsub('-', '')+'.com' + @site = Fabricate :site, domain: @domain + page.set_rack_session id: @site.id + + ca_cert = OpenSSL::X509::Certificate.new(@ca_cert.to_pem) + our_cert_keypair = OpenSSL::PKey::RSA.new(2048) + our_cert_req = OpenSSL::X509::Request.new + our_cert_req.subject = OpenSSL::X509::Name.new([ + ["C", "US"], + ["ST", "Oregon"], + ["L", "Portland"], + ["O", "Neocities User"], + ["CN", "*.#{@domain}"] + ]) + our_cert_req.public_key = our_cert_keypair.public_key + our_cert_req.sign our_cert_keypair, OpenSSL::Digest::SHA1.new + our_cert = OpenSSL::X509::Certificate.new + our_cert.subject = our_cert_req.subject + our_cert.issuer = ca_cert.subject + our_cert.not_before = Time.now + our_cert.not_after = Time.now + 100000000 # 3 or so years. + our_cert.serial = 123 # Should be an unique number, the CA probably has a database. + our_cert.public_key = our_cert_req.public_key + # To make the certificate valid for both wildcard and top level domain name, we need an extension. + ef = OpenSSL::X509::ExtensionFactory.new + ef.subject_certificate = our_cert + ef.issuer_certificate = ca_cert + our_cert.add_extension(ef.create_extension("subjectAltName", "DNS:#{@domain}, DNS:*.#{@domain}", false)) + our_cert.sign @ca_keypair, OpenSSL::Digest::SHA1.new + + our_cert_tmpfile = Tempfile.new 'our_cert' + our_cert_tmpfile.write our_cert.to_pem + our_cert_tmpfile.close + @cert_path = our_cert_tmpfile.path + + our_cert_keypair_tmpfile = Tempfile.new 'our_cert_keypair' + our_cert_keypair_tmpfile.write our_cert_keypair.to_pem + our_cert_keypair_tmpfile.close + @key_path = our_cert_keypair_tmpfile.path + + ca_cert_tmpfile = Tempfile.new 'ca_cert' + ca_cert_tmpfile.write @ca_cert.to_pem + ca_cert_tmpfile.close + @cert_intermediate_path = ca_cert_tmpfile.path + + end + + it 'fails without domain set' do + @site = Fabricate :site + page.set_rack_session id: @site.id + visit '/custom_domain' + page.must_have_content /Cannot upload SSL certificate until domain is added/i + end + + it 'works with valid key, cert and intermediate cert' do + visit '/custom_domain' + page.must_have_content /status: inactive/i + attach_file 'key', @key_path + attach_file 'cert', @cert_path + attach_file 'cert_intermediate', @cert_intermediate_path + click_button 'Upload SSL Key and Certificate' + page.current_path.must_equal '/custom_domain' + page.must_have_content /Updated SSL/ + page.must_have_content /status: installed/i + @site.reload + @site.ssl_key.must_equal File.read(@key_path) + @site.ssl_cert.must_equal File.read(@cert_path) + @site.ssl_cert_intermediate.must_equal File.read(@cert_intermediate_path) + end + + it 'fails with no uploads' do + visit '/custom_domain' + click_button 'Upload SSL Key and Certificate' + page.current_path.must_equal '/custom_domain' + page.must_have_content /ssl key.+certificate.+intermediate.+required to continue/i + @site.reload + @site.ssl_key.must_equal nil + @site.ssl_cert.must_equal nil + @site.ssl_cert_intermediate.must_equal nil + end + + it 'fails with encrypted key' do + visit '/custom_domain' + attach_file 'key', './tests/files/ssl/derpie.com-encrypted.key' + attach_file 'cert', @cert_path + attach_file 'cert_intermediate', @cert_intermediate_path + click_button 'Upload SSL Key and Certificate' + page.current_path.must_equal '/custom_domain' + page.must_have_content /could not process ssl key/i + end + + it 'fails with junk key' do + visit '/custom_domain' + attach_file 'key', './tests/files/index.html' + attach_file 'cert', @cert_path + attach_file 'cert_intermediate', @cert_intermediate_path + click_button 'Upload SSL Key and Certificate' + page.current_path.must_equal '/custom_domain' + page.must_have_content /could not process ssl key/i + end + + it 'fails with junk cert' do + visit '/custom_domain' + attach_file 'key', @key_path + attach_file 'cert', './tests/files/index.html' + attach_file 'cert_intermediate', @cert_intermediate_path + click_button 'Upload SSL Key and Certificate' + page.current_path.must_equal '/custom_domain' + page.must_have_content /could not process ssl certificate/i + end + + it 'fails with junk intermediate cert' do + visit '/custom_domain' + attach_file 'key', @key_path + attach_file 'cert', @cert_path + attach_file 'cert_intermediate', './tests/files/index.html' + click_button 'Upload SSL Key and Certificate' + page.current_path.must_equal '/custom_domain' + page.must_have_content /could not process intermediate ssl certificate/i + end + end + describe 'change username' do include Capybara::DSL diff --git a/tests/files/ssl/derpie.com-encrypted.key b/tests/files/ssl/derpie.com-encrypted.key new file mode 100644 index 00000000..79f52994 --- /dev/null +++ b/tests/files/ssl/derpie.com-encrypted.key @@ -0,0 +1,30 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,966995A71814CBB6 + +cPhqT0MucfSfZJYnfwOfS9Cqa+Ad5if4sluHaG1LlbCOl9GOWDjh/modLqvFEa1L +c2WCf6z00Haw9HZoD+kSmTzYcwX5b5eOpRPhgcaxHl7QOvLZtdPDQhZEmAREw2Ey +7oM9hKsqfFvi0fC0OnW6vOK2a3qAsLXLrehdR4SdHjMXr9WJuj80aqXZPkOjC8gH +/MMfzb6ZTeRzNQrz7vYW+K8Qs08k7/pEneLpqC9Bdmus05VRGQixI5plQzJ3+w+S +xE2sr9n885AojC7BN6IXouxbmbZv5q+L1DI5gtHrme6tA5l4ziB3s14drHZnmA2A +MTH0tD3URWK0gY8uuf0HE8LuMqxe8HngyMoxoLM3ck4sEPJ+/ZxlPHO2HDZ/S+m3 +z+R1hmIRs9XryRXUqiIF0zORrc0+3t2a6UOfdja3PZ8mNN1QrsuIpwTfP69lKe0u +prgdV0AkbTW3aC4/RfNI5uJ60uWLjcoWfiDK8fchUleOWYLHMUQt8YiVEWizpLFo +f3dP2kh4pB2R69zjMkvV6XlN1YOh0uVKWNPgrFXuKTvec9xLWSccW9XQoxtxN18k +sDSFTc2gxf+rTfJ7PTEDwPH1gIZjLQwGknPWPUlfySsbDOaZzaVZEj7f7NUu6BWm +L/r1jvW8EOjzSN+XL5Aw5mpIMVxb2CHezq2qF6HSW7SBxARjDn07rcHnRD5ayjqU +8AdsXFBYPbOY5ujpsIA5/d2VThY+JDfYom8iUbjXmuiXpqZxBJ+CBh46T2sRKxtU +S90WkujEFBAayuFE2b8h6HJsSg3RTTcDf2SbDWzSoH0ZCyf00/xwgcRZZ5bSbAfC +YhaTPtmENeHkA4ofMsBkTO9UCzqJAC+inp41PFptTIn6nDx/TwT4N3nsOlIDJVYw +edFhdpPlISJREI6g1SdzUhWb1YKhCFkkt8bKEbElOfUnF+bppHIEk6nwMkFkj4SV +504YTGA+cWLta0zxLWj/zVClodAtuaa9F5jjRezYlDbs9s1awqp/yLPXMD6d90Oa +vYWJclNT37y1PXdBHBrKummoMf3R0LoS45lekkPWazGRl0O7p0+tQUmcyOEVbPBj +fWZndBVi+3yw/ewImeZOX/PYPCuWSdYY/fFw3VMMq2UwetFwdM71k5tUAX695qhI +FMWVkTxonQOr6zgvl+5kdKV+cSgCXt+p/Fa70qGXbjhEgBysrh4uLuTV7hQs5Ext +aIkGm83cnqtQbyvpSNxzVQOj1PqegPi/Z4TKM/saWzBX77qEuSfUjo/+Sg6nMG9O +Tml9ZLXpkFVfeVx7rvpZNzXMk8TeEKVNo/lO5bKnvEUfKsY+FFwJl7ehe+V/Ig6z +2ugqtv/LSoVrwNNiIUHvUcx2b+Woa4iX1WJs4303U1cGhFOPtLSM32XNbHrRBk0u +SKLHTJgV5hMpXkq/I6UMnanD/bGNvK8Tx/XvvQY62sObAwSlKpBFfgOmSXcUU2PK +AE4ih0/4UE8edRY2VOW99JRfcLaJofRr6LLmsYzFSgo4R4d4SNDvzMV58zxXSLN4 +bwBq9FNvW/LErbE844U0FG1zwTKLpPr3SRFKk/pM1yful1yziWyijg== +-----END RSA PRIVATE KEY----- diff --git a/views/custom_domain.erb b/views/custom_domain.erb index cc6e34e5..4abaa438 100644 --- a/views/custom_domain.erb +++ b/views/custom_domain.erb @@ -9,17 +9,11 @@

- <% if !current_site.errors.empty? %> -
- <% current_site.errors.each do |error| %> -

<%= error.last.first %>

- <% end %> -
- <% end %> - - <% if flash[:success] %> -
- <%== flash[:success] %> + <% if flash.keys.length > 0 %> +
+ <% flash.keys.each do |key| %> + <%== flash[key] %> + <% end %>
<% end %> @@ -47,9 +41,45 @@
+
+
+

Add SSL Certificate

- NOTE: This is for advanced users, we cannot provide technical support for this feature. If you cannot make this work, please contact your domain registrar. + This allows you to add an SSL private key and certificate for your domain, enabling encryption for your site (https). It can take up to 5-30 minutes for the changes to go live, so please be patient.

+ + <% if current_site.domain.nil? || current_site.domain.empty? %> +

Cannot upload SSL certificate until domain is added.

+ <% else %> + +
+ <%== csrf_token_input_html %> + +

+ + Status: <%= current_site.ssl_installed? ? 'Installed' : 'Inactive' %> + +

+ +

+ Private key (yourdomain.com.key): + +

+ +

+ Primary Certificate (yourdomain.com.crt): + +

+ +

+ Intermediate Certificate (ca.crt/yourca.crt): + +

+ + + +
+ <% end %>
\ No newline at end of file