I solved this some time ago, but never had time to make it more "nicer". So generally problem was the way how wsHttpBinding message security works and how to implement it on PHP. I used concept from https://github.com/enginaygen/kps-soap-client/blob/master/KPSSoapClient.php and also added psha1 from Implementation of P_SHA1 algorithm in PHP.
So the way it needs to work is this:
- PHP request for security token from WSS with its request secret
- WS generates security token and returns it to PHP
- PHP generates SOAP C14N signed message with requested security token
Here is the imeplentation (NOTE: I have not implemented it by expanding PHP soap client due to problems on WSDL import. Also as I said I used other people concepts and never made to clean up code - especially XML generation).
// TODO implement this by extending SoapClient class
// currently not implemented in it because request params are not generated correctly
/**
* Client implementing SOAP wsHttpBinding with message security. <br>
* NOTE: this is adapted to work for special needs of our client. It can be modified and there is a lot of work that jet needs to be done (nicer code, options and optimization).
*/
class WSSoap
{
/**
* Securit token request template
*/
const STS_TEMPLATE = <<<X
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing"xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"><s:Header><a:Action s:mustUnderstand="1">http://schemas.xmlsoap.org/ws/2005/02/trust/RST/SCT</a:Action><a:MessageID></a:MessageID><a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo><a:To s:mustUnderstand="1"></a:To><o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><u:Timestamp u:Id="_0"><u:Created></u:Created><u:Expires></u:Expires></u:Timestamp><o:UsernameToken u:Id="_1"><o:Username></o:Username><o:Password Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"></o:Password></o:UsernameToken></o:Security></s:Header><s:Body><t:RequestSecurityToken xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust"><t:TokenType>http://schemas.xmlsoap.org/ws/2005/02/sc/sct</t:TokenType><t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType><t:Entropy><t:BinarySecret Type="http://schemas.xmlsoap.org/ws/2005/02/trust/Nonce"></t:BinarySecret></t:Entropy><t:KeySize>256</t:KeySize></t:RequestSecurityToken></s:Body></s:Envelope>
X;
/**
* Any action request template (mainly for headers)
*/
const KPS_TEMPLATE = <<<X
<s:Envelope xmlns:s="http://www.w3.org/2003/05/soap-envelope" xmlns:a="http://www.w3.org/2005/08/addressing" xmlns:u="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"><s:Header><a:Action s:mustUnderstand="1">n</a:Action><a:MessageID></a:MessageID><a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo><a:To s:mustUnderstand="1"></a:To><o:Security s:mustUnderstand="1" xmlns:o="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"><u:Timestamp u:Id="_0"><u:Created></u:Created><u:Expires></u:Expires></u:Timestamp><c:SecurityContextToken xmlns:c="http://schemas.xmlsoap.org/ws/2005/02/sc"><c:Identifier></c:Identifier></c:SecurityContextToken><Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></CanonicalizationMethod><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#hmac-sha1"></SignatureMethod><Reference URI="#_0"> <Transforms><Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"></Transform></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></DigestMethod><DigestValue></DigestValue></Reference></SignedInfo><SignatureValue></SignatureValue><KeyInfo><o:SecurityTokenReference><o:Reference ValueType="http://schemas.xmlsoap.org/ws/2005/02/sc/sct"></o:Reference></o:SecurityTokenReference></KeyInfo></Signature></o:Security></s:Header><s:Body></s:Body></s:Envelope>
X;
/**
* Namespaces
*/
const S11 = "http://schemas.xmlsoap.org/soap/envelope/";
const S12 = "http://www.w3.org/2003/05/soap-envelope";
const WSU = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd";
const WSSE = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd";
const WSSE11 = "http://docs.oasis-open.org/wss/oasis-wss-wsecurity-secext-1.1.xsd";
const WST = "http://schemas.xmlsoap.org/ws/2005/02/trust";
const DS = "http://www.w3.org/2000/09/xmldsig#";
const XENC = "http://www.w3.org/2001/04/xmlenc#";
const WSP = "http://schemas.xmlsoap.org/ws/2004/09/policy";
const WSA = "http://www.w3.org/2005/08/addressing";
const XS = "http://www.w3.org/2001/XMLSchema";
const WSDL = "http://schemas.xmlsoap.org/wsdl/";
const SP = "http://docs.oasis-open.org/ws-sx/ws-securitypolicy/200702";
const SC = "http://schemas.xmlsoap.org/ws/2005/02/sc";
/**
* STS Properties
*/
protected $stsHostName;
protected $stsEndpoint;
protected $stsUsername;
protected $stsPassword;
protected $stsNamespace;
/**
* Binary secret used for generating request
*/
protected $requestSecret;
protected $rstrBinarySecret;
protected $rstrKeyIdentifier;
protected $token;
protected $tokenReference;
function __construct( $username, $password, $endpointURL, $namespace )
{
$this->stsUsername = $username;
$this->stsPassword = $password;
$this->stsHostName = parse_url( $endpointURL, PHP_URL_HOST);
$this->stsEndpoint = $endpointURL;
$this->stsNamespace = $namespace;
}
function request( $action, $fullActionName, $params )
{
$this->stsRequest();
$kpsDom = new \DOMDocument("1.0", "utf-8");
$kpsDom->preserveWhiteSpace = false;
$kpsDom->loadXML(static::KPS_TEMPLATE);
$kpsXpath = new \DOMXPath($kpsDom);
$kpsXpath->registerNamespace('S12', static::S12);
$kpsXpath->registerNamespace('WSA', static::WSA);
$kpsXpath->registerNamespace('WSU', static::WSU);
$kpsXpath->registerNamespace('WSSE', static::WSSE);
$kpsXpath->registerNamespace('XENC', static::XENC);
$kpsXpath->registerNamespace('DS', static::DS);
$kpsXpath->registerNamespace('SC', static::SC);
// Addressing
$uuid = $this->uuid();
$actionPath = $kpsXpath->query("//S12:Envelope/S12:Header/WSA:Action");
$messageIDPath = $kpsXpath->query("//S12:Envelope/S12:Header/WSA:MessageID");
$toPath = $kpsXpath->query("//S12:Envelope/S12:Header/WSA:To");
$actionPath->item(0)->nodeValue = $fullActionName;
$messageIDPath->item(0)->nodeValue = sprintf("urn:uuid:%s", $uuid);
$toPath->item(0)->nodeValue = $this->stsEndpoint;
// Timestamp
$time = time();
$dateCreated = gmdate('Y-m-d\TH:i:s\Z', $time);
$dateExpires = gmdate('Y-m-d\TH:i:s\Z', $time + (5 * 60));
$timestampPath = $kpsXpath->query("//S12:Envelope/S12:Header/WSSE:Security/WSU:Timestamp");
$timestampDateCreatedPath = $kpsXpath->query("//S12:Envelope/S12:Header/WSSE:Security/WSU:Timestamp/WSU:Created");
$timestampDateExpiresPath = $kpsXpath->query("//S12:Envelope/S12:Header/WSSE:Security/WSU:Timestamp/WSU:Expires");
$timestampDateCreatedPath->item(0)->nodeValue = $dateCreated;
$timestampDateExpiresPath->item(0)->nodeValue = $dateExpires;
$timestampC14N = $timestampPath->item(0)->C14N(true, false);
// DigestValue
$digestValue = base64_encode(hash('sha1', $timestampC14N, true));
$digestValuePath = $kpsXpath->query("//S12:Envelope/S12:Header/WSSE:Security/DS:Signature/DS:SignedInfo/DS:Reference/DS:DigestValue");
$digestValuePath->item(0)->nodeValue = $digestValue;
// Signature
$signaturePath = $kpsXpath->query("//S12:Envelope/S12:Header/WSSE:Security/DS:Signature/DS:SignedInfo");
$signatureValuePath = $kpsXpath->query("//S12:Envelope/S12:Header/WSSE:Security/DS:Signature/DS:SignatureValue");
$signatureC14N = $signaturePath->item(0)->C14N(true, false);
$psBinary = $this->psha1( $this->requestSecret, $this->rstrBinarySecret );
$signatureValue = base64_encode(hash_hmac("sha1", $signatureC14N, $psBinary, true));
$signatureValuePath->item(0)->nodeValue = $signatureValue;
// token reference
$securityContextTokenReference = $kpsXpath->query("//S12:Envelope/S12:Header/WSSE:Security/DS:Signature/DS:KeyInfo/WSSE:SecurityTokenReference/WSSE:Reference");
$securityContextTokenReference->item(0)->setAttribute('URI', "#$this->tokenReference");
// token ID
$tokenPath = $kpsXpath->query("//S12:Envelope/S12:Header/WSSE:Security/SC:SecurityContextToken");
$tokenPath->item(0)->setAttribute('u:Id', $this->tokenReference);
// token
$tokenPath = $kpsXpath->query("//S12:Envelope/S12:Header/WSSE:Security/SC:SecurityContextToken/SC:Identifier");
$tokenPath->item(0)->nodeValue = $this->token;
// Message
$bodyElemet = $kpsXpath->query("//S12:Envelope/S12:Body")->item(0);
$root = $kpsDom->createElementNS( $this->stsNamespace, $action );
foreach( $params as $name => $value ) {
$root->appendChild( $kpsDom->createElement( $name, $value ) );
}
$bodyElemet->appendChild( $root );
$kpsRequest = $kpsDom->saveXML();
// Request
try {
$stsResponse = $this->execCurl( $kpsRequest );
} catch ( \Exception $e ) {
throw $e;
}
return $stsResponse;
}
/**
* Performs a STS request
*
* @param string $location Request location
*/
protected function stsRequest()
{
$rstXml = static::STS_TEMPLATE;
$rstDom = new \DOMDocument("1.0", "utf-8");
$rstDom->preserveWhiteSpace = false;
$rstDom->loadXML($rstXml);
$rstXpath = new \DOMXPath($rstDom);
$rstXpath->registerNamespace('S12', static::S12);
$rstXpath->registerNamespace('WSA', static::WSA);
$rstXpath->registerNamespace('WSU', static::WSU);
$rstXpath->registerNamespace('WSSE', static::WSSE);
$rstXpath->registerNamespace('XENC', static::XENC);
$rstXpath->registerNamespace('DS', static::DS);
$rstXpath->registerNamespace('WST', static::WST);
$rstXpath->registerNamespace('WSP', static::WSP);
// Addressing
$uuid = $this->uuid();
$messageIDPath = $rstXpath->query("//S12:Envelope/S12:Header/WSA:MessageID");
$toPath = $rstXpath->query("//S12:Envelope/S12:Header/WSA:To");
$messageIDPath->item(0)->nodeValue = sprintf("urn:uuid:%s", $uuid);
$toPath->item(0)->nodeValue = $this->stsEndpoint;
// Timestamp
$time = time();
$dateCreated = gmdate('Y-m-d\TH:i:s\Z', $time);
$dateExpires = gmdate('Y-m-d\TH:i:s\Z', $time + (5 * 60));
$timestampDateCreatedPath = $rstXpath->query("//S12:Envelope/S12:Header/WSSE:Security/WSU:Timestamp/WSU:Created");
$timestampDateExpiresPath = $rstXpath->query("//S12:Envelope/S12:Header/WSSE:Security/WSU:Timestamp/WSU:Expires");
$timestampDateCreatedPath->item(0)->nodeValue = $dateCreated;
$timestampDateExpiresPath->item(0)->nodeValue = $dateExpires;
// Credentials
$usernamePath = $rstXpath->query("//S12:Envelope/S12:Header/WSSE:Security/WSSE:UsernameToken/WSSE:Username");
$passwordPath = $rstXpath->query("//S12:Envelope/S12:Header/WSSE:Security/WSSE:UsernameToken/WSSE:Password");
$usernamePath->item(0)->nodeValue = $this->stsUsername;
$passwordPath->item(0)->nodeValue = $this->stsPassword;
// Set binary key
$this->requestSecret = uniqid();
$binaryKeyPath = $rstXpath->query("//S12:Envelope/S12:Body/WST:RequestSecurityToken/WST:Entropy/WST:BinarySecret");
$binaryKeyPath->item(0)->nodeValue = base64_encode( $this->requestSecret );
// Endpoint
$stsRequest = $rstDom->saveXML();
// Request
try {
$stsResponse = $this->execCurl( $stsRequest );
} catch ( \Exception $e ) {
throw $e;
}
$rstrDom = new \DOMDocument("1.0", "utf-8");
$rstrDom->preserveWhiteSpace = false;
$rstrDom->loadXML($stsResponse);
$rstrXpath = new \DOMXPath($rstrDom);
$rstrXpath->registerNamespace('S12', static::S12);
$rstrXpath->registerNamespace('WSA', static::WSA);
$rstrXpath->registerNamespace('WSU', static::WSU);
$rstrXpath->registerNamespace('WSSE', static::WSSE);
$rstrXpath->registerNamespace('XENC', static::XENC);
$rstrXpath->registerNamespace('DS', static::DS);
$rstrXpath->registerNamespace('WST', static::WST);
$rstrXpath->registerNamespace('WSP', static::WSP);
$rstrXpath->registerNamespace('SC', static::SC);
// parse security context token
$securityContextTokenReference = $rstrXpath->query("//S12:Envelope/S12:Body/WST:RequestSecurityTokenResponse/WST:RequestedSecurityToken/SC:SecurityContextToken");
$this->tokenReference = $securityContextTokenReference->item(0)->getAttribute('u:Id');
$securityContextToken = $rstrXpath->query("//S12:Envelope/S12:Body/WST:RequestSecurityTokenResponse/WST:RequestedSecurityToken/SC:SecurityContextToken/SC:Identifier");
$this->token = $securityContextToken->item(0)->nodeValue;
$securityContextToken = $rstrXpath->query("//S12:Envelope/S12:Body/WST:RequestSecurityTokenResponse/WST:Entropy/WST:BinarySecret");
$this->rstrBinarySecret = base64_decode( $securityContextToken->item(0)->nodeValue );
}
protected function execCurl( $request )
{
// Request
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->stsEndpoint);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); // disable SSL verification - re-enable if needed
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
"Host: " . $this->stsHostName,
"Content-Type: application/soap+xml; charset=utf-8",
"Content-Length: " . strlen( $request ),
));
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $request );
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
if ( $response === false ) {
throw new \Exception(curl_error($ch));
}
curl_close($ch);
return $response;
}
/**
* Generates UUID
*
* @return string UUID
*/
protected function uuid()
{
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', //
mt_rand(0, 0xffff), //
mt_rand(0, 0xffff), //
mt_rand(0, 0xffff), //
mt_rand(0, 0x0fff) | 0x4000, //
mt_rand(0, 0x3fff) | 0x8000, //
mt_rand(0, 0xffff), //
mt_rand(0, 0xffff), //
mt_rand(0, 0xffff) //
);
}
/**
* Calculate psha1 hash used for signature generation
* @param unknown $clientSecret
* @param unknown $serverSecret
* @param number $sizeBits
* @return string
*/
protected function psha1($clientSecret, $serverSecret, $sizeBits = 256)
{
$sizeBytes = $sizeBits / 8;
$hmacKey = $clientSecret;
$hashSize = 160; // HMAC_SHA1 length is always 160
$bufferSize = $hashSize / 8 + strlen($serverSecret);
$i = 0;
$b1 = $serverSecret;
$b2 = "";
$temp = null;
$psha = array();
while ($i < $sizeBytes) {
$b1 = hash_hmac('SHA1', $b1, $hmacKey, true);
$b2 = $b1 . $serverSecret;
$temp = hash_hmac('SHA1', $b2, $hmacKey, true);
for ($j = 0; $j < strlen($temp); $j++) {
if ($i < $sizeBytes) {
$psha[$i] = $temp[$j];
$i++;
} else {
break;
}
}
}
return implode("", $psha);
}
}
So to get something like this in request:
<s:Header>
<a:Action s:mustUnderstand="1">https://some.url/NamespaceName/IServices/CheckTransaction</a:Action>
...
</s:Header>
<s:Body>
<CheckTransaction xmlns="https://sime.url/ActionToDo">
<TransactionID>1234567</TransactionID>
</CheckTransaction>
</s:Body>
Code would be:
$url = 'https://some.url/Services.svc';
$namespace = 'https://some.url/NamespaceName'; // this is action namespace you need, since there is no WSDL parsing you need to set it by yourself
try {
$c = new WSSoap( $username, $password, $url, $namespace );
$params = array(
'TransactionID' => '1234567'
);
$r = $c->request( 'CheckTransaction', 'https://some.url/NamespaceName/IServices/CheckTransaction', $params ); // also applies - no WSDL parsing so we need to set params
} catch (Exception $e) {
throw $e;
}