Skip to content

SoapClient feature: Helper function to signed SOAP messages #8035

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

jahyvari
Copy link

@jahyvari jahyvari commented Feb 4, 2022

Usage:

$client = new SoapClient("my.wsdl");

$client->__setWSS(array(                                    // New function to SoapClient: __setWSS
    "digest_method"     => SOAP_WSS_DIGEST_METHOD_SHA256,   // New constants: SOAP_WSS_DIGEST_METHOD_SHA1, SOAP_WSS_DIGEST_METHOD_SHA256 & SOAP_WSS_DIGEST_METHOD_SHA512
    "wss_version"       => SOAP_WSS_VERSION_1_1,            // New constants: SOAP_WSS_VERSION_1_0 & SOAP_WSS_VERSION_1_1
    "add_timestamp"     => true,
    "timestamp_expires" => 300,
    "x509_binsectoken"  => file_get_contents("cert.der"),
    "signfunc"          => function($data) {
        $signature = "";
        openssl_sign(
            $data,
            $signature,
            openssl_pkey_get_private("file://priv.key"),
            OPENSSL_ALGO_SHA1
        );
        return $signature;
    }
));

$client->testFunction(array(
    "Param1" => "foo",
    "Param2" => "bar"
));

Generated SOAP message:

<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:env="http://www.w3.org/2003/05/soap-envelope" xmlns:ns1="http://localhost/" xmlns:wsse11="http://docs.oasis-open.org/wss/oasis-wss-wssecurity-secext-1.1.xsd" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
    <env:Header>
        
        <!-- Generated WSS header -->
        
        <wsse11:Security env:mustUnderstand="true">
            <wsu:Timestamp wsu:Id="TimestampID-1">
                <wsu:Created>2022-01-01T00:00:00Z</wsu:Created>
                <wsu:Expires>2022-01-01T00:05:00Z</wsu:Expires>
            </wsu:Timestamp>
            <wsse11:BinarySecurityToken EncodingType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3" wsu:Id="TokenID-1">...</wsse11:BinarySecurityToken>
            <ds:Signature>
                <ds:SignedInfo>
                    <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                    <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
                    <ds:Reference URI="#TimestampID-1">
                        <ds:Transforms>
                            <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>...</ds:DigestValue>
                    </ds:Reference>
                    <ds:Reference URI="#BodyID-1">
                        <ds:Transforms>
                            <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>...</ds:DigestValue>
                    </ds:Reference>
                </ds:SignedInfo>
                <ds:SignatureValue>...</ds:SignatureValue>
                <ds:KeyInfo>
                    <wsse11:SecurityTokenReference>
                        <wsse11:Reference URI="#TokenID-1" ValueType="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-x509-token-profile-1.0#X509v3"/>
                    </wsse11:SecurityTokenReference>
                </ds:KeyInfo>
            </ds:Signature>
        </wsse11:Security>
        
        <!-- Header end -->
        
    </env:Header>
    <env:Body wsu:Id="BodyID-1">
        <ns1:testFunctionIn>
            <ns1:Param1>foo</ns1:Param1>
            <ns1:Param2>bar</ns1:Param2>
        </ns1:testFunctionIn>
    </env:Body>
</env:Envelope>

WSS specifications:

https://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0.pdf
https://www.oasis-open.org/committees/download.php/16790/wss-v1.1-spec-os-SOAPMessageSecurity.pdf

@cmb69
Copy link
Member

cmb69 commented Feb 7, 2022

Apparently, this PR breaks the Windows build of ext/soap. Can you please have a look?

@jahyvari
Copy link
Author

jahyvari commented Feb 8, 2022

The error seems to be:

error LNK2019: unresolved external symbol __imp_xmlOutputBufferGetContent
error LNK2019: unresolved external symbol __imp_xmlOutputBufferGetSize

I was able to fix this issue with my local Windows build by adding a following to config.w32 file:

} else {
    if (!CHECK_LIB('libxml2.lib', 'soap')) {
        WARNING("soap support can't be enabled, libxml is not found");
    }
}

