SAML-Toolkits / php-saml

Simple SAML toolkit for PHP
MIT License
1.21k stars 462 forks source link

Issue decrypting signed assertion under PHP 8.1 (7.4 is just fine) #562

Open yphoenix opened 11 months ago

yphoenix commented 11 months ago

Starting with the response......

<ns5:Response
    xmlns:ns5="urn:oasis:names:tc:SAML:2.0:protocol"
    xmlns="http://www.w3.org/2009/xmlenc11#"
    xmlns:ns2="urn:oasis:names:tc:SAML:2.0:assertion"
    xmlns:ns3="http://www.w3.org/2000/09/xmldsig#"
    xmlns:ns4="http://www.w3.org/2001/04/xmlenc#" Destination="https://xxxx" ID="xxx" InResponseTo="ONELOGIN_81aeef9ca3d98b3fa3e505164baff00b3aeeab16" IssueInstant="2023-07-26T23:35:07.423Z" Version="2.0">
    <ns2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">xxx</ns2:Issuer>
    <ds:Signature
        xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
        <ds:SignedInfo>
            <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
            <ds:Reference URI="#_a13a2e99a1a23945f8d58c5df3f781772c47">
                <ds:Transforms>
                    <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
                    <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                </ds:Transforms>
                <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
                <ds:DigestValue>xxxx=</ds:DigestValue>
            </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>xxx==</ds:SignatureValue>
        <ds:KeyInfo
            xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
            <ds:X509Data>
                <ds:X509Certificate>MIIFxxxxx=</ds:X509Certificate>
            </ds:X509Data>
        </ds:KeyInfo>
    </ds:Signature>
    <ns5:Status>
        <ns5:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
    </ns5:Status>
    <ns2:EncryptedAssertion
        xmlns="http://www.w3.org/2009/xmlenc11#"
        xmlns:ns2="urn:oasis:names:tc:SAML:2.0:assertion"
        xmlns:ns3="http://www.w3.org/2000/09/xmldsig#"
        xmlns:ns4="http://www.w3.org/2001/04/xmlenc#"
        xmlns:ns5="urn:oasis:names:tc:SAML:2.0:protocol">
        <xenc:EncryptedData
            xmlns:xenc="http://www.w3.org/2001/04/xmlenc#" Type="http://www.w3.org/2001/04/xmlenc#Element">
            <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc"/>
            <ds:KeyInfo
                xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                <xenc:EncryptedKey
                    xmlns:xenc="http://www.w3.org/2001/04/xmlenc#">
                    <xenc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p"/>
                    <xenc:CipherData>
                        <xenc:CipherValue>xxxxx==</xenc:CipherValue>
                    </xenc:CipherData>
                </xenc:EncryptedKey>
                <ds:X509Data>
                    <ds:X509Certificate>MIIGMxxxxxx</ds:X509Certificate>
                </ds:X509Data>
            </ds:KeyInfo>
            <xenc:CipherData>
                <xenc:CipherValue>xxxxx</xenc:CipherValue>
            </xenc:CipherData>
        </xenc:EncryptedData>
    </ns2:EncryptedAssertion>
</ns5:Response>

It all gets decrypted just fine (8.1 & 7.4), ...

