Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions lib/duo_api/api_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
#
class DuoApi
attr_accessor :ca_file
attr_reader :default_params
attr_reader :default_params, :ca_pinning_disabled

VERSION = Gem.loaded_specs['duo_api'] ? Gem.loaded_specs['duo_api'].version : '0.0.0'

Expand All @@ -22,7 +22,7 @@ class DuoApi
INITIAL_BACKOFF_WAIT_SECS = 1
BACKOFF_FACTOR = 2

def initialize(ikey, skey, host, proxy = nil, ca_file: nil, default_params: {})
def initialize(ikey, skey, host, proxy = nil, ca_file: nil, disable_ca_pinning: false, default_params: {})
@ikey = ikey
@skey = skey
@host = host
Expand All @@ -38,8 +38,15 @@ def initialize(ikey, skey, host, proxy = nil, ca_file: nil, default_params: {})
proxy_uri.password
]
end
@ca_file = ca_file ||
File.join(File.dirname(__FILE__), '..', '..', 'ca_certs.pem')

raise ArgumentError, 'Cannot both disable CA pinning and provide a custom CA file' if disable_ca_pinning && ca_file

@ca_pinning_disabled = disable_ca_pinning
@ca_file = if disable_ca_pinning
nil
else
ca_file || File.join(File.dirname(__FILE__), '..', '..', 'ca_certs.pem')
end
@default_params = default_params.transform_keys(&:to_sym)
end

Expand Down Expand Up @@ -80,11 +87,9 @@ def request(method, path, params = {}, additional_headers = nil)
end

# Start the HTTP session
Net::HTTP.start(
uri.host, uri.port, *@proxy,
use_ssl: true, ca_file: @ca_file,
verify_mode: OpenSSL::SSL::VERIFY_PEER
) do |http|
http_opts = { use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_PEER }
http_opts[:ca_file] = @ca_file if @ca_file
Net::HTTP.start(uri.host, uri.port, *@proxy, **http_opts) do |http|
wait_secs = INITIAL_BACKOFF_WAIT_SECS
loop do
resp = http.request(request)
Expand Down
67 changes: 67 additions & 0 deletions test/test_disable_ca_pinning.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# frozen_string_literal: true

require_relative 'common'

class TestDisableCaPinning < Test::Unit::TestCase
def test_default_has_ca_pinning_enabled
client = DuoApi.new(IKEY, SKEY, HOST)
assert_equal(false, client.ca_pinning_disabled)
assert_not_nil(client.ca_file)
assert(File.exist?(client.ca_file))
end

def test_disable_ca_pinning_removes_ca_file
client = DuoApi.new(IKEY, SKEY, HOST, nil, disable_ca_pinning: true)
assert_equal(true, client.ca_pinning_disabled)
assert_nil(client.ca_file)
end

def test_disable_ca_pinning_and_custom_ca_file_raises
assert_raise(ArgumentError) do
DuoApi.new(IKEY, SKEY, HOST, nil, ca_file: '/some/cert.pem', disable_ca_pinning: true)
end
end

def test_disable_ca_pinning_and_custom_ca_file_error_message
error = assert_raise(ArgumentError) do
DuoApi.new(IKEY, SKEY, HOST, nil, ca_file: '/some/cert.pem', disable_ca_pinning: true)
end
assert_equal('Cannot both disable CA pinning and provide a custom CA file', error.message)
end

def test_custom_ca_file_without_disable_works
client = DuoApi.new(IKEY, SKEY, HOST, nil, ca_file: '/some/cert.pem')
assert_equal(false, client.ca_pinning_disabled)
assert_equal('/some/cert.pem', client.ca_file)
end
end

class TestDisableCaPinningHTTP < BaseTestCase
def setup
@client_pinned = DuoApi.new(IKEY, SKEY, HOST)
@client_unpinned = DuoApi.new(IKEY, SKEY, HOST, nil, disable_ca_pinning: true)
@mock_http = mock
@ok_resp = Net::HTTPSuccess.new('200')
end

def test_pinned_client_passes_ca_file_to_http
ca_file = @client_pinned.ca_file
Net::HTTP.expects(:start).with do |_host, _port, **opts|
opts[:use_ssl] == true &&
opts[:verify_mode] == OpenSSL::SSL::VERIFY_PEER &&
opts[:ca_file] == ca_file
end.yields(@mock_http)
@mock_http.expects(:request).returns(@ok_resp)
@client_pinned.request('GET', '/foo/bar')
end

def test_unpinned_client_does_not_pass_ca_file_to_http
Net::HTTP.expects(:start).with do |_host, _port, **opts|
opts[:use_ssl] == true &&
opts[:verify_mode] == OpenSSL::SSL::VERIFY_PEER &&
!opts.key?(:ca_file)
end.yields(@mock_http)
@mock_http.expects(:request).returns(@ok_resp)
@client_unpinned.request('GET', '/foo/bar')
end
end
Loading