Unfortunately I'm not that familiar with PHPs Windows build system, so I can't say was that the best possible solution.

@cmb69
Copy link
Member

cmb69 commented Feb 8, 2022

I was able to fix this issue with my local Windows build by adding a following to config.w32 file:

Ah, I see, thanks! I think instead of that, the missing functions should be declared in php_libxml2.def.

@jahyvari
Copy link
Author

jahyvari commented Feb 9, 2022

Thanks! I added missing declarations to php_libxml2.def and removed CHECK_LIB from w32.

@vaclavvanik
Copy link

Hi Jahyvari,

+1 for WSS support. I think __setWSS does two things.

a) it creates SOAP header
b) it injects SOAP header to SOAP message

Imho I would create factory method which creates WSS SOAP header. Returned header could be used in __soapCall.

$client = new SoapClient("my.wsdl");

$wssSoapHeader = $client->__createSoapWssHeader(array(      // New function to SoapClient: reateSoapWssHeader
    "digest_method"     => SOAP_WSS_DIGEST_METHOD_SHA256,   // New constants: SOAP_WSS_DIGEST_METHOD_SHA1, SOAP_WSS_DIGEST_METHOD_SHA256 & SOAP_WSS_DIGEST_METHOD_SHA512
    "wss_version"       => SOAP_WSS_VERSION_1_1,            // New constants: SOAP_WSS_VERSION_1_0 & SOAP_WSS_VERSION_1_1
    "add_timestamp"     => true,
    "timestamp_expires" => 300,
    "x509_binsectoken"  => file_get_contents("cert.der"),
    "signfunc"          => function($data) {
        $signature = "";
        openssl_sign(
            $data,
            $signature,
            openssl_pkey_get_private("file://priv.key"),
            OPENSSL_ALGO_SHA1
        );
        return $signature;
    }
));


$client->__soapCall("SomeFunction", [$a, $b, $c], null, $wssSoapHeader);

For direct soap method call

$client->testFunction(array(
    "Param1" => "foo",
    "Param2" => "bar"
));

which require wss header existing __setSoapHeaders method could be used. If I am not wrong.

@jahyvari
Copy link
Author

jahyvari commented Feb 9, 2022

which require wss header existing __setSoapHeaders method could be used. If I am not wrong.

Hi, thanks for feedback! __setSoapHeaders can be used for static WSS headers (plain username and password). But when there is a need for signed SOAP messages, then the signature varies between SOAP calls because it is calculated from SOAP Body (and timestamp if used). And therefore signature can be injected/calculated only after the SOAP Body has been constructed.

@vaclavvanik
Copy link

Thanks for response and clarification. I am not familiar with WSS.

@DemiMarie
Copy link

If I may make a request: would it be possible for the final output to be canonicalized XML with no comments, to make life a bit easier on consumers?

@jahyvari
Copy link
Author

jahyvari commented Mar 6, 2022

If I may make a request: would it be possible for the final output to be canonicalized XML with no comments, to make life a bit easier on consumers?

The example output above was manually pretty printed and commented for clarity purposes. Actual output format is the same as before = minified XML without comments.

@github-actions
Copy link

github-actions bot commented May 6, 2022

There has not been any recent activity in this PR. It will automatically be closed in 7 days if no further action is taken.

@github-actions github-actions bot added the Stale label May 6, 2022
@jahyvari
Copy link
Author

There has not been any recent activity in this PR. It will automatically be closed in 7 days if no further action is taken.

Hi. Is there anything else what I should do for this PR, or could this go to be Reviewed?

@bukka
Copy link
Member

bukka commented Jul 19, 2022

Apology for this not getting any review. I noticed this issue yesterday when going through 8.2 milestone and needed to look to some other things as well. I had a quick look but unfortunately it's too big to get properly reviewed for PHP 8.2 at this time (feature freeze is end of today).

