Skip to content

Commit

Permalink
Add ssl_ca_file and ssl_ca_path options to allow SSL certificate veri…
Browse files Browse the repository at this point in the history
…fication.
  • Loading branch information
Dwayne Litzenberger authored and sandro committed Jul 19, 2010
1 parent c068bbf commit f9c7478
Show file tree
Hide file tree
Showing 15 changed files with 305 additions and 2 deletions.
20 changes: 20 additions & 0 deletions lib/httparty.rb
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,26 @@ def pem(pem_contents)
default_options[:pem] = pem_contents
end

# Allows setting an OpenSSL certificate authority file
#
# class Foo
# include HTTParty
# ssl_ca_file '/etc/ssl/certs/ca-certificates.crt'
# end
def ssl_ca_file(path)
default_options[:ssl_ca_file] = path
end

# Allows setting an OpenSSL certificate authority path (directory)
#
# class Foo
# include HTTParty
# ssl_ca_path '/etc/ssl/certs/'
# end
def ssl_ca_path(path)
default_options[:ssl_ca_path] = path
end

# Allows setting a custom parser for the response.
#
# class Foo
Expand Down
14 changes: 12 additions & 2 deletions lib/httparty/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -68,12 +68,22 @@ def http
http.read_timeout = options[:timeout]
end

# By default, don't do any SSL verification (!), but this can be overridden.
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
# Client certificate authentication
if options[:pem] && http.use_ssl?
http.cert = OpenSSL::X509::Certificate.new(options[:pem])
http.key = OpenSSL::PKey::RSA.new(options[:pem])
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
else
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
end
# SSL certificate authority file and/or directory
if options[:ssl_ca_file] && http.use_ssl?
http.ca_file = options[:ssl_ca_file]
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
end
if options[:ssl_ca_path] && http.use_ssl?
http.ca_path = options[:ssl_ca_path]
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
end

if options[:debug_output]
Expand Down
29 changes: 29 additions & 0 deletions spec/fixtures/ssl/generate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#!/bin/sh
set -e

if [ -d "generated" ] ; then
echo >&2 "error: 'generated' directory already exists. Delete it first."
exit 1
fi

mkdir generated

# Generate the CA private key and certificate
openssl req -batch -subj '/CN=INSECURE Test Certificate Authority' -newkey rsa:1024 -new -x509 -days 999999 -keyout generated/ca.key -nodes -out generated/ca.crt

# Create symlinks for ssl_ca_path
c_rehash generated

# Generate the server private key and self-signed certificate
openssl req -batch -subj '/CN=localhost' -newkey rsa:1024 -new -x509 -days 999999 -keyout generated/server.key -nodes -out generated/selfsigned.crt

# Generate certificate signing request with bogus hostname
openssl req -batch -subj '/CN=bogo' -new -days 999999 -key generated/server.key -nodes -out generated/bogushost.csr

# Sign the certificate requests
openssl x509 -CA generated/ca.crt -CAkey generated/ca.key -set_serial 1 -in generated/selfsigned.crt -out generated/server.crt -clrext -extfile openssl-exts.cnf -extensions cert
openssl x509 -req -CA generated/ca.crt -CAkey generated/ca.key -set_serial 1 -in generated/bogushost.csr -out generated/bogushost.crt -clrext -extfile openssl-exts.cnf -extensions cert