<ns2:Assertion xmlns:ns2="urn:oasis:names:tc:SAML:2.0:assertion" ID="_f1a2269c3902a05d627c7829ab8e3eed50bd" IssueInstant="2023-07-26T23:35:07.423Z" Version="2.0">
        <ns2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">xxx</ns2:Issuer><ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></ds:SignatureMethod><ds:Reference URI="#_f1a2269c3902a05d627c7829ab8e3eed50bd"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>xxx=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>xxx==</ds:SignatureValue><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:X509Data><ds:X509Certificate>MIIF...=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>
        <ns2:Subject>
            <ns2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">1039893981</ns2:NameID>
            <ns2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <ns2:SubjectConfirmationData InResponseTo="ONELOGIN_81aeef9ca3d98b3fa3e505164baff00b3aeeab16" NotOnOrAfter="2023-07-26T23:36:37.407Z" Recipient="xxx"></ns2:SubjectConfirmationData>
            </ns2:SubjectConfirmation>
        </ns2:Subject>
        <ns2:Conditions NotBefore="2023-07-26T23:34:37.407Z" NotOnOrAfter="2023-07-26T23:36:37.407Z">
            <ns2:AudienceRestriction>
                <ns2:Audience>xxx</ns2:Audience>
            </ns2:AudienceRestriction>
        </ns2:Conditions>
        <ns2:AuthnStatement AuthnInstant="2023-07-26T23:34:32.401Z" SessionIndex="Qn7OdKYdWW5JzPPHtWEeGjGmM54=YbXNfA==">
            <ns2:AuthnContext>
                <ns2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</ns2:AuthnContextClassRef>
            </ns2:AuthnContext>
        </ns2:AuthnStatement>
        <ns2:AttributeStatement>
            ...
        </ns2:AttributeStatement>
</ns2:Assertion>

In particular the ds:Signature section..

<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:CanonicalizationMethod><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></ds:SignatureMethod><ds:Reference URI="#_f1a2269c3902a05d627c7829ab8e3eed50bd"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></ds:Transform></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></ds:DigestMethod><ds:DigestValue>xxx=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>xxx==</ds:SignatureValue><ds:KeyInfo xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:X509Data><ds:X509Certificate>MIIF...=</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>

Now all goes well until the decrypted XML is merged in to replace the encrypted XML in the Utils::treeCopyReplace() routine After which it becomes... PHP 7.4

<ns2:Assertion xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="_f1a2269c3902a05d627c7829ab8e3eed50bd" IssueInstant="2023-07-26T23:35:07.423Z" Version="2.0">
        <ns2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">xxx</ns2:Issuer><ds:Signature><ds:SignedInfo><ds:CanonicalizationMethod...

All OK, but PHP 8.1

<ns2:Assertion xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="_f1a2269c3902a05d627c7829ab8e3eed50bd" IssueInstant="2023-07-26T23:35:07.423Z" Version="2.0">
        <ns2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">xxx</ns2:Issuer><ns3:Signature><ns3:SignedInfo><ds:CanonicalizationMethod ...

The <ds:...> blocks become <ns3:...> blocks and of course the signature then fails.... because the data that was signed has been mangled and become something else.

Note: PHP 7.4, perfect, 8.1, not so much.

Thanks

yphoenix commented 11 months ago

Seems to be $targetNode->appendChild($clonedNode)


$sourceNode: <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"><ds:SignedInfo><ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/><ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/><ds:Reference URI="#_f1a2269c3902a05d627c7829ab8e3eed50bd"><ds:Transforms><ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/></ds:Transforms><ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/><ds:DigestValue>xxx=</ds:DigestValue></ds:Reference></ds:SignedInfo><ds:SignatureValue>xxx</ds:X509Certificate></ds:X509Data></ds:KeyInfo></ds:Signature>

$clonedNode => <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#"/>

$targetNode => <ns2:Assertion xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" ID="_f1a2269c3902a05d627c7829ab8e3eed50bd" IssueInstant="2023-07-26T23:35:07.423Z" Version="2.0">
        <ns2:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">xxx</ns2:Issuer></ns2:Assertion>

$targetNode->appendChild($clonedNode)

$resultNode => <ns3:Signature/>

On PHP 7.4 it's <ds:Signature/>
yphoenix commented 11 months ago

