PHP openssl_sign() - different result from SoapUi - php

I have to send a soap request, which part should be signed using certificate and algorithm = http://www.w3.org/2000/09/xmldsig#rsa-sha1. I have to generate it in PHP.
I use SoapUi application to verify, if my final outgoing request is correct. After configuring SoapUi (with the same certificate) I have a request, which is accepted by external (receiver's) system. So I can view in SoapUi, how the final outgoing xml looks like.
Because there is no fine-working implementation in PHP, which would allow to successfully sign a part of outgoing message, I prepared my own piece of code. I have extended php SoapClient and now I have almost the same outgoing xml, like in SoapUi. Comparing to xml generated by SoapUi my message is different only in one field: "SignatureValue". The rest of xml is the same.
The "SignatureValue" field contains base64_encode($signature). $signature is calculate by the following php code:
openssl_sign($signedInfo_canonized, $signature, $private_key_id, 'sha1WithRSAEncryption');
where:
$signedInfo_canonized is the result of c14N() function on "SignedInfo" xml tag. This tag is the same as generated by SoapUi. It contains the same (proper) DigestValue.
$signature is the signature result.
$private_key_id is =
$private_key_id = openssl_pkey_get_private('file://'.constant('PRIVATE_KEY_PATH'), constant('PRIVATE_KEY_PASSWORD'));
Additionally, the signature verification in php returns true:
openssl_verify($signedInfo_canonized, $signature, $public_key_id);
So everything works fine and there is no error, but my "SignatureValue" tag, created as above, contains "HyaVsqoML0 ... " base64 string, but should contain "YsSMvMYfm75 ... ".
I tried a few different canonization methods of "SignedInfo" node and each time received different signature, but each time different then in SoapUi.
I also cannot debug SoapUi to look inside and check, how the individual parts of above process looke like in Java.
I found a few articles and questions about the same problem (for example this one: Problems with signature --- openssl_sign), they look similar, but none of solutions worked for me.
Any ideas to help? ;-)

Related

What's the right way to handle chunked json response with swagger and autogenerated php client?

I'm a bit lost between definitions, generated code and many things that are a bit of a black box, even after debugging.
But let's start at the beginning. I have an API written with node using NestJS as framework. NestJS automatically creates the swagger/openapi json file. Using swagger-codegen I create a PHP class to access the API from another server. Works like a charm for simple API request.
Now the problem are API request returning a bigger response, i.e. >1000 rows from one or more DBs. Do make the client not wait a long time and create a big JSON response on the server I've switched to NDJSON, which splits the reponse in chunks of smaller JSON parts, each on it's own line. This also works when I create my request by hand with curl or a HTTP wrapper in PHP using fopen and fread. The response type is application/x-ndjson.
But the code generated by swagger-codegen is always waiting until the whole response has been received. It's even worse, because it fails to decode NDJSON with json_decode() and just returns null. Underneath Guzzle is used, which uses PSR7 streams for the response.
Now I could just skip the autogenerated code for the NDJSON endpoints. But I'd prefer not to add special handling and lose all the useful generated checks.
So is it somehow possible to make swagger-codegen give access to the stream of the response? Am I missing a parameter to codegen or something in the swagger JSON? It does have a produce with application/x-ndjson.
Do answer my own question it is possible, but not easily - meaning there is no option or parameter.
First create a class that extends the autogenerated API class. There you have access to all of the protected methods. We assume the endpoint or api method is called testMethodGet. The only thing that can be reused is the request method, i.e. testMethodGetRequest, but it does all of the client side validation and transforming of input data, so that's already a big win. Do also get some of the boilerplate result validation you can copy the "http info method", i.e. testMethodGetWithHttpInfo (use async version if you wish). Remove the if/else block after $responseBody = $response->getBody(); and in the return replace the ObjectSearializer line with just $responseBody.
What you are now getting back is the body as PSR7 stream, but there is one last catch. By default the whole response is drained into a temporary file. To really stream the result body from the server in the created method add an option to $options: $options['stream'] = true; Now the method returns as soon as the body is started. No need to wait for the whole body content.
One more thing. As the result is a PSR7 stream you can use $stream->detach() to get the PHP stream resource if you prefer to use the normal file and stream methods.

REST (PHP, CURL) PUT/POST XML Issue: 400 Bad Request, "Invalid URL" Response with Walmart OAuth API (Postman to test)