# Remove certificate signing requests
rm -f generated/*.csr

1 change: 1 addition & 0 deletions spec/fixtures/ssl/generated/1fe462c2.0
13 changes: 13 additions & 0 deletions spec/fixtures/ssl/generated/bogushost.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIICAzCCAWygAwIBAgIBATANBgkqhkiG9w0BAQUFADAuMSwwKgYDVQQDEyNJTlNF
Q1VSRSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0xMDA3MDkwMTU5MTda
Fw0xMDA4MDgwMTU5MTdaMA8xDTALBgNVBAMTBGJvZ28wgZ8wDQYJKoZIhvcNAQEB
BQADgY0AMIGJAoGBAKMU3pAeBZzKYcYF8eDIPvb9qYF8odH9ZK23IGn1T9D9Oxd0
2+IltkMJ0sOWpknp+kTzbAP0dammMNExt/YFuFqN4MenTMp87FFQAXSK0WhtjNcb
Fl9gv4iThWedFVSq3qZs8c+ZTCrrC/A/ScGAH7g4C57Degci7SCdf4v97m7nAgMB
AAGjUDBOMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFMXuuqokuPcMMS7OFu0jeoGK
oJsDMB8GA1UdIwQYMBaAFJYXkYdQ2afmGvLRN9YGOqrGK/MLMA0GCSqGSIb3DQEB
BQUAA4GBAIPfOu+RadQpZlSaMg3m7t7wn3yPPp2yXtz6s98JqBvoTtZZ0f9JMG6z
muVss0JmHPTyPDlNk54DaySa0wAUArAqTUvq05U+VoxtN7QEqR9bgdsRSByowVax
/01r5CdrMC/xDs4OWz3sv8Kyw0hmd9nnCvMoMUQgEZsjNcEPmN7K
-----END CERTIFICATE-----
15 changes: 15 additions & 0 deletions spec/fixtures/ssl/generated/ca.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN CERTIFICATE-----
MIICazCCAdSgAwIBAgIJAMBBDJhEZSUlMA0GCSqGSIb3DQEBBQUAMC4xLDAqBgNV
BAMTI0lOU0VDVVJFIFRlc3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5MB4XDTEwMDcw
OTAxNTkxN1oXDTI2MDUxOTE2MzM1N1owLjEsMCoGA1UEAxMjSU5TRUNVUkUgVGVz
dCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJ
AoGBAMAfEr8RkcWZhIwj7zz2F+phGpASWiO6EPUNsltXvksPo06DGJJJqWWJ/zUm
/1m9Wjd0yx1dinT+C1jpViWTvJZ0KaSs29PW1EFuwZ8tOlm4TdWti6mGp9QZ5KVU
24QXAAbrvXrEVBUfCmhd1gxrqvXH9gMbtndXtDnmbba6u2ehAgMBAAGjgZAwgY0w
HQYDVR0OBBYEFJYXkYdQ2afmGvLRN9YGOqrGK/MLMF4GA1UdIwRXMFWAFJYXkYdQ
2afmGvLRN9YGOqrGK/MLoTKkMDAuMSwwKgYDVQQDEyNJTlNFQ1VSRSBUZXN0IENl
cnRpZmljYXRlIEF1dGhvcml0eYIJAMBBDJhEZSUlMAwGA1UdEwQFMAMBAf8wDQYJ
KoZIhvcNAQEFBQADgYEAgN+TxBq4sYMnJZ3WHZ/RLcnpIbZdElUuM3lBbwKOmL5E
KZ7uh5ZzyihNMnuw61MqvSMjwZfipyD0xNhX7e4dF47Q1oOUaMSxTS92PgQ22otY
IMVtP7r8h8op7oKsiaSu4Y984abXirhMdVa1nUvyliBSYs94YIyZtwujhQJKN5Y=
-----END CERTIFICATE-----
15 changes: 15 additions & 0 deletions spec/fixtures/ssl/generated/ca.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXgIBAAKBgQDAHxK/EZHFmYSMI+889hfqYRqQElojuhD1DbJbV75LD6NOgxiS
Sallif81Jv9ZvVo3dMsdXYp0/gtY6VYlk7yWdCmkrNvT1tRBbsGfLTpZuE3VrYup
hqfUGeSlVNuEFwAG6716xFQVHwpoXdYMa6r1x/YDG7Z3V7Q55m22urtnoQIDAQAB
AoGAMJMqsDiG/Mj15GDpiiZGobHvf2HEfKf8xZiy8blbmarYhW9L9SC+vbeIWS4E
/fGML91NxZzy9uWMhOxqJZIW6hsPbaZaxWcI/Ar78YcwskKRZC8vZd2bZ+jbwp6Y
rI0/FVla42wcFXr6dbdnLKOeYBurB/F/839jeErjoWJ5F2kCQQDsmOl9A+ni+OYI
We8/Vc/BASVnpKbvYYUi4BlzDjhcj5cn44pIuGRS/VDfmiQTGPf/gMOd6GM2jDUm
kvFUZY5fAkEAz+BwsTZAwF9vPOBu9iuVsCzJ+OhyNtl1PrWDqEdMkFRHN++eXkL0
U4uNMLma3pCDLJ2bv49mTPzDu2AY03bJ/wJBALMSzW5MvwKGhnz9rOJQDa20M15t
tdfrBLyvxzNZKPmNyMdtJiYCQhS6HDMRVIqL1HCzQdvLnwQTPMtUXooVT5sCQQDF
BpFJJYbRzqJ8LKx/HmhOBuWXyZkXa5y4xwn2YT2sPnUSC0crSIKS/N3hpMmo0YfC
rc+FDMGFjr1lx3tAUoK5AkEAiEbs3Kn1donT7bT5+WeTGSIIBnWN2s93Gq7AXpV3
K96lVDUnXa4qGwGdhmvlhXpwhYrzMNHkzaU/AcSOi3Cqww==
-----END RSA PRIVATE KEY-----
14 changes: 14 additions & 0 deletions spec/fixtures/ssl/generated/selfsigned.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICGzCCAYSgAwIBAgIJALEY3/cqVrhXMA0GCSqGSIb3DQEBBQUAMBQxEjAQBgNV
BAMTCWxvY2FsaG9zdDAeFw0xMDA3MDkwMTU5MTdaFw0yNjA1MTkxNjMzNTdaMBQx
EjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA
oxTekB4FnMphxgXx4Mg+9v2pgXyh0f1krbcgafVP0P07F3Tb4iW2QwnSw5amSen6
RPNsA/R1qaYw0TG39gW4Wo3gx6dMynzsUVABdIrRaG2M1xsWX2C/iJOFZ50VVKre
pmzxz5lMKusL8D9JwYAfuDgLnsN6ByLtIJ1/i/3ubucCAwEAAaN1MHMwHQYDVR0O
BBYEFMXuuqokuPcMMS7OFu0jeoGKoJsDMEQGA1UdIwQ9MDuAFMXuuqokuPcMMS7O
Fu0jeoGKoJsDoRikFjAUMRIwEAYDVQQDEwlsb2NhbGhvc3SCCQCxGN/3Kla4VzAM
BgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBQUAA4GBAKC8iJBjfAe78/QzKmMk6QJN
kdLolcGGdINaCyGJRG67givjgkLj9N0JDJImeXWebyykrb/RKSNh7jAcBah+qnvS
SkuXG5E2qKvG66rN/4sjhP68pvD10psvKY/pYmZTPu1VHLZXWwNHtKy/F/ktj/7P
HyuNWpYma8yzHT8RMizZ
-----END CERTIFICATE-----
13 changes: 13 additions & 0 deletions spec/fixtures/ssl/generated/server.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-----BEGIN CERTIFICATE-----
MIICCDCCAXGgAwIBAgIBATANBgkqhkiG9w0BAQUFADAuMSwwKgYDVQQDEyNJTlNF
Q1VSRSBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTAeFw0xMDA3MDkwMTU5MTda
Fw0xMDA4MDgwMTU5MTdaMBQxEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG
9w0BAQEFAAOBjQAwgYkCgYEAoxTekB4FnMphxgXx4Mg+9v2pgXyh0f1krbcgafVP
0P07F3Tb4iW2QwnSw5amSen6RPNsA/R1qaYw0TG39gW4Wo3gx6dMynzsUVABdIrR
aG2M1xsWX2C/iJOFZ50VVKrepmzxz5lMKusL8D9JwYAfuDgLnsN6ByLtIJ1/i/3u
bucCAwEAAaNQME4wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxe66qiS49wwxLs4W
7SN6gYqgmwMwHwYDVR0jBBgwFoAUlheRh1DZp+Ya8tE31gY6qsYr8wswDQYJKoZI
hvcNAQEFBQADgYEAPRrgDgJG0YSDNUl57cghBqHvlb4QOZDGgW1XLiG2GYLl1o5Q
jbHij7BZBdk4CBwr1vzExL6Ef7ktvhEEvgXEJQeiOU9Y+v/w3EG914dpJp39Epv6
vBvO9yAyAaozk7JvRGlB8nbb36pYuFnv+KsRNn/KFjauCXm+gknQRSP9vpE=
-----END CERTIFICATE-----
15 changes: 15 additions & 0 deletions spec/fixtures/ssl/generated/server.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXQIBAAKBgQCjFN6QHgWcymHGBfHgyD72/amBfKHR/WSttyBp9U/Q/TsXdNvi
JbZDCdLDlqZJ6fpE82wD9HWppjDRMbf2BbhajeDHp0zKfOxRUAF0itFobYzXGxZf
YL+Ik4VnnRVUqt6mbPHPmUwq6wvwP0nBgB+4OAuew3oHIu0gnX+L/e5u5wIDAQAB
AoGBAJpAsie1De/5CbRJiTjpj40F79/3qAQ83o7lqTYv/7gY3lzYfucQbq5IS2AP
TeiZ9MxlRuUSxHycIo6srWl6jZ0u1M4vfRvmQf2aXZbN4MPcY+XnpUCYGKgKBVLC
tXoD9iHgCpvexbxjJgLIeeWotUoY3xAo8YnX2Luf+Vtah6dRAkEA03hLG+w8lov2
J3AfHQgKjKfRrMU5eC4P9I4LN+nLyHd7i6T+JP5V5NadRkN+xj0zHfo8PxjaPmDv
QQqkhUlV3wJBAMVsGqa6NbtnCvHPc/M0XanBRuGejz2mDJqqaXMCBw8tY318XEMs
gRdlDVWrNg7AcSEuzh48OA7c6lazKLa5N/kCQHhA69VRHZMuvCfpJohHzlf2BtIM
xYWGDCSxscd1+CBjcaoThUJcL1QWhxExyKHKo4rkheYLp+/ZB7Ug7DWvYlkCQQCZ
SO+Ehs5TfJVF3UJ1EjKrLHNhmOA1CKl+qVQIxQlAIoi+FQH58iMlTAPHgZEOcSMl
lZbaaP1JpQOaX677+OHZAkA8vVYbYC+7t0SUqoY0X8z11Iobttpqir9trNq1CK8A
/3To+/BFK2A6Sj7R5u90Vi523FpWq1a74oNAsnB3x7TU
-----END RSA PRIVATE KEY-----
9 changes: 9 additions & 0 deletions spec/fixtures/ssl/openssl-exts.cnf
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[ca]
basicConstraints=critical,CA:true
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer:always

[cert]
basicConstraints=critical,CA:false
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
54 changes: 54 additions & 0 deletions spec/httparty/ssl_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
require File.expand_path(File.join(File.dirname(__FILE__), '..', 'spec_helper'))

describe HTTParty::Request do
context "SSL certificate verification" do
before do
FakeWeb.allow_net_connect = true # enable network connections just for this test
end

after do
FakeWeb.allow_net_connect = false # Restore allow_net_connect value for testing
end

it "should work with when no trusted CA list is specified" do
ssl_verify_test(nil, nil, "selfsigned.crt").should == {'success' => true}
end

it "should work with when no trusted CA list is specified, even with a bogus hostname" do
ssl_verify_test(nil, nil, "bogushost.crt").should == {'success' => true}
end

it "should work when using ssl_ca_file with a self-signed CA" do
ssl_verify_test(:ssl_ca_file, "selfsigned.crt", "selfsigned.crt").should == {'success' => true}
end

it "should work when using ssl_ca_file with a certificate authority" do
ssl_verify_test(:ssl_ca_file, "ca.crt", "server.crt").should == {'success' => true}
end
it "should work when using ssl_ca_path with a certificate authority" do
ssl_verify_test(:ssl_ca_path, ".", "server.crt").should == {'success' => true}
end

it "should fail when using ssl_ca_file and the server uses an unrecognized certificate authority" do
lambda do
ssl_verify_test(:ssl_ca_file, "ca.crt", "selfsigned.crt")
end.should raise_error(OpenSSL::SSL::SSLError)
end
it "should fail when using ssl_ca_path and the server uses an unrecognized certificate authority" do
lambda do
ssl_verify_test(:ssl_ca_path, ".", "selfsigned.crt")
end.should raise_error(OpenSSL::SSL::SSLError)
end

it "should fail when using ssl_ca_file and the server uses a bogus hostname" do
lambda do
ssl_verify_test(:ssl_ca_file, "ca.crt", "bogushost.crt")
end.should raise_error(OpenSSL::SSL::SSLError)
end
it "should fail when using ssl_ca_path and the server uses a bogus hostname" do
lambda do
ssl_verify_test(:ssl_ca_path, ".", "bogushost.crt")
end.should raise_error(OpenSSL::SSL::SSLError)
end
end
end
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ def file_fixture(filename)

Spec::Runner.configure do |config|
config.include HTTParty::StubResponse
config.include HTTParty::SSLTestHelper
config.before(:suite) do
FakeWeb.allow_net_connect = false
end
Expand Down
25 changes: 25 additions & 0 deletions spec/support/ssl_test_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module HTTParty
module SSLTestHelper
def ssl_verify_test(mode, ca_basename, server_cert_filename)
test_server = nil
begin
# Start an HTTPS server
test_server = SSLTestServer.new(
:rsa_key => File.read(File.expand_path("../../fixtures/ssl/generated/server.key", __FILE__)),
:cert => File.read(File.expand_path("../../fixtures/ssl/generated/#{server_cert_filename}", __FILE__)))
test_server.start

# Build a request
if mode
ca_path = File.expand_path("../../fixtures/ssl/generated/#{ca_basename}", __FILE__)
raise ArgumentError.new("#{ca_path} does not exist") unless File.exist?(ca_path)
return HTTParty.get("https://localhost:#{test_server.port}/", :format => :json, :timeout=>30, mode => ca_path)
else
return HTTParty.get("https://localhost:#{test_server.port}/", :format => :json, :timeout=>30)
end
ensure
test_server.stop if test_server
end
end
end
end
69 changes: 69 additions & 0 deletions spec/support/ssl_test_server.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
require 'openssl'
require 'socket'
require 'thread'

# NOTE: This code is garbage. It probably has deadlocks, it might leak
# threads, and otherwise cause problems in a real system. It's really only
# intended for testing HTTParty.
class SSLTestServer
attr_accessor :ctx # SSLContext object
attr_reader :port

def initialize(options={})
@ctx = OpenSSL::SSL::SSLContext.new
@ctx.cert = OpenSSL::X509::Certificate.new(options[:cert])
@ctx.key = OpenSSL::PKey::RSA.new(options[:rsa_key])
@ctx.verify_mode = OpenSSL::SSL::VERIFY_NONE # Don't verify client certificate
@port = options[:port] || 0
@thread = nil
@stopping_mutex = Mutex.new
@stopping = false
end

def start
@raw_server = TCPServer.new(@port)
if @port == 0
@port = Socket::getnameinfo(@raw_server.getsockname, Socket::NI_NUMERICHOST|Socket::NI_NUMERICSERV)[1].to_i
end
@ssl_server = OpenSSL::SSL::SSLServer.new(@raw_server, @ctx)
@stopping_mutex.synchronize{
return if @stopping
@thread = Thread.new{ thread_main }
}
nil
end

def stop
@stopping_mutex.synchronize{
return if @stopping
@stopping = true
}
@thread.join
end

private

def thread_main
until @stopping_mutex.synchronize{ @stopping }
(rr,ww,ee) = select([@ssl_server.to_io], nil, nil, 0.1)
next unless rr && rr.include?(@ssl_server.to_io)
socket = @ssl_server.accept
Thread.new{
header = []
until (line = socket.readline).rstrip.empty?
header << line
end

socket.write <<'EOF'.gsub(/\r\n/n, "\n").gsub(/\n/n, "\r\n")
HTTP/1.1 200 OK
Connection: close
Content-Type: application/json; charset=UTF-8
{"success":true}
EOF
socket.close
}
end
@ssl_server.close
end
end

0 comments on commit f9c7478

Please sign in to comment.