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 )