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

89
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
begin certs_string = params[:cert][:tempfile].read
cert = OpenSSL::X509::Certificate.new params[:cert][:tempfile].read
rescue => e cert_array = certs_string.lines.slice_before(/-----BEGIN CERTIFICATE-----/).to_a.collect {|a| a.join}
flash[:error] = 'Could not process SSL certificate, file may be incorrect or damaged.'
if cert_array.empty?
flash[:error] = 'Cert file does not contain any certificates.'
redirect '/custom_domain' redirect '/custom_domain'
end end
if cert.not_after < Time.now cert_valid_for_domain = false
flash[:error] = 'SSL Certificate is expired, please create a new one.'
cert_array.each do |cert_string|
begin
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 has expired, please create a new one.'
redirect '/custom_domain'
end
cert_cn = cert.subject.to_a.select {|a| a.first == 'CN'}.flatten[1]
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' redirect '/custom_domain'
end end
cert_cn = cert.subject.to_a.select {|a| a.first == 'CN'}.flatten[1] # Everything else was worse.
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 crtfile = Tempfile.new 'crtfile'
cert_intermediate = OpenSSL::X509::Certificate.new params[:cert_intermediate][:tempfile].read crtfile.write cert_array.join
rescue => e crtfile.close
flash[:error] = 'Could not process intermediate SSL certificate, file may be incorrect or damaged.'
redirect '/custom_domain' 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
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 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,83 +1,101 @@
require_relative './environment.rb' require_relative './environment.rb'
def generate_ssl_certs(opts={})
# https://github.com/kyledrake/ruby-openssl-cheat-sheet/blob/master/certificate_authority.rb
res = {}
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
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)
res[:ca_cert] = ca_cert
res[:ca_keypair] = ca_keypair
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([
["C", "US"],
["ST", "Oregon"],
["L", "Portland"],
["O", "Neocities User"],
["CN", "*.#{opts[: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
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.
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 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
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
res[:key_path] = our_cert_keypair_tmpfile.path
ca_cert_tmpfile = Tempfile.new 'ca_cert'
ca_cert_tmpfile.write res[:ca_cert].to_pem
ca_cert_tmpfile.close
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 'site/settings' do
describe 'ssl' do describe 'ssl' do
include Capybara::DSL include Capybara::DSL
before do 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' @domain = SecureRandom.uuid.gsub('-', '')+'.com'
@site = Fabricate :site, domain: @domain @site = Fabricate :site, domain: @domain
page.set_rack_session id: @site.id 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 end
it 'fails without domain set' do it 'fails without domain set' do
@ -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'
visit '/custom_domain' it 'fails with bad cert chain' do
attach_file 'key', @key_path @ssl = generate_ssl_certs domain: @domain
attach_file 'cert', @cert_path visit '/custom_domain'
attach_file 'cert_intermediate', './tests/files/index.html' attach_file 'key', @ssl[:key_path]
click_button 'Upload SSL Key and Certificate' attach_file 'cert', @ssl[:bad_combined_cert_path]
page.current_path.must_equal '/custom_domain' click_button 'Upload SSL Key and Certificate'
page.must_have_content /could not process intermediate ssl certificate/i page.current_path.must_equal '/custom_domain'
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>