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
34 changes: 24 additions & 10 deletions lib/Duo/API.pm
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ Duo::API objects have the following methods:
Returns a handle to sign and send requests. These parameters are
obtained when creating an API integration.

An optional C<$enable_ca_pinning> parameter (6th argument, default: true)
controls whether connections are validated against the pinned CA bundle.
When set to false, the TLS library validates using the OS trust store
instead. TLS verification is always enforced regardless of this setting.

=item json_api_call($method, $path, \%params)

Make a request to an API endpoint with the given HTTPS method and
Expand Down Expand Up @@ -111,6 +116,7 @@ use Digest::SHA qw(hmac_sha512_hex);
use File::Basename qw( dirname );
use File::Spec::Functions qw(catfile);
use JSON qw(decode_json encode_json);
use IO::Socket::SSL qw(SSL_VERIFY_PEER);
use LWP::UserAgent;
use MIME::Base64 qw(encode_base64);
use POSIX qw(strftime);
Expand All @@ -126,16 +132,18 @@ use constant RATE_LIMIT_HTTP_CODE => 429;
use constant DEFAULT_CA_CERTS => catfile(dirname(abs_path((__FILE__))) , 'ca_certs.pem');

sub new {
my($proto, $ikey, $skey, $host, $paging_limit, $ca_certs) = @_;
my($proto, $ikey, $skey, $host, $paging_limit, $ca_certs, $enable_ca_pinning) = @_;
my $class = ref($proto) || $proto;
$paging_limit ||= 100;
$ca_certs ||= DEFAULT_CA_CERTS;
$enable_ca_pinning = 1 unless defined($enable_ca_pinning);
my $self = {
ikey => $ikey,
skey => $skey,
host => $host,
paging_limit => $paging_limit,
ca_certs => $ca_certs,
ikey => $ikey,
skey => $skey,
host => $host,
paging_limit => $paging_limit,
ca_certs => $ca_certs,
enable_ca_pinning => $enable_ca_pinning,
};
bless($self, $class);
return $self;
Expand Down Expand Up @@ -222,11 +230,17 @@ sub api_call {
sub make_request {
my ($self, $req) = @_;

my %ssl_opts = (
verify_hostname => 1,
SSL_verify_mode => SSL_VERIFY_PEER,
);

if ($self->{enable_ca_pinning}) {
$ssl_opts{SSL_ca_file} = $self->{ca_certs};
}

my $ua = LWP::UserAgent->new(
ssl_opts => {
SSL_ca_file => $self->{ca_certs},
verify_hostname => 1,
}
ssl_opts => \%ssl_opts,
);

if ($ENV{'DEBUG'}) {
Expand Down
47 changes: 47 additions & 0 deletions t/api_calls.t
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use URI;
use URI::QueryParam;

use Duo::API;
use IO::Socket::SSL qw(SSL_VERIFY_PEER);
use LWP::UserAgent;
use Time::HiRes;

Expand Down Expand Up @@ -410,6 +411,52 @@ describe "A duo api client" => sub {
]);
};
};

describe "CA pinning" => sub {
my $req = mock();
my %captured_ssl_opts;

$req->stubs(
as_string => "I am a request",
);

before each => sub {
%captured_ssl_opts = ();

LWP::UserAgent->stubs(new => sub {
my ($class, %args) = @_;
%captured_ssl_opts = %{$args{ssl_opts}};
my $ua = mock();
$ua->stubs(request => sub { return $mock_response; });
return $ua;
});

$mock_response->stubs(code => 200);
};

it "CA pinning is enabled by default" => sub {
my $client = Duo::API->new('ikey', 'skey', 'host');
is($client->{enable_ca_pinning}, 1);
$client->make_request($req);
is($captured_ssl_opts{SSL_ca_file}, Duo::API::DEFAULT_CA_CERTS);
is($captured_ssl_opts{SSL_verify_mode}, SSL_VERIFY_PEER);
is($captured_ssl_opts{verify_hostname}, 1);
};

it "CA pinning can be disabled via constructor parameter" => sub {
my $client = Duo::API->new('ikey', 'skey', 'host', undef, undef, 0);
is($client->{enable_ca_pinning}, 0);
$client->make_request($req);
ok(!exists $captured_ssl_opts{SSL_ca_file});
};

it "TLS verification is still enforced when CA pinning is disabled" => sub {
my $client = Duo::API->new('ikey', 'skey', 'host', undef, undef, 0);
$client->make_request($req);
is($captured_ssl_opts{SSL_verify_mode}, SSL_VERIFY_PEER);
is($captured_ssl_opts{verify_hostname}, 1);
};
};
};

describe "test" => sub {
Expand Down
Loading