fixes and improvements to ssl verification

This commit is contained in:
Kyle Drake 2014-09-03 17:14:11 -07:00
parent 8cd61a06f2
commit 4168854d76
6 changed files with 207 additions and 125 deletions

View file

@ -91,21 +91,21 @@ end
desc 'Produce SSL config package for proxy' desc 'Produce SSL config package for proxy'
task :buildssl => [:environment] do task :buildssl => [:environment] do
sites = Site.select(:id, :username, :domain, :ssl_key, :ssl_cert, :ssl_cert_intermediate). sites = Site.select(:id, :username, :domain, :ssl_key, :ssl_cert).
exclude(domain: nil). exclude(domain: nil).
exclude(ssl_key: nil). exclude(ssl_key: nil).
exclude(ssl_cert: nil). exclude(ssl_cert: nil).
exclude(ssl_cert_intermediate: nil).
all all
payload = [] payload = []
FileUtils.rm './files/sslsites.zip'
Zip::Archive.open('./files/sslsites.zip', Zip::CREATE) do |ar| Zip::Archive.open('./files/sslsites.zip', Zip::CREATE) do |ar|
ar.add_dir 'ssl' ar.add_dir 'ssl'
sites.each do |site| sites.each do |site|
ar.add_buffer "ssl/#{site.username}.key", site.ssl_key ar.add_buffer "ssl/#{site.username}.key", site.ssl_key
ar.add_buffer "ssl/#{site.username}.crt", "#{site.ssl_cert_intermediate}\n\n#{site.ssl_cert}" ar.add_buffer "ssl/#{site.username}.crt", site.ssl_cert
payload << {username: site.username, domain: site.domain} payload << {username: site.username, domain: site.domain}
end end

71
app.rb
View file

@ -489,8 +489,8 @@ end
post '/settings/ssl' do post '/settings/ssl' do
require_login require_login
unless params[:key] && params[:cert] && params[:cert_intermediate] unless params[:key] && params[:cert]
flash[:error] = 'SSL key, certificate, and intermediate certificate are required to continue.' flash[:error] = 'SSL key and certificate are required.'
redirect '/custom_domain' redirect '/custom_domain'
end end
@ -506,34 +506,83 @@ post '/settings/ssl' do
redirect '/custom_domain' redirect '/custom_domain'
end end
certs_string = params[:cert][:tempfile].read
cert_array = certs_string.lines.slice_before(/-----BEGIN CERTIFICATE-----/).to_a.collect {|a| a.join}
if cert_array.empty?
flash[:error] = 'Cert file does not contain any certificates.'
redirect '/custom_domain'
end
cert_valid_for_domain = false
cert_array.each do |cert_string|
begin begin
cert = OpenSSL::X509::Certificate.new params[:cert][:tempfile].read cert = OpenSSL::X509::Certificate.new cert_string
rescue => e rescue => e
flash[:error] = 'Could not process SSL certificate, file may be incorrect or damaged.' flash[:error] = 'Could not process SSL certificate, file may be incorrect or damaged.'
redirect '/custom_domain' redirect '/custom_domain'
end end
if cert.not_after < Time.now if cert.not_after < Time.now
flash[:error] = 'SSL Certificate is expired, please create a new one.' flash[:error] = 'SSL Certificate has expired, please create a new one.'
redirect '/custom_domain' redirect '/custom_domain'
end end
cert_cn = cert.subject.to_a.select {|a| a.first == 'CN'}.flatten[1] cert_cn = cert.subject.to_a.select {|a| a.first == 'CN'}.flatten[1]
if !cert_cn.match(current_site.domain) cert_valid_for_domain = true if cert_cn && cert_cn.match(current_site.domain)
flash[:error] = "The certificate CN (common name) #{cert_cn} does not match your domain: #{current_site.domain}" end
unless cert_valid_for_domain
flash[:error] = "Your certificate CN (common name) does not match your domain: #{current_site.domain}"
redirect '/custom_domain' redirect '/custom_domain'
end end
# Everything else was worse.
crtfile = Tempfile.new 'crtfile'
crtfile.write cert_array.join
crtfile.close
keyfile = Tempfile.new 'keyfile'
keyfile.write key.to_pem
keyfile.close
if ENV['TRAVIS'] != 'true'
nginx_testfile = Tempfile.new 'nginx_testfile'
nginx_testfile.write %{
pid /tmp/throwaway.pid;
events {}
http {
access_log off;
error_log /dev/null error;
server {
listen 60000 ssl;
server_name #{current_site.domain} *.#{current_site.domain};
ssl_certificate #{crtfile.path};
ssl_certificate_key #{keyfile.path};
}
}
}
nginx_testfile.close
line = Cocaine::CommandLine.new(
"nginx", "-t -c :path",
expected_outcodes: [0],
swallow_stderr: true
)
begin begin
cert_intermediate = OpenSSL::X509::Certificate.new params[:cert_intermediate][:tempfile].read output = line.run path: nginx_testfile.path
rescue => e rescue Cocaine::ExitStatusError => e
flash[:error] = 'Could not process intermediate SSL certificate, file may be incorrect or damaged.' flash[:error] = "There is something wrong with your certificate, please check with your issuing CA."
redirect '/custom_domain' redirect '/custom_domain'
end end
end
current_site.ssl_key = key.to_pem current_site.ssl_key = key.to_pem
current_site.ssl_cert = cert.to_pem current_site.ssl_cert = cert_array.join
current_site.ssl_cert_intermediate = cert_intermediate.to_pem
current_site.save current_site.save
flash[:success] = 'Updated SSL key/certificate.' flash[:success] = 'Updated SSL key/certificate.'

View file

@ -0,0 +1,9 @@
Sequel.migration do
up {
DB.drop_column :sites, :ssl_cert_intermediate
}
down {
DB.add_column :sites, :ssl_cert_intermediate, :text
}
end

View file

@ -799,7 +799,7 @@ class Site < Sequel::Model
end end
def ssl_installed? def ssl_installed?
ssl_key && ssl_cert && ssl_cert_intermediate ssl_key && ssl_cert
end end
def to_rss def to_rss

View file

@ -1,12 +1,9 @@
require_relative './environment.rb' require_relative './environment.rb'
describe 'site/settings' do def generate_ssl_certs(opts={})
describe 'ssl' do
include Capybara::DSL
before do
# https://github.com/kyledrake/ruby-openssl-cheat-sheet/blob/master/certificate_authority.rb # https://github.com/kyledrake/ruby-openssl-cheat-sheet/blob/master/certificate_authority.rb
# TODO make ca generation run only once res = {}
ca_keypair = OpenSSL::PKey::RSA.new(2048) ca_keypair = OpenSSL::PKey::RSA.new(2048)
ca_cert = OpenSSL::X509::Certificate.new ca_cert = OpenSSL::X509::Certificate.new
ca_cert.not_before = Time.now ca_cert.not_before = Time.now
@ -17,7 +14,6 @@ describe 'site/settings' do
["CN", "Neocities CA"] ["CN", "Neocities CA"]
]) ])
ca_cert.issuer = ca_cert.subject 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.not_after = Time.now + 1000000000 # 40 or so years
ca_cert.serial = 1 ca_cert.serial = 1
ca_cert.public_key = ca_keypair.public_key ca_cert.public_key = ca_keypair.public_key
@ -30,14 +26,10 @@ describe 'site/settings' do
ca_cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false)) ca_cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash", false))
ca_cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false)) ca_cert.add_extension(ef.create_extension("authorityKeyIdentifier", "keyid:always", false))
ca_cert.sign(ca_keypair, OpenSSL::Digest::SHA256.new) ca_cert.sign(ca_keypair, OpenSSL::Digest::SHA256.new)
@ca_cert = ca_cert res[:ca_cert] = ca_cert
@ca_keypair = ca_keypair res[:ca_keypair] = ca_keypair
@domain = SecureRandom.uuid.gsub('-', '')+'.com' ca_cert = OpenSSL::X509::Certificate.new(res[:ca_cert].to_pem)
@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_keypair = OpenSSL::PKey::RSA.new(2048)
our_cert_req = OpenSSL::X509::Request.new our_cert_req = OpenSSL::X509::Request.new
our_cert_req.subject = OpenSSL::X509::Name.new([ our_cert_req.subject = OpenSSL::X509::Name.new([
@ -45,7 +37,7 @@ describe 'site/settings' do
["ST", "Oregon"], ["ST", "Oregon"],
["L", "Portland"], ["L", "Portland"],
["O", "Neocities User"], ["O", "Neocities User"],
["CN", "*.#{@domain}"] ["CN", "*.#{opts[:domain]}"]
]) ])
our_cert_req.public_key = our_cert_keypair.public_key our_cert_req.public_key = our_cert_keypair.public_key
our_cert_req.sign our_cert_keypair, OpenSSL::Digest::SHA1.new our_cert_req.sign our_cert_keypair, OpenSSL::Digest::SHA1.new
@ -53,7 +45,11 @@ describe 'site/settings' do
our_cert.subject = our_cert_req.subject our_cert.subject = our_cert_req.subject
our_cert.issuer = ca_cert.subject our_cert.issuer = ca_cert.subject
our_cert.not_before = Time.now our_cert.not_before = Time.now
our_cert.not_after = Time.now + 100000000 # 3 or so years. if opts[:expired]
our_cert.not_after = Time.now - 100000000
else
our_cert.not_after = Time.now + 100000000
end
our_cert.serial = 123 # Should be an unique number, the CA probably has a database. our_cert.serial = 123 # Should be an unique number, the CA probably has a database.
our_cert.public_key = our_cert_req.public_key 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. # To make the certificate valid for both wildcard and top level domain name, we need an extension.
@ -61,22 +57,44 @@ describe 'site/settings' do
ef.subject_certificate = our_cert ef.subject_certificate = our_cert
ef.issuer_certificate = ca_cert ef.issuer_certificate = ca_cert
our_cert.add_extension(ef.create_extension("subjectAltName", "DNS:#{@domain}, DNS:*.#{@domain}", false)) our_cert.add_extension(ef.create_extension("subjectAltName", "DNS:#{@domain}, DNS:*.#{@domain}", false))
our_cert.sign @ca_keypair, OpenSSL::Digest::SHA1.new our_cert.sign res[:ca_keypair], OpenSSL::Digest::SHA1.new
our_cert_tmpfile = Tempfile.new 'our_cert' our_cert_tmpfile = Tempfile.new 'our_cert'
our_cert_tmpfile.write our_cert.to_pem our_cert_tmpfile.write our_cert.to_pem
our_cert_tmpfile.close our_cert_tmpfile.close
@cert_path = our_cert_tmpfile.path res[:cert_path] = our_cert_tmpfile.path
our_cert_keypair_tmpfile = Tempfile.new 'our_cert_keypair' our_cert_keypair_tmpfile = Tempfile.new 'our_cert_keypair'
our_cert_keypair_tmpfile.write our_cert_keypair.to_pem our_cert_keypair_tmpfile.write our_cert_keypair.to_pem
our_cert_keypair_tmpfile.close our_cert_keypair_tmpfile.close
@key_path = our_cert_keypair_tmpfile.path res[:key_path] = our_cert_keypair_tmpfile.path
ca_cert_tmpfile = Tempfile.new 'ca_cert' ca_cert_tmpfile = Tempfile.new 'ca_cert'
ca_cert_tmpfile.write @ca_cert.to_pem ca_cert_tmpfile.write res[:ca_cert].to_pem
ca_cert_tmpfile.close ca_cert_tmpfile.close
@cert_intermediate_path = ca_cert_tmpfile.path res[:cert_intermediate_path] = ca_cert_tmpfile.path
combined_cert_tmpfile = Tempfile.new 'combined_cert'
combined_cert_tmpfile.write "#{File.read(res[:cert_path])}\n#{File.read(res[:cert_intermediate_path])}"
combined_cert_tmpfile.close
res[:combined_cert_path] = combined_cert_tmpfile.path
bad_combined_cert_tmpfile = Tempfile.new 'combined_cert'
bad_combined_cert_tmpfile.write "#{File.read(res[:cert_intermediate_path])}\n#{File.read(res[:cert_path])}"
bad_combined_cert_tmpfile.close
res[:bad_combined_cert_path] = bad_combined_cert_tmpfile.path
res
end
describe 'site/settings' do
describe 'ssl' do
include Capybara::DSL
before do
@domain = SecureRandom.uuid.gsub('-', '')+'.com'
@site = Fabricate :site, domain: @domain
page.set_rack_session id: @site.id
end end
@ -87,71 +105,82 @@ describe 'site/settings' do
page.must_have_content /Cannot upload SSL certificate until domain is added/i page.must_have_content /Cannot upload SSL certificate until domain is added/i
end end
it 'works with valid key, cert and intermediate cert' do it 'fails with expired key' do
@ssl = generate_ssl_certs domain: @domain, expired: true
visit '/custom_domain' visit '/custom_domain'
attach_file 'key', @ssl[:key_path]
attach_file 'cert', @ssl[:combined_cert_path]
click_button 'Upload SSL Key and Certificate'
page.must_have_content /ssl certificate has expired/i
end
it 'works with valid key and unified cert' do
@ssl = generate_ssl_certs domain: @domain
visit '/custom_domain'
key = File.read @ssl[:key_path]
combined_cert = File.read @ssl[:combined_cert_path]
page.must_have_content /status: inactive/i page.must_have_content /status: inactive/i
attach_file 'key', @key_path attach_file 'key', @ssl[:key_path]
attach_file 'cert', @cert_path attach_file 'cert', @ssl[:combined_cert_path]
attach_file 'cert_intermediate', @cert_intermediate_path
click_button 'Upload SSL Key and Certificate' click_button 'Upload SSL Key and Certificate'
page.current_path.must_equal '/custom_domain' page.current_path.must_equal '/custom_domain'
page.must_have_content /Updated SSL/ page.must_have_content /Updated SSL/
page.must_have_content /status: installed/i page.must_have_content /status: installed/i
@site.reload @site.reload
@site.ssl_key.must_equal File.read(@key_path) @site.ssl_key.must_equal key
@site.ssl_cert.must_equal File.read(@cert_path) @site.ssl_cert.must_equal combined_cert
@site.ssl_cert_intermediate.must_equal File.read(@cert_intermediate_path)
end end
it 'fails with no uploads' do it 'fails with no uploads' do
visit '/custom_domain' visit '/custom_domain'
click_button 'Upload SSL Key and Certificate' click_button 'Upload SSL Key and Certificate'
page.current_path.must_equal '/custom_domain' page.current_path.must_equal '/custom_domain'
page.must_have_content /ssl key.+certificate.+intermediate.+required to continue/i page.must_have_content /ssl key.+certificate.+required/i
@site.reload @site.reload
@site.ssl_key.must_equal nil @site.ssl_key.must_equal nil
@site.ssl_cert.must_equal nil @site.ssl_cert.must_equal nil
@site.ssl_cert_intermediate.must_equal nil
end end
it 'fails with encrypted key' do it 'fails gracefully with encrypted key' do
@ssl = generate_ssl_certs domain: @domain
visit '/custom_domain' visit '/custom_domain'
attach_file 'key', './tests/files/ssl/derpie.com-encrypted.key' attach_file 'key', './tests/files/ssl/derpie.com-encrypted.key'
attach_file 'cert', @cert_path attach_file 'cert', @ssl[:cert_path]
attach_file 'cert_intermediate', @cert_intermediate_path
click_button 'Upload SSL Key and Certificate' click_button 'Upload SSL Key and Certificate'
page.current_path.must_equal '/custom_domain' page.current_path.must_equal '/custom_domain'
page.must_have_content /could not process ssl key/i page.must_have_content /could not process ssl key/i
end end
it 'fails with junk key' do it 'fails with junk key' do
@ssl = generate_ssl_certs domain: @domain
visit '/custom_domain' visit '/custom_domain'
attach_file 'key', './tests/files/index.html' attach_file 'key', './tests/files/index.html'
attach_file 'cert', @cert_path attach_file 'cert', @ssl[:cert_path]
attach_file 'cert_intermediate', @cert_intermediate_path
click_button 'Upload SSL Key and Certificate' click_button 'Upload SSL Key and Certificate'
page.current_path.must_equal '/custom_domain' page.current_path.must_equal '/custom_domain'
page.must_have_content /could not process ssl key/i page.must_have_content /could not process ssl key/i
end end
it 'fails with junk cert' do it 'fails with junk cert' do
@ssl = generate_ssl_certs domain: @domain
visit '/custom_domain' visit '/custom_domain'
attach_file 'key', @key_path attach_file 'key', @ssl[:key_path]
attach_file 'cert', './tests/files/index.html' attach_file 'cert', './tests/files/index.html'
attach_file 'cert_intermediate', @cert_intermediate_path
click_button 'Upload SSL Key and Certificate' click_button 'Upload SSL Key and Certificate'
page.current_path.must_equal '/custom_domain' page.current_path.must_equal '/custom_domain'
page.must_have_content /could not process ssl certificate/i page.must_have_content /could not process ssl certificate/i
end end
it 'fails with junk intermediate cert' do if ENV['TRAVIS'] != 'true'
it 'fails with bad cert chain' do
@ssl = generate_ssl_certs domain: @domain
visit '/custom_domain' visit '/custom_domain'
attach_file 'key', @key_path attach_file 'key', @ssl[:key_path]
attach_file 'cert', @cert_path attach_file 'cert', @ssl[:bad_combined_cert_path]
attach_file 'cert_intermediate', './tests/files/index.html'
click_button 'Upload SSL Key and Certificate' click_button 'Upload SSL Key and Certificate'
page.current_path.must_equal '/custom_domain' page.current_path.must_equal '/custom_domain'
page.must_have_content /could not process intermediate ssl certificate/i page.must_have_content /there is something wrong with your certificate/i
end
end end
end end

View file

@ -46,7 +46,7 @@
<article> <article>
<h2>Add SSL Certificate</h2> <h2>Add SSL Certificate</h2>
<p> <p>
This allows you to add an SSL 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. This allows you to add an SSL 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. All files must be in PEM format. If your certificate is not bundled with the root and intermediate certificates, ask your certificate provider for help on how to do that.
</p> </p>
<% if current_site.domain.nil? || current_site.domain.empty? %> <% if current_site.domain.nil? || current_site.domain.empty? %>
@ -68,15 +68,10 @@
</p> </p>
<p> <p>
Primary Certificate (yourdomain.com.crt): Bundled Certificates (yourdomain.com-bundle.crt):
<input name="cert" type="file"> <input name="cert" type="file">
</p> </p>
<p>
Intermediate Certificate (ca.crt/yourca.crt):
<input name="cert_intermediate" type="file">
</p>
<input class="btn-Action" type="submit" value="Upload SSL Key and Certificate"> <input class="btn-Action" type="submit" value="Upload SSL Key and Certificate">
</form> </form>