Last active
June 25, 2017 05:20
-
-
Save arika/7ece215699f354e03eae8ec737a282ac to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Generate files: | |
# | |
# ``` | |
# ruby client_auth_test.rb dir | |
# ``` | |
# | |
# Run Apache HTTP server: | |
# | |
# ``` | |
# apachectl -d dir -f httpd.conf -X | |
# ``` | |
# | |
# Test: | |
# | |
# ``` | |
# curl -v \ | |
# --cacert dir/ca1.crt \ | |
# --cert z/ca1_client.crt \ | |
# --key z/ca1_client.key \ | |
# --resolve 'server.example.jp:8888:127.0.0.1' \ | |
# https://server.example.jp:8888/none/httpd.pid | |
# | |
# ruby -r net/http -e ' | |
# h = Net::HTTP.new("server.example.jp", 8888) | |
# def h.address; "localhost"; end | |
# h.use_ssl = true | |
# h.ca_file = "dir/ca1.crt" | |
# h.key = OpenSSL::PKey::RSA.new(File.read("dir/ca1_client.key")) | |
# h.cert = OpenSSL::X509::Certificate.new(File.read("dir/ca1_client.crt")) | |
# p h.get("/none/http.pid").to_hash | |
# ' | |
# ``` | |
# | |
# Auto-test: | |
# | |
# ``` | |
# ruby client_auth_test.rb | |
# ``` | |
# | |
# Ref: | |
# | |
# * https://docs.ruby-lang.org/en/2.4.0/OpenSSL/X509/Certificate.html#class-OpenSSL::X509::Certificate-label-Creating+a+root+CA+certificate+and+an+end-entity+certificate | |
require 'openssl' | |
require 'fileutils' | |
require 'tmpdir' | |
require 'net/http' | |
$serial = 0 | |
def serial | |
$serial += 1 | |
end | |
def root_ca(cn) | |
root_key = OpenSSL::PKey::RSA.new 2048 # the CA's public/private key | |
root_ca = OpenSSL::X509::Certificate.new | |
root_ca.version = 2 # cf. RFC 5280 - to make it a 'v3' certificate | |
root_ca.serial = serial | |
root_ca.subject = OpenSSL::X509::Name.parse "/DC=jp/DC=example/CN=#{cn}" | |
root_ca.issuer = root_ca.subject # root CA's are 'self-signed' | |
root_ca.public_key = root_key.public_key | |
root_ca.not_before = Time.now - 10*24*60*60 | |
root_ca.not_after = root_ca.not_before + 20*24*60*60 | |
ef = OpenSSL::X509::ExtensionFactory.new | |
ef.subject_certificate = root_ca | |
ef.issuer_certificate = root_ca | |
root_ca.add_extension(ef.create_extension('basicConstraints', 'CA:TRUE', true)) | |
root_ca.add_extension(ef.create_extension('keyUsage', 'keyCertSign, cRLSign', true)) | |
root_ca.add_extension(ef.create_extension('subjectKeyIdentifier', 'hash', false)) | |
root_ca.add_extension(ef.create_extension('authorityKeyIdentifier', 'keyid:always', false)) | |
root_ca.sign(root_key, OpenSSL::Digest::SHA256.new) | |
[root_key, root_ca] | |
end | |
def end_entity(cn, root_key, root_ca, not_before: nil, not_after: nil) | |
key = OpenSSL::PKey::RSA.new 2048 | |
cert = OpenSSL::X509::Certificate.new | |
cert.version = 2 | |
cert.serial = serial | |
cert.subject = OpenSSL::X509::Name.parse "/DC=jp/DC=example/CN=#{cn}" | |
cert.issuer = root_ca ? root_ca.subject : cert.subject | |
cert.public_key = key.public_key | |
cert.not_before = not_before || Time.now - 5*24*60*60 | |
cert.not_after = not_after || cert.not_before + 10*24*60*60 | |
ef = OpenSSL::X509::ExtensionFactory.new | |
ef.subject_certificate = cert | |
ef.issuer_certificate = root_ca || cert | |
cert.add_extension(ef.create_extension('keyUsage', 'digitalSignature', true)) | |
cert.add_extension(ef.create_extension('subjectKeyIdentifier', 'hash', false)) | |
cert.sign(root_key || key, OpenSSL::Digest::SHA256.new) | |
[key, cert] | |
end | |
def key_export(filename, key) | |
open(filename, 'wb') {|io| io.write key.export } | |
end | |
def crt_export(filename, crt) | |
open(filename, 'wb') {|io| io.write crt.to_pem } | |
end | |
def pkcs12_export(filename, name, key, crt, cas = nil, pass = 'pass') | |
pkcs12 = OpenSSL::PKCS12.create(pass, name, key, crt, cas) | |
open(filename, 'wb') {|io| io.write pkcs12.to_der } | |
end | |
def apachectl(dir, *args) | |
system 'apachectl', '-d', dir, '-f', "#{dir}/httpd.conf", *args | |
end | |
def write_httpd_conf(dir, hostname, key_file, crt_file, ca_crt_file) | |
module_dir = %w( | |
/usr/lib/apache2/modules | |
/usr/libexec/apache2 | |
).detect {|d| File.exist?(d) } | |
open("#{dir}/httpd.conf", 'wb') do |io| | |
[ | |
%w(mpm_worker_module mod_mpm_worker.so), | |
%w(unixd_module mod_unixd.so), | |
%w(authn_core_module mod_authn_core.so), | |
%w(authz_core_module mod_authz_core.so), | |
%w(log_config_module mod_log_config.so) | |
].each do |mname, mfile| | |
mfile = "#{module_dir}/#{mfile}" | |
io.puts "LoadModule #{mname} #{mfile}" if File.exist?(mfile) | |
end | |
io.write <<-E | |
LoadModule headers_module #{module_dir}/mod_headers.so | |
LoadModule alias_module #{module_dir}/mod_alias.so | |
LoadModule ssl_module #{module_dir}/mod_ssl.so | |
LogFormat "%h %l %u %t \\"%r\\" %>s %b %X" common | |
CustomLog access.log common | |
LogLevel debug | |
ErrorLog error.log | |
PidFile httpd.pid | |
Listen 8888 | |
ServerName #{hostname} | |
SSLEngine on | |
SSLOptions +StdEnvVars +ExportCertData | |
SSLCertificateFile #{crt_file} | |
SSLCertificateKeyFile #{key_file} | |
SSLCaCertificateFile #{ca_crt_file} | |
Header set X-Client-Verify %{SSL_CLIENT_VERIFY}s | |
Header set X-Client-DN-CN %{SSL_CLIENT_S_DN_CN}s | |
Header set X-Client-M-Serial %{SSL_CLIENT_M_SERIAL}s | |
DocumentRoot "#{dir}" | |
<Directory "#{dir}"> | |
AuthType None | |
Require all granted | |
</Directory> | |
E | |
%w(none optional optional_no_ca require).each do |t| | |
io.write <<-E | |
Alias /#{t} #{dir}/ | |
<Location /#{t}> | |
SSLVerifyClient #{t} | |
</Location> | |
E | |
end | |
unless File.exist?('/dev/random') | |
io.write <<-E | |
SSLRandomSeed startup builtin | |
SSLRandomSeed connect builtin | |
E | |
end | |
end | |
apachectl dir, '-t' | |
end | |
def get(target, server_name, key, crt, ca_file) | |
h = Net::HTTP.new(server_name, 8888) | |
def h.address | |
'localhost' | |
end | |
h.use_ssl = true | |
h.ca_file = ca_file | |
h.key = key | |
h.cert = crt | |
# h.set_debug_output($stderr) | |
h.get("/#{target}/httpd.pid") | |
end | |
def chdir(dir) | |
if dir | |
dir = "#{Dir.pwd}/#{dir}" unless %r{\A/} =~ dir | |
FileUtils.mkdir_p(dir) | |
Dir.chdir(dir) { yield(dir) } | |
else | |
Dir.mktmpdir do |td| | |
Dir.chdir(td) { yield(td) } | |
end | |
end | |
end | |
t = {} # { not_after: Time.now - 300 } | |
ca1_key, ca1_crt = root_ca('CA1') | |
ca2_key, ca2_crt = root_ca('CA2') | |
server_key, server_crt = end_entity('server.example.jp', ca1_key, ca1_crt) | |
ca1_client_key, ca1_client_crt = end_entity('my-name', ca1_key, ca1_crt, t) | |
ca2_client_key, ca2_client_crt = end_entity('my-name', ca2_key, ca2_crt, t) | |
self_client_key, self_client_crt = end_entity('my-name', nil, nil, t) | |
conf_dir = ARGV.shift | |
chdir(conf_dir) do |dir| | |
[ | |
['ca1', ca1_key, ca1_crt], | |
['ca2', ca2_key, ca2_crt], | |
['server', server_key, server_crt], | |
].each do |name, key, crt| | |
key_export("#{name}.key", key) | |
crt_export("#{name}.crt", crt) | |
end | |
server_name = server_crt.subject.to_a.assoc('CN')[1] | |
begin | |
write_httpd_conf(dir, server_name, 'server.key', 'server.crt', 'ca1.crt') | |
unless conf_dir | |
apachectl dir, '-k', 'start' | |
results = {} | |
end | |
[ | |
['ca1', ca1_client_key, ca1_client_crt, ca1_crt], | |
['ca2', ca2_client_key, ca2_client_crt, ca2_crt], | |
['self', self_client_key, self_client_crt], | |
['(none)'] | |
].each do |prefix, key, crt, ca| | |
if key | |
pkcs12_export("#{prefix}_client.p12", "#{prefix}-with-ca", key, crt, [ca]) if ca | |
pkcs12_export("#{prefix}_client_noca.p12", "#{prefix}-with-no-ca", key, crt) | |
key_export("#{prefix}_client.key", key) | |
crt_export("#{prefix}_client.crt", crt) | |
end | |
unless conf_dir | |
# auto-test | |
%w(none optional optional_no_ca require).each do |t| | |
begin | |
resp = get(t, server_name, key, crt, 'ca1.crt') | |
result = [ | |
resp['X-Client-Verify'], | |
resp['X-Client-DN-CN'], | |
resp['X-Client-M-Serial'], | |
resp.code | |
] | |
rescue => ex | |
result = [ | |
"#{ex.message} (Client side error)", | |
'-', '-', '-' | |
] | |
end | |
results[t] ||= {} | |
results[t][prefix] = result | |
end | |
end | |
end | |
ensure | |
unless conf_dir | |
apachectl dir, '-k', 'graceful-stop' | |
puts 'SSLVerifyClient | CA | Result | CN | SERIAL | CODE' | |
puts '--------------- | -- | ------ | -- | ------ | ----' | |
results.each do |t, v| | |
v.each do |prefix, result| | |
puts [t, prefix, *result].join(' | ') | |
end | |
end | |
puts | |
puts <<-E | |
* CA - Client certificate's CA | |
* Result - CLIENT_VERIFY value or Client side error | |
* CN - SSL_CLIENT_S_DN_CN (mod_ssl) | |
* SERIAL - SSL_CLIENT_M_SERIAL (mod_ssl) | |
* Code - HTTP response code | |
E | |
end | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Output: