Net-SAML2
view release on metacpan or search on metacpan
lib/Net/SAML2/Protocol/Assertion.pm view on Meta::CPAN
return $xpath->findvalue('//saml:AuthnStatement/@SessionIndex') unless $assertion_node;
return $xpath->findvalue('saml:AuthnStatement/@SessionIndex', $assertion_node);
}
sub _get_in_response_to {
my $class = shift;
my $xpath = shift;
my $assertion_node = shift;
return $xpath->findvalue(
'//saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData/@InResponseTo'
) unless $assertion_node;
return $xpath->findvalue(
'saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData/@InResponseTo',
$assertion_node);
}
sub _get_trusted_assertion {
my $class = shift;
my $xpath = shift;
my ($candidate_refs) = @_;
my $assertion_node;
for my $sign_id_ref (@$candidate_refs) {
next unless (defined $sign_id_ref && XsdID->check($sign_id_ref));
my $candidates = $xpath->findnodes("//*[\@ID='$sign_id_ref']");
croak("XSW guard: signed Reference URI '$sign_id_ref' is ambiguous "
. "(matched " . $candidates->size . " elements)")
if $candidates->size > 1;
my $root = $candidates->get_node(1);
next unless $root;
my $ln = $root->localname // '';
my $ns = $root->namespaceURI // '';
if ($ln eq 'Assertion'
&& $ns eq 'urn:oasis:names:tc:SAML:2.0:assertion') {
$assertion_node = $root;
last;
}
my $asns = $xpath->findnodes('.//saml:Assertion', $root);
if ($asns->size) {
$assertion_node = $asns->get_node(1);
last;
}
}
return $assertion_node;
}
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 @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 $ref = $xpath->findvalue(
'./dsig:SignedInfo/dsig:Reference/@URI', $sig);
next unless defined $ref;
$ref =~ s/^#//;
next unless XsdID->check($ref);
my $resolved = $xpath->findnodes("//*[\@ID='$ref']");
# A CA-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)")
if $resolved->size > 1;
next unless $resolved->size == 1;
my $node = $resolved->get_node(1);
my $genuine = try {
Net::SAML2::XML::Sig->new({
cert_text => $pem,
no_xml_declaration => 1,
})->verify($node->toString);
};
next unless $genuine;
push @trusted_refs, $ref;
}
return @trusted_refs;
}
sub _verify_encrypted_assertion {
my $self = shift;
my $xml = shift;
my $cacert = shift;
my $key_file = shift;
my $key_name = shift;
my $insecure_trust_embedded_cert = shift;
my $cert_text = shift;
my $require_signed_assertion = shift;
unless ($cacert || $cert_text || $insecure_trust_embedded_cert) {
croak(
"'cacert' or 'cert_text' is required to verify assertion signatures. "
. "Without a trusted certificate the verifier accepts any "
. "KeyInfo-embedded certificate. To explicitly disable this check "
. "(test/dev only), pass insecure_trust_embedded_cert => 1 to "
. "new_from_xml()."
);
}
my $xpath = XML::LibXML::XPathContext->new($xml);
$xpath->registerNs('saml', 'urn:oasis:names:tc:SAML:2.0:assertion');
$xpath->registerNs('samlp', 'urn:oasis:names:tc:SAML:2.0:protocol');
$xpath->registerNs('dsig', 'http://www.w3.org/2000/09/xmldsig#');
$xpath->registerNs('xenc', 'http://www.w3.org/2001/04/xmlenc#');
return $xml unless $xpath->exists('//saml:EncryptedAssertion');
croak "Encrypted Assertions require key_file" if !defined $key_file;
$xml = $self->_decrypt(
$xml,
key_file => $key_file,
key_name => $key_name,
);
$xpath->setContextNode($xml);
my $assert_nodes = $xpath->findnodes('//saml:Assertion');
return $xml unless $assert_nodes->size;
my $assert = $assert_nodes->get_node(1);
unless ($xpath->exists('dsig:Signature', $assert)) {
return $xml unless $require_signed_assertion;
croak(
"Decrypted assertion has no signature. Set require_signed_assertion => 0 "
. "to accept unsigned encrypted assertions (not recommended)."
);
}
$self->verify_xml(
$assert->toString(),
no_xml_declaration => 1,
$cert_text ? (cert_text => $cert_text) : (),
$cacert ? (cacert => $cacert) : (),
);
return $xml;
}
sub new_from_xml {
my($class, %args) = @_;
my $key_file = $args{key_file};
my $cacert = delete $args{cacert};
my $cert_text = delete $args{cert_text};
my $issuer = delete $args{issuer};
my $destination = delete $args{destination};
my $insecure_trust_embedded_cert = delete $args{insecure_trust_embedded_cert} // 0;
my $require_signed_assertion = delete $args{require_signed_assertion} // 0;
my $xpath = XML::LibXML::XPathContext->new();
$xpath->registerNs('saml', 'urn:oasis:names:tc:SAML:2.0:assertion');
$xpath->registerNs('samlp', 'urn:oasis:names:tc:SAML:2.0:protocol');
$xpath->registerNs('dsig', 'http://www.w3.org/2000/09/xmldsig#');
$xpath->registerNs('xenc', 'http://www.w3.org/2001/04/xmlenc#');
my $xml = no_comments($args{xml});
$xpath->setContextNode($xml);
my $actual_destination = $class->_get_actual_destination($destination, $xpath);
if ($cacert && $xpath->findnodes('//dsig:Signature')->size > 0) {
my $verifier = Net::SAML2::XML::Sig->new({
x509 => 1,
no_xml_declaration => 1,
});
my $ok = try {
$verifier->verify($xml->toString)
} catch {
croak(sprintf(
"XML signature verification failed in new_from_xml%s",
$_ ? " ($_)" : '',
));
};
# XML::Sig can croak or return 0 in event that the signature fails
croak(sprintf(
"XML signature verification failed in new_from_xml%s",
$_ ? " ($_)" : '',
)) unless $ok;
}
$xml = $class->_verify_encrypted_assertion(
$xml,
$cacert,
$key_file,
$args{key_name},
$insecure_trust_embedded_cert,
$cert_text,
$require_signed_assertion,
);
my $dec = $class->_decrypt(
$xml,
key_file => $key_file,
key_name => $args{key_name}
);
$xpath->setContextNode($dec);
my @trusted_refs = $class->_trusted_signature_refs($xpath, $cacert);
my $sig_count = $xpath->findnodes('//dsig:Signature')->size;
if ($cacert && $sig_count > 0 && !@trusted_refs) {
croak(
"No <dsig:Signature> in the document chains to the configured "
. "cacert. Refusing to extract assertion content."
);
}
my @candidate_refs;
if (@trusted_refs) {
@candidate_refs = @trusted_refs;
}
else {
croak("No trusted signature found in the assertion. Pass "
. "insecure_trust_embedded_cert => 1 to new_from_xml() to trust "
. "embedded certificates (dev/test only).")
unless $insecure_trust_embedded_cert;
my $ids = $xpath->findnodes('//saml:Assertion/@ID');
if ($ids->size == 1) {
(my $ref = $ids->get_node(1)->value) =~ s/^#//;
@candidate_refs = ($ref) if length $ref;
}
}
my $assertion_node = $class->_get_trusted_assertion($xpath, \@candidate_refs);
if ($cacert && $sig_count > 0 && !$assertion_node) {
croak(
"XSW guard: no CA-trusted signature anchors a <saml:Assertion>. "
. "Refusing to extract assertion content via document order."
);
}
if (defined $destination && $assertion_node) {
my $recipient = $xpath->findvalue(
'saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData/@Recipient',
$assertion_node,
);
if (($recipient // '') ne $destination) {
croak(sprintf(
"Assertion SubjectConfirmationData/Recipient (%s) does "
. "not match expected destination (%s)",
$recipient, $destination,
));
}
}
die "Net::SAML2: no Assertion found in response\n"
unless $assertion_node || $xpath->exists('//saml:Assertion');
my $attributes = {};
my @attr_owners = $assertion_node
? $xpath->findnodes(
'./saml:AttributeStatement/saml:Attribute/saml:AttributeValue/..',
$assertion_node)
: $xpath->findnodes(
'//saml:Assertion/saml:AttributeStatement/saml:Attribute/saml:AttributeValue/..');
for my $node (@attr_owners) {
my @values = $xpath->findnodes("saml:AttributeValue", $node);
$attributes->{$node->getAttribute('Name')} = [map $_->string_value, @values];
}
my $not_before = $class->_get_not_before($xpath, $assertion_node);
( run in 1.252 second using v1.01-cache-2.11-cpan-5fbc6bb55f2 )