allow adding of ssl certs for domain

This commit is contained in:
Kyle Drake 2014-09-02 18:36:17 -07:00
parent 92fcee0aea
commit af605f352e
7 changed files with 307 additions and 14 deletions

54
app.rb
View file

@ -486,6 +486,60 @@ post '/settings/profile' do
redirect '/settings' redirect '/settings'
end 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 post '/signin' do
dashboard_if_signed_in dashboard_if_signed_in

12
migrations/040_add_ssl.rb Normal file
View file

@ -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

View file

@ -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

View file

@ -798,6 +798,10 @@ class Site < Sequel::Model
"#{THUMBNAILS_URL_ROOT}/#{values[:username]}/#{path}.#{resolution}.#{ext}" "#{THUMBNAILS_URL_ROOT}/#{values[:username]}/#{path}.#{resolution}.#{ext}"
end end
def ssl_installed?
ssl_key && ssl_cert && ssl_cert_intermediate
end
def to_rss def to_rss
RSS::Maker.make("atom") do |maker| RSS::Maker.make("atom") do |maker|
maker.channel.title = title maker.channel.title = title
@ -808,8 +812,8 @@ class Site < Sequel::Model
latest_events.each do |event| latest_events.each do |event|
if event.site_change_id if event.site_change_id
maker.items.new_item do |item| maker.items.new_item do |item|
item.link = "http://#{username}.neocities.org" item.link = "https://#{host}"
item.title = "#{username}.neocities.org has been updated" item.title = "#{title} has been updated"
item.updated = event.site_change.created_at item.updated = event.site_change.created_at
end end
end end

View file

@ -1,6 +1,160 @@
require_relative './environment.rb' require_relative './environment.rb'
describe 'site/settings' do 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 describe 'change username' do
include Capybara::DSL include Capybara::DSL

View file

@ -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-----

View file

@ -9,17 +9,11 @@
<h3></h3> <h3></h3>
<article> <article>
<% if !current_site.errors.empty? %> <% if flash.keys.length > 0 %>
<div class="alert alert-block alert-error"> <div class="alert alert-block alert-success">
<% current_site.errors.each do |error| %> <% flash.keys.each do |key| %>
<p><%= error.last.first %></p> <%== flash[key] %>
<% end %> <% end %>
</div>
<% end %>
<% if flash[:success] %>
<div class="alert alert-block alert-success" style="margin-top: 20px">
<%== flash[:success] %>
</div> </div>
<% end %> <% end %>
@ -47,9 +41,45 @@
<br> <br>
<input class="btn-Action" type="submit" value="Update Domain"> <input class="btn-Action" type="submit" value="Update Domain">
</form> </form>
</article>
<article>
<h2>Add SSL Certificate</h2>
<p> <p>
<strong>NOTE: This is for advanced users, we cannot provide technical support for this feature.</strong> 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.
</p> </p>
<% if current_site.domain.nil? || current_site.domain.empty? %>
<p><strong>Cannot upload SSL certificate until domain is added.</strong></p>
<% else %>
<form method="POST" action="/settings/ssl" enctype="multipart/form-data">
<%== csrf_token_input_html %>
<p>
<strong>
Status: <%= current_site.ssl_installed? ? 'Installed' : 'Inactive' %>
</strong>
</p>
<p>
Private key (yourdomain.com.key):
<input name="key" type="file">
</p>
<p>
Primary Certificate (yourdomain.com.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>
<% end %>
</article> </article>
</div> </div>