Skip to content

Instantly share code, notes, and snippets.

@arika
Last active June 25, 2017 05:20
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arika/7ece215699f354e03eae8ec737a282ac to your computer and use it in GitHub Desktop.
Save arika/7ece215699f354e03eae8ec737a282ac to your computer and use it in GitHub Desktop.
# 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
@arika
Copy link
Author

arika commented Apr 22, 2017

Output:

SSLVerifyClient CA Result CN SERIAL CODE
none ca1 NONE (null) (null) 200
none ca2 NONE (null) (null) 200
none self NONE (null) (null) 200
none (none) NONE (null) (null) 200
optional ca1 SUCCESS my-name 04 200
optional ca2 SSL_read: ssl handshake failure (Client side error) - - -
optional self SSL_read: ssl handshake failure (Client side error) - - -
optional (none) NONE (null) (null) 200
optional_no_ca ca1 SUCCESS my-name 04 200
optional_no_ca ca2 FAILED:unable to verify the first certificate my-name 05 200
optional_no_ca self FAILED:unable to verify the first certificate my-name 06 200
optional_no_ca (none) NONE (null) (null) 200
require ca1 SUCCESS my-name 04 200
require ca2 SSL_read: ssl handshake failure (Client side error) - - -
require self SSL_read: ssl handshake failure (Client side error) - - -
require (none) SSL_read: ssl handshake failure (Client side error) - - -
  • 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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment