Skip to content
Closed
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
28 changes: 23 additions & 5 deletions lib/Net/SAML2/Object/Response.pm
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ validating the certificate provided for a Response.
It is required for ensuring that the Response is properly
validated.

=head2 cert_text

text form (FORMAT_PEM) of the IdP signing certificate. Unlike C<cacert>,
this B<pins> a specific certificate rather than verifying a CA chain: an
embedded signature is trusted only when its certificate is byte-for-byte
this one. Propagated to C<to_assertion> the same way C<cacert> is -- see
L<Net::SAML2::Protocol::Assertion/new_from_xml> for how it's used there.

=head2 insecure_trust_embedded_cert

Boolean, default false. When true, C<to_assertion> proceeds with
Expand Down Expand Up @@ -116,6 +124,11 @@ has 'cacert' => (
is => 'ro',
required => 0);

has 'cert_text' => (
isa => 'Str',
is => 'ro',
required => 0);

has 'insecure_trust_embedded_cert' => (
isa => 'Bool',
is => 'ro',
Expand All @@ -136,10 +149,11 @@ around BUILDARGS => sub {

my %params = @_;
unless ($params{cacert}
|| $params{cert_text}
|| $params{insecure_trust_embedded_cert}) {
croak(
"Net::SAML2::Object::Response->new() requires 'cacert' "
. "on the object to verify SAML response signatures. "
"Net::SAML2::Object::Response->new() requires 'cacert' or "
. "'cert_text' on the object to verify SAML response signatures. "
. "To explicitly disable signature verification (test/dev only) "
. ", pass insecure_trust_embedded_cert => 1 to new()."
);
Expand All @@ -163,6 +177,7 @@ sub new_from_xml {
my $xml = no_comments($args{xml});
my $destination = delete $args{destination};
my $cacert = delete $args{cacert};
my $cert_text = delete $args{cert_text};
my $insecure_trust_embedded_cert = delete $args{insecure_trust_embedded_cert};

# The default may change in the future
Expand Down Expand Up @@ -231,6 +246,7 @@ sub new_from_xml {
in_response_to => $response->getAttribute('InResponseTo'),
$nodes->size ? (assertions => $nodes) : (),
$cacert ? (cacert => $cacert) : (),
$cert_text ? (cert_text => $cert_text) : (),
$insecure_trust_embedded_cert ? (insecure_trust_embedded_cert => $insecure_trust_embedded_cert) : (),
require_signed_response => $require_signed_response,
);
Expand Down Expand Up @@ -264,11 +280,13 @@ sub to_assertion {

# Propagate the trust configuration from the Response so the Assertion
# inherits the same trust anchor. Without this the caller would have to
# supply cacert (or insecure_trust_embedded_cert) a second time, and a
# Response built with a trust anchor could silently produce an Assertion
# built with none. Caller-supplied %args still override these defaults.
# supply cacert/cert_text (or insecure_trust_embedded_cert) a second
# time, and a Response built with a trust anchor could silently produce
# an Assertion built with none. Caller-supplied %args still override
# these defaults.
return Net::SAML2::Protocol::Assertion->new_from_xml(
$self->cacert ? (cacert => $self->cacert) : (),
$self->cert_text ? (cert_text => $self->cert_text) : (),
$self->insecure_trust_embedded_cert
? (insecure_trust_embedded_cert => $self->insecure_trust_embedded_cert)
: (),
Expand Down
98 changes: 65 additions & 33 deletions lib/Net/SAML2/Protocol/Assertion.pm
Original file line number Diff line number Diff line change
Expand Up @@ -95,24 +95,33 @@ used by the IdP to Encrypt the response (or parts of the response)

=item B<cacert>

path to the CA certificate for verification. Optional: This is only used for
validating the certificate provided for a signed Assertion that was found
when the EncryptedAssertion is decrypted.

While optional it is recommended for ensuring that the Assertion in an
EncryptedAssertion is properly validated.

C<cacert> verifies the signature against the certificate embedded in the
document's C<KeyInfo>. When the IdP references its signing key by
C<KeyName> or C<RetrievalMethod> (no embedded C<X509Certificate>), use
path to the CA certificate for verification. Used both for validating the
certificate embedded in a signed (plain or decrypted-from-Encrypted)
C<saml:Assertion>, and the certificate provided for a signed Assertion found
when an C<EncryptedAssertion> is decrypted.

While optional it is recommended for ensuring that the Assertion is
properly validated.

C<cacert> verifies that the certificate embedded in the document's
C<KeyInfo> chains, via a real X.509 CA-verification, to the given CA
certificate. This requires the IdP's signing certificate to actually be
issued by (or be) that CA. It will not succeed merely because you trust
the IdP's signing certificate directly, unless that certificate happens to
be self-signed. For an IdP that references its signing key by C<KeyName> or
C<RetrievalMethod> (no embedded C<X509Certificate>), or for pinning a
CA-issued certificate directly rather than verifying a chain, use
C<cert_text> instead.

=item B<cert_text>

text form of the IdP signing certificate (FORMAT_PEM) used to verify the
assertion signature. Unlike C<cacert>, this B<pins> a specific
certificate: the signature is verified directly against it. One of
C<cacert>, C<cert_text> or C<insecure_trust_embedded_cert> is required.
assertion signature, both for a plain (or decrypted) C<saml:Assertion> and
for a signed Assertion found when an C<EncryptedAssertion> is decrypted.
Unlike C<cacert>, this B<pins> a specific certificate: the signature is
trusted only when the certificate embedded in the document is byte-for-byte
this certificate, regardless of what issued it. One of C<cacert>,
C<cert_text> or C<insecure_trust_embedded_cert> is required.

=item B<require_signed_assertion>

Expand Down Expand Up @@ -340,25 +349,47 @@ sub _get_trusted_assertion {
}

sub _trusted_signature_refs {
my ($class, $xpath, $cacert) = @_;

return unless $cacert;

my $ca = Crypt::OpenSSL::Verify->new($cacert, { strict_certs => 0 });

# We are looking for references for trusted Signature nodes here
# the X509Certificate of each signature is verified against the
# cacert and a list of trusted references is created
my ($class, $xpath, $cacert, $cert_text) = @_;

return unless $cacert || $cert_text;

my $ca = $cacert
? Crypt::OpenSSL::Verify->new($cacert, { strict_certs => 0 })
: undef;

# cert_text pins one specific certificate: rather than asking whether
# the embedded cert chains to a CA, we ask whether it *is* (byte for
# byte) the certificate the caller told us to trust. Comparing DER
# bytes sidesteps any PEM formatting/whitespace differences between
# what's embedded in the document and what the caller supplied.
my $pinned_cert_obj = $cert_text
? try { Crypt::OpenSSL::X509->new_from_string($cert_text) }
: undef;

# We are looking for references for trusted Signature nodes here.
# The X509Certificate of each signature is checked against the cacert
# (CA-chain trust) and/or cert_text (exact pin), and a list of trusted
# references is created from whichever ones pass.
my @trusted_refs;
for my $sig ($xpath->findnodes('//dsig:Signature')) {
my $pem = $class->get_pem_from_keynode($sig);
my $cert_obj = try { Crypt::OpenSSL::X509->new_from_string($pem) };
next unless $cert_obj;

# Crypt::OpenSSL::Verify->verify can both return a bool AND die on
# parse / chain failure; treat both as untrusted.
my $ok = try { $ca->verify($cert_obj) };
next unless $ok;
my $trusted = 0;

if ($ca) {
# Crypt::OpenSSL::Verify->verify can both return a bool AND die
# on parse / chain failure; treat both as untrusted.
$trusted ||= !!(try { $ca->verify($cert_obj) });
}

if (!$trusted && $pinned_cert_obj) {
$trusted ||= ($cert_obj->fingerprint_sha256
eq $pinned_cert_obj->fingerprint_sha256);
}

next unless $trusted;

my $ref = $xpath->findvalue(
'./dsig:SignedInfo/dsig:Reference/@URI', $sig);
Expand All @@ -369,7 +400,7 @@ sub _trusted_signature_refs {

my $resolved = $xpath->findnodes("//*[\@ID='$ref']");

# A CA-trusted signature whose Reference URI resolves to more than
# A trusted signature whose Reference URI resolves to more than
# one element is an active XSW1 (duplicate-ID) attack - fail closed.
die("XSW guard: trusted signature Reference URI '$ref' is "
. "ambiguous (matched " . $resolved->size . " elements)")
Expand Down Expand Up @@ -472,7 +503,7 @@ sub new_from_xml {
$xpath->setContextNode($xml);

my $actual_destination = $class->_get_actual_destination($destination, $xpath);
if ($cacert && $xpath->findnodes('//dsig:Signature')->size > 0) {
if (($cacert || $cert_text) && $xpath->findnodes('//dsig:Signature')->size > 0) {
my $verifier = Net::SAML2::XML::Sig->new({
x509 => 1,
no_xml_declaration => 1,
Expand Down Expand Up @@ -510,12 +541,13 @@ sub new_from_xml {
);
$xpath->setContextNode($dec);

my @trusted_refs = $class->_trusted_signature_refs($xpath, $cacert);
my @trusted_refs = $class->_trusted_signature_refs($xpath, $cacert, $cert_text);
my $sig_count = $xpath->findnodes('//dsig:Signature')->size;
if ($cacert && $sig_count > 0 && !@trusted_refs) {
if (($cacert || $cert_text) && $sig_count > 0 && !@trusted_refs) {
croak(
"No <dsig:Signature> in the document chains to the configured "
. "cacert. Refusing to extract assertion content."
. "cacert, or matches the configured cert_text. Refusing to "
. "extract assertion content."
);
}

Expand All @@ -538,9 +570,9 @@ sub new_from_xml {

my $assertion_node = $class->_get_trusted_assertion($xpath, \@candidate_refs);

if ($cacert && $sig_count > 0 && !$assertion_node) {
if (($cacert || $cert_text) && $sig_count > 0 && !$assertion_node) {
croak(
"XSW guard: no CA-trusted signature anchors a <saml:Assertion>. "
"XSW guard: no trusted signature anchors a <saml:Assertion>. "
. "Refusing to extract assertion content via document order."
);
}
Expand Down
Loading