Skip to content

Commit 4902c89

Browse files
committed
Make SAML trust anchors work on verification of the SAML request
SAML uses a concept called trust anchors where you can say, I trust this party because of $reasons. These trust anchors can be the issuing party, the DN of the certificate, etc. The previous code implemented a partial solution to this, it just said that the trust anchor was the DN of the certificate of the signer. This is somewhat weird, as the certificate is already verified by the CA certificate which is user supplied. Now you can submit a CA and you can inject trust anchors into the SOAP binding so it is checked with the verify_xml call. The trust anchors can be one of the following `subject`, `issuer` or `issuer_hash`. Signed-off-by: Wesley Schwengle <waterkip@cpan.org>
1 parent af68b68 commit 4902c89

File tree

4 files changed

+138
-16
lines changed

4 files changed

+138
-16
lines changed

lib/Net/SAML2/Binding/SOAP.pm

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ use Moose;
55

66
use MooseX::Types::URI qw/ Uri /;
77
use Net::SAML2::XML::Util qw/ no_comments /;
8+
use Carp qw(croak);
89

910
with 'Net::SAML2::Role::VerifyXML';
1011

11-
# ABSTRACT: Net::SAML2::Binding::Artifact - SOAP binding for SAML
12+
# ABSTRACT: Net::SAML2::Binding::SOAP - SOAP binding for SAML
1213

1314
=head1 NAME
1415
15-
Net::SAML2::Binding::Artifact - SOAP binding for SAML2
16+
Net::SAML2::Binding::SOAP - SOAP binding for SAML2
1617
1718
=head1 SYNOPSIS
1819
@@ -94,7 +95,18 @@ has 'url' => (isa => Uri, is => 'ro', required => 1, coerce => 1);
9495
has 'key' => (isa => 'Str', is => 'ro', required => 1);
9596
has 'cert' => (isa => 'Str', is => 'ro', required => 1);
9697
has 'idp_cert' => (isa => 'Str', is => 'ro', required => 1);
97-
has 'cacert' => (isa => 'Str', is => 'ro', required => 0);
98+
has 'cacert' => (
99+
is => 'ro',
100+
isa => 'Str',
101+
required => 0,
102+
predicate => 'has_cacert'
103+
);
104+
has 'anchors' => (
105+
is => 'ro',
106+
isa => 'HashRef',
107+
required => 0,
108+
predicate => 'has_anchors'
109+
);
98110

99111
=head2 request( $message )
100112
@@ -149,6 +161,7 @@ sub handle_response {
149161
no_xml_declaration => 1,
150162
cert_text => $self->idp_cert,
151163
cacert => $self->cacert,
164+
anchors => $self->anchors
152165
);
153166
return $saml;
154167

@@ -180,11 +193,15 @@ sub handle_request {
180193

181194
sub _get_saml_from_soap {
182195
my $soap = shift;
183-
my $dom = no_comments($soap);
196+
my $dom = no_comments($soap);
184197
my $parser = XML::LibXML::XPathContext->new($dom);
185198
$parser->registerNs('soap-env', 'http://schemas.xmlsoap.org/soap/envelope/');
186199
$parser->registerNs('samlp', 'urn:oasis:names:tc:SAML:2.0:protocol');
187-
return $parser->findnodes_as_string('/soap-env:Envelope/soap-env:Body/*');
200+
my $set = $parser->findnodes('/soap-env:Envelope/soap-env:Body/*');
201+
if ($set->size) {
202+
return $set->get_node(1)->toString();
203+
}
204+
return;
188205
}
189206

190207
=head2 create_soap_envelope( $message )

lib/Net/SAML2/Role/VerifyXML.pm

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ use Moose::Role;
44

55
use Net::SAML2::XML::Sig;
66
use Crypt::OpenSSL::Verify;
7+
use Crypt::OpenSSL::X509;
8+
use Carp qw(croak);
9+
use List::Util qw(none);
710

811
# ABSTRACT: A role to verify the SAML response XML
912

@@ -41,9 +44,18 @@ use Crypt::OpenSSL::Verify;
4144
# Most of these options are passed to Net::SAML2::XML::Sig, except for the
4245
# cacert
4346
# Most options are optional
44-
cacert => $self->cacert,
4547
cert_text => $self->cert,
4648
no_xml_declaration => 1,
49+
50+
# Used for a trust model, if lacking, everything is trusted
51+
cacert => $self->cacert,
52+
# or check specific certificates based on subject/issuer or issuer hash
53+
anchors => {
54+
# one of the following is allowed
55+
subject => ["subject a", "subject b"],
56+
issuer => ["Issuer A", "Issuer B"],
57+
issuer_hash => ["Issuer A hash", "Issuer B hash"],
58+
},
4759
);
4860
4961
=cut
@@ -54,23 +66,53 @@ sub verify_xml {
5466
my %args = @_;
5567

5668
my $cacert = delete $args{cacert};
69+
my $anchors = delete $args{anchors};
5770

5871
my $x = Net::SAML2::XML::Sig->new({
5972
x509 => 1,
6073
exclusive => 1,
6174
%args,
6275
});
6376

64-
die "XML signature check failed\n" unless $x->verify($xml);
77+
croak("XML signature check failed") unless $x->verify($xml);
6578

66-
return unless $cacert;
79+
if (!$anchors && !$cacert) {
80+
return 1;
81+
}
6782

6883
my $cert = $x->signer_cert
6984
or die "Certificate not provided in SAML Response, cannot validate\n";
7085

71-
my $ca = Crypt::OpenSSL::Verify->new($cacert, { strict_certs => 0 });
72-
return if $ca->verify($cert);
73-
die "Could not verify CA certificate!\n";
86+
if ($cacert) {
87+
my $ca = Crypt::OpenSSL::Verify->new($cacert, { strict_certs => 0 });
88+
eval { $ca->verify($cert) };
89+
if ($@) {
90+
croak("Could not verify CA certificate: $@");
91+
}
92+
}
93+
94+
return 1 if !$anchors;
95+
96+
if (ref $anchors ne 'HASH') {
97+
croak("Unable to verify anchor trust");
98+
}
99+
100+
my ($key) = keys %$anchors;
101+
if (none { $key eq $_ } qw(subject issuer issuer_hash)) {
102+
croak("Unable to verify anchor trust, requires subject, issuer or issuer_hash");
103+
}
104+
105+
my $got = $cert->$key;
106+
my $want = $anchors->{$key};
107+
if (!ref $want) {
108+
$want = [ $want ];
109+
}
110+
111+
if (none { $_ eq $got } @$want) {
112+
croak("Could not verify trust anchors of certificate!");
113+
}
114+
return 1;
115+
74116
}
75117

76118

lib/Net/SAML2/XML/Util.pm

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@ sub no_comments {
4444

4545
# Remove comments from XML to mitigate XML comment auth bypass
4646
my $dom = XML::LibXML->load_xml(
47-
string => $xml,
48-
no_network => 1,
49-
load_ext_dtd => 0,
50-
expand_entities => 0 );
47+
string => $xml,
48+
no_network => 1,
49+
load_ext_dtd => 0,
50+
expand_entities => 0
51+
);
5152

5253
for my $comment_node ($dom->findnodes('//comment()')) {
5354
$comment_node->parentNode->removeChild($comment_node);

t/05-soap-binding.t

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use Test::Lib;
44
use Test::Net::SAML2;
55

66
use Net::SAML2::IdP;
7+
use Net::SAML2::Binding::SOAP;
8+
use Test::Mock::One;
79

810
use LWP::UserAgent;
911

@@ -40,7 +42,7 @@ my $request_xml = $request->as_xml;
4042
my $xp = get_xpath($request_xml);
4143
isa_ok($xp, "XML::LibXML::XPathContext");
4244

43-
my $ua = LWP::UserAgent->new;
45+
my $ua = LWP::UserAgent->new;
4446
my $soap = $sp->soap_binding($ua, $slo_url, $idp_cert);
4547
isa_ok($soap, "Net::SAML2::Binding::SOAP");
4648

@@ -65,4 +67,64 @@ is($soaped_request->session, $request->session,
6567
is($soaped_request->nameid, $request->nameid,
6668
"SOAP nameid equals request nameid");
6769

70+
{
71+
# Testing trust anchors of SAML
72+
# You can set various trust anchors of SAML so the response is checked
73+
# against some kind of anchor.
74+
my %anchors = (
75+
subject => [qw(foo bar)],
76+
issuer => 'Net::SAML2',
77+
issuer_hash => [
78+
'f1d2d2f924e986ac86fdf7b36c94bcdf32beec15',
79+
'e242ed3bffccdf271b7fbaf34ed72d089537b42f'
80+
],
81+
);
82+
83+
my $xml = "<xml></xml>";
84+
my $override = Sub::Override->new(
85+
'Net::SAML2::Binding::SOAP::_get_saml_from_soap' => sub {
86+
return $xml;
87+
},
88+
);
89+
$override->override(
90+
'XML::Sig::new' => sub {
91+
return Test::Mock::One->new(
92+
subject => 'foo',
93+
issuer => 'Net::SAML2',
94+
issuer_hash => 'f1d2d2f924e986ac86fdf7b36c94bcdf32beec15',
95+
);
96+
}
97+
);
98+
99+
foreach (keys %anchors) {
100+
my $soap = Net::SAML2::Binding::SOAP->new(
101+
url => 'https://example.com/auth/saml',
102+
key => $sp->key,
103+
cert => $sp->cert,
104+
idp_cert => $idp_cert,
105+
anchors => { $_ => $anchors{$_} }
106+
);
107+
isa_ok($soap, "Net::SAML2::Binding::SOAP");
108+
109+
is($soap->handle_response('here be soap'),
110+
"<xml></xml>", "We got our XML, so we are verified");
111+
}
112+
113+
my $soap = Net::SAML2::Binding::SOAP->new(
114+
url => 'https://example.com/auth/saml',
115+
key => $sp->key,
116+
cert => $sp->cert,
117+
idp_cert => $idp_cert,
118+
anchors => { subject => 'testsuite failure expected' }
119+
);
120+
121+
throws_ok(
122+
sub {
123+
$soap->handle_response('here be failure');
124+
},
125+
qr/Could not verify trust anchors of certificate!/,
126+
"We cannot trust the anchor"
127+
)
128+
}
129+
68130
done_testing;

0 commit comments

Comments
 (0)