Saving and restoring the namespace prefix works around the issue as there doesn't seem to be a way to stop PHP from changing the namespace on you....

    public static function treeCopyReplace(DomNode $targetNode, DomNode $sourceNode, $recurse = false)
    {
        if ($targetNode->parentNode === null) {
            throw new Exception('Illegal argument targetNode. It has no parentNode.');
        }
        $clonedNode = $targetNode->ownerDocument->importNode($sourceNode, false);

        $ns = $clonedNode->prefix;             // SAVE

        if ($recurse) {
            $resultNode = $targetNode->appendChild($clonedNode);
        } else {
            $resultNode = $targetNode->parentNode->insertBefore($clonedNode, $targetNode);
        }

        if ($resultNode->prefix !== $ns)
        {
            $resultNode->prefix = $ns;        // RESTORE
        }
yphoenix commented 11 months ago

For splicing in the decrypted XML does this make sense? Surely it should be as decrypted and not manipulated!

pitbulk commented 11 months ago

@yphoenix thanks for your research, I will investigate and merge your PR if makes sense.

Navi2016 commented 9 months ago

I might be running into the same issue after updating my server from PHP 8.2.7 to 8.2.8.

Signatures are no longer valid and SAML login fails.

It's in a Laravel 10 app using 24slides/laravel-saml2 (2.2.0), which uses onelogin/php-saml (4.1.0).

Everything works great on PHP 8.2.7 and before, no longer on 8.2.8 and up.

As someone mentioned before, there are quite some DOM patches in 8.2.8.

When using the fixed version (php-saml-4.1.0-Issio-Fix-10609) it does not work yet, getting a different error though: DOMDocument::loadXML(): Failed to parse QName ':Issuer' in Entity, line: 6

Navi2016 commented 9 months ago

Some additional info, the exact errors i'm getting are: invalid_response from vendor/onelogin/php-saml/src/Saml2/Auth.php:252

and

Reference validation failed from vendor/robrichards/xmlseclibs/src/XMLSecurityDSig.php:614 (v 3.1.1)

yphoenix commented 9 months ago

Try the fix I proposed above to see if it fixes things for you. I only had issues with encrypted assertions. When it was decrypted the XML tree would get messed up when treeCopyReplace() replaced the encrypted node with the decrypted version.

Navi2016 commented 9 months ago

Like i mentioned i have tried the fixed version (from the merge), but it did not resolve the issue :(

yphoenix commented 9 months ago

In that case probably something different. Took me a good 18 hours of stepping through the code running it both under 7.4 and 8.1 to find the difference and thus my fix to my problem. Looks like your issue might be in the xmlseclibs library.

nielsdos commented 9 months ago

PHP DOM maintainer here. This is caused by a regression introduced by a bugfix in PHP. This is a behaviour change that's caused by an unintended side-effect of using a newer namespace API from libxml2. I have opened a PR to fix this at PHP's end by restoring the original behaviour. I have run these tests https://github.com/SAML-Toolkits/php-saml/actions/runs/6269430086/job/17025850045 to verify if the behaviour is OK now, and it seems to pass the XML related tests.

pitbulk commented 9 months ago

@nielsdos , thanks for the update!

yphoenix commented 9 months ago

Thanks @nielsdos - looking forward to not needing the patch anymore.

yphoenix commented 9 months ago

So this looks like it will be in the next release of PHP 8.x, missed the release that was put out today (2023-09-28)

pitbulk commented 9 months ago

@yphoenix, should we detect at the code affected PHP versions and apply your patch only on those cases? Do you want then to adapt your PR and add test cases?

yphoenix commented 9 months ago

That is an interesting idea, however, first I need to check that it is indeed fixed in 8.1.25 and then decided whether to work on a fix that is needed for PHP 8.1.21-8.1.24 (and appropriate 8.2 / 8.3 versions) or suggest that it is documented that they are broken and leave it at that.

Navi2016 commented 8 months ago

Issue seems to be resolved in PHP 8.2.12!

yphoenix commented 8 months ago

Suspect it is also fixed in 8.1.25 but haven’t tested it yet.

pitbulk commented 6 months ago

@yphoenix, do you thinhk you gonna have time to review the work to be done here?

nielsdos commented 6 months ago

FWIW, this is indeed also fixed in 8.1.25, 8.2.12, 8.3+.