There's a lot to unpack here. First of all, I've edited the title because I realize while eventually my REST request will be implemented into PHP code, right now I've stripped this down to Postman to test JUST the REST, so I've stripped it as low and basic as possible. I can officially say the problem is with my request.
Basically, I'm making a POST request and also testing with a PUT request to Walmart's API using the "new" OAuth authentication. Sounds grand. GET works BEAUTIFULLY in Postman and in my actual PHP code. POST and PUT immediately return the exact same error, no matter what and how I do: 400 Bad Request, Invalid URL. In the case of my PUT test, which I was doing because it's a simpler and faster text with far less XML to try to comb through, here's the exact response in HTML headers:
<HTML>
<HEAD>
<TITLE>Invalid URL</TITLE>
</HEAD>
<BODY>
<H1>Invalid URL</H1>
The requested URL "http://%5bNo%20Host%5d/v3/inventory?", is invalid.
<p>
Reference #9.c9384317.1556319123.8c89b8dc
</BODY>
</HTML>
I have left testing in PHP through my server and moved into Postman to try to locate the exact issue I'm having, and GET requests work beautifully. I am generating a new Token every 15 minutes or so. I have done... SO many minor changes, but the way the Feed examples and requests work, for all that I can tell I'm doing everything right. I honestly think I'm losing my marbles at this point.
What is most frustrating to me is that GET works. My TOKEN is working. My OAuth is working just fine. A lot of the headers that GET uses for the Walmart API are the exact same between PUT/POST/GET. The difference here is ONLY that the link has query parameters AND XML being shoved into the body. Edit: What I mean is that my headers do not change between the GET and the POST; the only thing that changes in what I am supplying is that XML is being sent in the body, and that query params are required. This is the only thing that changes between a successful GET and an unsuccessful 400 bad request PUT/POST. This leads me to believe something is wrong with how I'm processing the query params or my XML, but considering in the below example I've copy/pasted the XML... I'm not sure. It is an existing item in our catalog, I know for a fact.
Something I have noticed that I'm not quite knowledgeable enough to know if it's an issue or not with Postman is that Walmart's API requests that content-type be multipart/form-data. I've noticed it uses the term "example" when stating this, however, it usually says "this or this" if it'll accept something else. If I switch content-type in Postman to multipart/form-data, however, the Body automatically becomes raw: text instead of raw: XML(application/xml) or text/xml. If I try to swap the raw to those types, it flips my content-type automatically to application/xml, so that's a little... hinky.
I am not going through a Proxy. I've turned off Global Proxy Configuration and Use System Proxy. Request timeout is set to 0. There's nothing Client Certificates. I mean, GET works, and my Token is successfully generated via outside PHP code (not in Postman, couldn't get that to work, said heck it).
HEADERS
PUT URL: https://marketplace.walmartapis.com/v3/inventory?sku=0xyz0
AUTHORIZATION
Bearer Token: Bearer Basic --insert token here--
WM_SVC.NAME: Walmart Marketplace
WM_QOS.CORRELATION_ID: randomString123
WM_SEC.ACCESS_TOKEN: --insert token here--
Accept: application/xml
Host: https://marketplace.walmartapis.com
Content-type: multipart/form-data
BODY
raw: XML(application/xml)
<?xml version="1.0" encoding="UTF-8"?>
<inventory xmlns="http://walmart.com/">
<sku>0xyz0</sku>
<quantity>
<unit>EACH</unit>
<amount>7</amount>
</quantity>
<fulfillmentLagTime>1</fulfillmentLagTime>
</inventory>
Exact response
400 Bad Request
<HTML>
<HEAD>
<TITLE>Invalid URL</TITLE>
</HEAD>
<BODY>
<H1>Invalid URL</H1>
The requested URL "http://%5bNo%20Host%5d/v3/inventory?", is invalid.
<p>
Reference #9.c9384317.1556320429.8ca752c4
</BODY>
</HTML>
Please send help, I think I've been staring at this so long I'm going to leave this physical world behind. Walmart relatively recently updated their authentication to OAuth and they've made vague passes at saying their old authentication will be deprecated and phased out, so I obviously want to try to get this to work.I tried to copy paste everything as best as possible. That XML is copy-pasted almost letter for letter from their example, with my own product switched in.
Also, the reference number down there always changes every time I run this, so it's not something I can actually look up. I've only supplied the Postman side of things because frankly if I can get that to work, my PHP will be fine, I've already knocked out some minor issues with the successful GET request.
If it's a semi-colon issue, I'll scream.
API Documentation: https://developer.walmart.com/#/apicenter/marketPlace/latest#updateInventoryForAnItem
Well, I've figured it out.
You'll notice I'm required to supply a "Host" with my headers. That host is replacing my URl that I'm trying to connect to via POST/PUT/GET, so if my Host is https://marketplace.walmartapis.com, then my request URL is https://https://marketplace.walmartapis.com.
Once I took the https:// out of the host, the entire thing granted me a 200 response. The times I got a correct GET response, I had actually copy-pasted the correct HOST without the HTTPS by pure chance, so I completely missed this between my two separate test cases.

OpenSearch + PHP for INSPIRE ATOM : Why can I get the correct Content-Type?

I've been following the official Technical Guide for Download Services of INSPIRE to build an ATOM feed with OpenSearch: https://inspire.ec.europa.eu/documents/technical-guidance-implementation-inspire-download-services
I'm stuck at creating the PHP that performs the search.
According to the before mentioned guide, the OpenSearch description XML has to have these templates:
<Url type="application/zip" rel="results" template="http://myWeb/atom/search.php?spatial_dataset_identifier_code={inspire_dls:spatial_dataset_identifier_code?}&spatial_dataset_identifier_namespace={inspire_dls:spatial_dataset_identifier_namespace?}&crs={inspire_dls:crs?}&language={language?}&q={searchTerms?}"/>
<Url type="application/atom+xml" rel="describedby" template="http://myWeb/atom/search.php?spatial_dataset_identifier_code={inspire_dls:spatial_dataset_identifier_code?}&spatial_dataset_identifier_namespace={inspire_dls:spatial_dataset_identifier_namespace?}&crs={inspire_dls:crs?}&language={language?}&q={searchTerms?}"/>
The same guide has a PHP script to help the developer write the "search.php" file. That script starts like this:
$returnFile = false;
foreach (apache_request_headers() as $name => $value) {
//echo("$name: $value\n");
if ($name=="Accept" && $value=="application/zip"){
$returnFile = true;
}
}
If $returnFile is true, a ZIP file must be returned. If it is false, an XML is returned (that is what the rest of the script does).
My problem is that the condition $name=="Accept" && $value=="application/zip" is never true, so the search always returns an XML.
How can $value be application/zip?
This is the Accept value that I get when accessing http://myWeb/atom/search.php:
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
EDIT: The guide tells something more about this:
The example given in Annex A uses content negotiation to identify which
operation to perform. This means that even though the operation endpoint is
only one, i.e. http://myWeb/search.php, the client has to set the HTTP
―Accept‖ Header to the correct value in order to receive the expected
result.
So, if I test my ATOM feed in the INSPIRE validator, the client is the INSPIRE validator (the one that sends the queries to search.php)... am I wrong?
I opened an issue with the developers of the validator and they confirmed that is not working as expected. The validator should add the 'Accept' header in the request, but it leaves it blank. They are working in a fix.

How to pass SOAP Request Properties in PHP (as shown in SoapUI)

In SOAP UI I am able to pass a few request properties, specifically Username, Password and WSS-Password Type. They are marked in the screenshot below by a red box:
I've tried passing these values in PHP as the second parameter of the SoapClient function like so:
$soap = new SoapClient('https://rev-int.api.us.fleetmatics.com/Vehicle/SageQuest/VehicleService.svc?wsdl',
array("Username" => "blah#example.com",
"Password" => "notarealpassword",
"WSS-Password Type" => "PasswordText"));
But I get the security error: An error occurred when verifying security for the message.
The WSDL I'm calling itself is here, though my question is about passing the request properties this way in general, really: https://rev-int.api.us.fleetmatics.com/Vehicle/SageQuest/VehicleService.svc?wsdl
It is likely that a security header with username and password is sent along with the message itself, but you can not see this header in the normal window in SoapUI.
Click "http log" in the bottom panel of SoapUI to see the whole communication. Look thoroughly for Header ithems.
If any header ithems, they must be set before your function SOAP call in PHP, but after you've constructed the client.
Use __ setSoapHeaders() to set headers in PHP
I was also getting that security error.
I used the soapui tool as well. But i had better success with the Chrome App called Boomerang.
You need to choose WSS PasswordText under Auth and then everything just seems to work.
Then you can even copy the xml that Boomerang generates into PHP and it works as well.

Invoking WCF service with PHP (with federated security)

I’m trying to invoke a WCF service (.NET) from PHP. It’s a little more complicated than just using a SoapClient since the service uses a WS2007FederationHttpBinding to authenticate.
Here’s the code I’m using at the moment. I haven’t even added credentials as I’m not sure how, but regardless, I’m not even at the point where I’m getting access denied errors.
$wsdl = "https://slc.centershift.com/sandbox40/StoreService.svc?wsdl";
$client = new SoapClient($wsdl,array(
//'soap_version'=>SOAP_1_2 // default 1.1, but this gives 'Uncaught SoapFault exception: [HTTP] Error Fetching http headers'
));
$params = array();
$params['SiteID'] = 123;
$params['GetPromoData'] = false;
$ret = $client->GetSiteUnitData(array('GetSiteUnitData_Request'=>$params));
print_r($ret);
Which WSDL should I be pointing to?
https://slc.centershift.com/Sandbox40/StoreService.svc?wsdl
Seems to be very short, but includes a reference to (note the wsdl0) https://slc.centershift.com/Sandbox40/StoreService.svc?wsdl=wsdl0
https://slc.centershift.com/Sandbox40/StoreService.svc?singleWsdl
Seems to have everything in it.
Do I need to specify SOAP 1.2? When I do, I get a connection timeout ([HTTP] Error Fetching http headers). When I don’t, the default of SOAP 1.1 is used and I get a [HTTP] Cannot process the message because the content type 'text/xml; charset=utf-8' was not the expected type 'application/soap+xml; charset=utf-8'. Is this because I’m not authenticated yet, or because I’m using the wrong SOAP version?
How to authenticate in PHP? Here’s the corresponding .NET/C# code. Do I need to somehow put these as SOAP headers? Or am I thinking about it all wrong, and I need to do some kind of authentication before I even call the method (from what I read, I’m supposed to get a token back and then use it for all future method calls – I think I see an example of this in an answer here on Stack Overflow.
If I call $client->__getFunctions(), using either WSDL and either SOAP version, I’m getting a valid list of all functions, so I assume either of these is fine and my real issue is the authentication.
Other programmers I’ve talked to had spent time trying to get this to work, but gave up and instead implemented a proxy in .NET. They pass their parameters from PHP to their own unsecured .NET service, which in turn calls this secure service. It works, but seems crazily inefficient to me, and counter-productive, as the purpose of WCF is to support all types of clients (even non-HTTP ones!).
I’ve read How to: Create a WSFederationHttpBinding on MSDN, but it didn’t help.
You can use this URL for WSDL https://slc.centershift.com/Sandbox40/StoreService.svc?singleWsdl. This WSDL has all definitions.
You have to use 1.2 because this webservice works with SOAP 1.2 version. I tried it with 1.1 and 1.2 and both of them gived error. 1.1 is version error, 1.2 is timeout error. I think there is an error at this test server. I used it with svcutil to generate code but it gived error too. Normaly it should get information and generate the code example to call service.
Normally you can add authenticate parameters with SoapHeader or directly add to options in SoapClient consruct (if service authentication is basic authentication). I write below code according to your screenshot. But it gives timeout after long wait.
$wsdl = "https://slc.centershift.com/sandbox40/StoreService.svc?wsdl";
$client = new SoapClient($wsdl,array('trace' => 1,'soap_version' => SOAP_1_2));
$security = array(
'UserName' => array(
'UserName'=>'TestUser',
'Password'=>'TestPassword',
'SupportInteractive'=>false
)
);
$header = new SoapHeader('ChannelFactory','Credentials',$security, false);
$client->__setSoapHeaders($header);
$params = array();
$params['SiteID'] = 100000000;
$params['Channel'] = 999;
try {
$ret = $client->GetSiteUnitData($params);
print_r($ret);
}catch(Exception $e){
echo $e->getMessage();
}
__getFunctions works, because it prints functions defined in WSDL. There is no problem with getting WSDL information at first call. But real problem is communication. PHP gets WSDL, generates required SOAP request then sends to server, but server is not responding correctly. SOAP server always gives a response even if parameters or request body are not correct.
You should communicate with service provider, I think they can give clear answer to your questions.
Having worked with consuming .NET WS from PHP before I believe you would need to create objects from classes in PHP that matches the names that .NET is expecting. The WSDL should tell you the types it is expecting. I hope this assist with your path forward!
If the SOAP call works from a C# application, you could use Wireshark (with the filter ip.dst == 204.246.130.80) to view the actual request being made and then construct a similar request from php.
Check this answer to see how you can do a custom SOAP call.
There's also the option of doing raw curl requests, since it might be easier to build your xml body, but then you would have to parse the response yourself with simplexml.

Categories