I will try to look more to the SOAP issues as the extension has been quite neglected. I will do my best to get this at least to 8.3. If you could rebase it in the meantime and get the pipeline green, that would be great.

Modified files

BAD_CAST & bailout

Random id & tests

+1 test

+2 tests

Newline

+1 test

Spaces -> tabs

CRLF -> LF

Added CHECK_LIB

Missing declarations & sprintf -> slprintf (just in case)

Fix test
@jahyvari
Copy link
Author

Apology for this not getting any review. I noticed this issue yesterday when going through 8.2 milestone and needed to look to some other things as well. I had a quick look but unfortunately it's too big to get properly reviewed for PHP 8.2 at this time (feature freeze is end of today).

I will try to look more to the SOAP issues as the extension has been quite neglected. I will do my best to get this at least to 8.3. If you could rebase it in the meantime and get the pipeline green, that would be great.

Hi. Thanks for getting back to this. I rebased the PR.

Comment on lines +63 to +67
case SOAP_WSS_DIGEST_METHOD_SHA1:
algo = zend_string_init("sha1", strlen("sha1"), 0);
ops = php_hash_fetch_ops(algo);
xmlSetProp(reference_dm, BAD_CAST("Algorithm"), BAD_CAST(SOAP_WSS_SHA1));
break;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
case SOAP_WSS_DIGEST_METHOD_SHA1:
algo = zend_string_init("sha1", strlen("sha1"), 0);
ops = php_hash_fetch_ops(algo);
xmlSetProp(reference_dm, BAD_CAST("Algorithm"), BAD_CAST(SOAP_WSS_SHA1));
break;

SHA1 ought to be dead. Let’s not add another use of it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be needed for legacy servers so I would keep it - there's not such a big harm in it.

case SOAP_WSS_DIGEST_METHOD_SHA256:
algo = zend_string_init("sha256", strlen("sha256"), 0);
ops = php_hash_fetch_ops(algo);
xmlSetProp(reference_dm, BAD_CAST("Algorithm"), BAD_CAST(SOAP_WSS_SHA256));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this fail?

case SOAP_WSS_DIGEST_METHOD_SHA512:
algo = zend_string_init("sha512", strlen("sha512"), 0);
ops = php_hash_fetch_ops(algo);
xmlSetProp(reference_dm, BAD_CAST("Algorithm"), BAD_CAST(SOAP_WSS_SHA512));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto


if (ops == NULL) {
if (algo != NULL) {
php_error_docref(NULL, E_WARNING, "invalid hashing algorithm, %s", ZSTR_VAL(algo));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should throw an exception.

php_error_docref(NULL, E_WARNING, "invalid hashing algorithm, %s", ZSTR_VAL(algo));
zend_string_release_ex(algo, 0);
} else {
php_error_docref(NULL, E_WARNING, "unknown hashing algorithm");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto.

ZSTR_VAL(digest)[ops->digest_size] = 0;

zend_string *digest_base64 = php_base64_encode((unsigned char *)ZSTR_VAL(digest), ZSTR_LEN(digest));
xmlNewChild(reference, ns_ds, BAD_CAST("DigestValue"), BAD_CAST(ZSTR_VAL(digest_base64)));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this fail (e.g. with out of memory)?


return 1;
} else {
php_error_docref(NULL, E_WARNING, "exclusive C14N failed, URI \"%s\"", ZSTR_VAL(source_uri));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should throw.

Copy link
Member

@bukka bukka Jul 20, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we want to keep it consistent and the extension mainly uses warnings from the quick look so I would keep it that way. The algorithm above could be exception but this is failure so it might be better to be a warning for consistency.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that unless one sets an error handler, there is no way to know what happened.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SOAP extension seem to use E_ERROR's, E_WARNING's & SoapFault object to signal error events. So maybe some of these new warnings ("invalid hashing algorithm" & "unknown hashing algorithm") could be E_ERROR's instead and this one maybe a SoapFault?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E_ERROR and E_WARNING are bad. No idea about SoapFault.

Comment on lines +137 to +150
ns_wsse = xmlNewNs(envelope, BAD_CAST(SOAP_WSS_SECEXT_1_1), BAD_CAST("wsse11"));
} else {
ns_wsse = xmlNewNs(envelope, BAD_CAST(SOAP_WSS_SECEXT_1_0), BAD_CAST("wsse"));
}

xmlNsPtr ns_ds = xmlNewNs(envelope, BAD_CAST(SOAP_WSS_XMLDSIG), BAD_CAST("ds"));
xmlNsPtr ns_wsu = xmlNewNs(envelope, BAD_CAST(SOAP_WSS_UTILITY), BAD_CAST("wsu"));
xmlNodePtr security = xmlNewChild(head, ns_wsse, BAD_CAST("Security"), NULL);
xmlSetNsProp(body, ns_wsu, BAD_CAST("Id"), BAD_CAST(ZSTR_VAL(body_id)));

if (soap_version == SOAP_1_1) {
xmlSetProp(security, BAD_CAST(SOAP_1_1_ENV_NS_PREFIX":mustUnderstand"), BAD_CAST("1"));
} else {
xmlSetProp(security, BAD_CAST(SOAP_1_2_ENV_NS_PREFIX":mustUnderstand"), BAD_CAST("true"));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can any of the xml* calls fail due to out of memory?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly if the libxml handles everything in memory, but then the same goes for the rest of the SOAP extension where xml* calls are used.

@@ -522,6 +522,14 @@ PHP_MINIT_FUNCTION(soap)
REGISTER_LONG_CONSTANT("SOAP_SSL_METHOD_SSLv3", SOAP_SSL_METHOD_SSLv3, CONST_CS | CONST_PERSISTENT);
REGISTER_LONG_CONSTANT("SOAP_SSL_METHOD_SSLv23", SOAP_SSL_METHOD_SSLv23, CONST_CS | CONST_PERSISTENT);

/* SOAP WSS Constants */
REGISTER_LONG_CONSTANT("SOAP_WSS_DIGEST_METHOD_SHA1", SOAP_WSS_DIGEST_METHOD_SHA1, CONST_CS | CONST_PERSISTENT);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SHA-1 should not be supported.

@vaclavvanik
Copy link

Hello Jahyvari,
as I wrote before I am not familiar with WSS.

I have few suggestions.

Your proposal is add __setWss method with plenty of params.
It is unclear which params are required. Which values should be passed and so on.

What about create a class eg.

class WssSigner
{
   //...
   //...
    public function __construct(
        string $digestMethod,
        string $wssVersion,
        bool $addTimestamp,
        int $expires,
        string x509BinSecToken,
        callable $callback
    ) {
       //...
    }

    // I am not sure about in/out typehints
    public function sign(string $request): string
    {
       //...
    }
}

and then pass the instance into soapClient:

$signer = new WssSigner(
    SOAP_WSS_DIGEST_METHOD_SHA256,
    SOAP_WSS_VERSION_1_1,
    true,
    300,
    file_get_contents("cert.der"),
    function($data) {
        $signature = "";
        openssl_sign(
            $data,
            $signature,
            openssl_pkey_get_private("file://priv.key"),
            OPENSSL_ALGO_SHA1
        );
        return $signature;
    }
);

$client = new SoapClient("my.wsdl");
$client->__setWSS($signer);

If WssSigner was very variable on input params than I would create WssSigner interface with concrete implementations.

I think my proposal has much better type hinting and as bonus it could be used standalone.

@jahyvari
Copy link
Author

jahyvari commented Dec 7, 2022

What about create a class eg.

Yep, I agree. That could be better for the type hinting. I think that I based the current function signature to SoapClient's constructor which takes the options as an array.

@adoy adoy removed this from the PHP 8.2 milestone Feb 16, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants