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'
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(ssl_key: nil).
exclude(ssl_cert: nil).
exclude(ssl_cert_intermediate: nil).
all
payload = []
FileUtils.rm './files/sslsites.zip'
Zip::Archive.open('./files/sslsites.zip', Zip::CREATE) do |ar|
ar.add_dir 'ssl'
sites.each do |site|
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}
end

71
app.rb
View file

@ -489,8 +489,8 @@ 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.'
unless params[:key] && params[:cert]
flash[:error] = 'SSL key and certificate are required.'
redirect '/custom_domain'
end
@ -506,34 +506,83 @@ post '/settings/ssl' do
redirect '/custom_domain'
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
cert = OpenSSL::X509::Certificate.new params[:cert][:tempfile].read
cert = OpenSSL::X509::Certificate.new cert_string
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.'
flash[:error] = 'SSL Certificate has 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}"
cert_valid_for_domain = true if cert_cn && cert_cn.match(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'
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
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.'
output = line.run path: nginx_testfile.path
rescue Cocaine::ExitStatusError => e
flash[:error] = "There is something wrong with your certificate, please check with your issuing CA."
redirect '/custom_domain'
end
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.ssl_cert = cert_array.join
current_site.save
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
def ssl_installed?
ssl_key && ssl_cert && ssl_cert_intermediate
ssl_key && ssl_cert
end
def to_rss

View file

@ -1,12 +1,9 @@
require_relative './environment.rb'
describe 'site/settings' do
describe 'ssl' do
include Capybara::DSL
before do
def generate_ssl_certs(opts={})
# 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_cert = OpenSSL::X509::Certificate.new
ca_cert.not_before = Time.now
@ -17,7 +14,6 @@ describe 'site/settings' do
["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
@ -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("authorityKeyIdentifier", "keyid:always", false))
ca_cert.sign(ca_keypair, OpenSSL::Digest::SHA256.new)
@ca_cert = ca_cert
@ca_keypair = ca_keypair
res[:ca_cert] = ca_cert
res[: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)
ca_cert = OpenSSL::X509::Certificate.new(res[: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([
@ -45,7 +37,7 @@ describe 'site/settings' do
["ST", "Oregon"],
["L", "Portland"],
["O", "Neocities User"],
["CN", "*.#{@domain}"]
["CN", "*.#{opts[:domain]}"]
])
our_cert_req.public_key = our_cert_keypair.public_key
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.issuer = ca_cert.subject
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.public_key = our_cert_req.public_key
# 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.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.sign res[: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
res[: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
res[: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.write res[:ca_cert].to_pem
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
@ -87,71 +105,82 @@ describe 'site/settings' do
page.must_have_content /Cannot upload SSL certificate until domain is added/i
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'
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
attach_file 'key', @key_path
attach_file 'cert', @cert_path
attach_file 'cert_intermediate', @cert_intermediate_path
attach_file 'key', @ssl[:key_path]
attach_file 'cert', @ssl[:combined_cert_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)
@site.ssl_key.must_equal key
@site.ssl_cert.must_equal combined_cert
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
page.must_have_content /ssl key.+certificate.+required/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
it 'fails gracefully with encrypted key' do
@ssl = generate_ssl_certs domain: @domain
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
attach_file 'cert', @ssl[:cert_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
@ssl = generate_ssl_certs domain: @domain
visit '/custom_domain'
attach_file 'key', './tests/files/index.html'
attach_file 'cert', @cert_path
attach_file 'cert_intermediate', @cert_intermediate_path
attach_file 'cert', @ssl[:cert_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
@ssl = generate_ssl_certs domain: @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_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
if ENV['TRAVIS'] != 'true'
it 'fails with bad cert chain' do
@ssl = generate_ssl_certs domain: @domain
visit '/custom_domain'
attach_file 'key', @key_path
attach_file 'cert', @cert_path
attach_file 'cert_intermediate', './tests/files/index.html'
attach_file 'key', @ssl[:key_path]
attach_file 'cert', @ssl[:bad_combined_cert_path]
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
page.must_have_content /there is something wrong with your certificate/i
end
end
end

View file

@ -46,7 +46,7 @@
<article>
<h2>Add SSL Certificate</h2>
<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>
<% if current_site.domain.nil? || current_site.domain.empty? %>
@ -68,15 +68,10 @@
</p>
<p>
Primary Certificate (yourdomain.com.crt):
Bundled Certificates (yourdomain.com-bundle.crt):
<input name="cert" type="file">
</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">
</form>