SAML-Toolkits/php-saml

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

yphoenix opened this issue · 19 comments

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

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/>

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
        }

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

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

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

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)

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.

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

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.

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.

@nielsdos , thanks for the update!

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

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)

@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?

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.

Issue seems to be resolved in PHP 8.2.12!

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

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

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