WCF SSL Service with PHP

Datetime:2016-08-23 04:35:29          Topic: PHP  WCF           Share

We had a task recently that required our team – me with my colleague Ahmad to write php code to integrate with existing WCF webservice that includes attaching SSL certificates to requests. The application used to integrate with third-party banking system using a form of B2B web service.

In the following post, the main steps we used to write PHP code compatible with WCF:

Existing .net application

The original code was written in C# using WCF webservice over SOAP, it was attaching SSL certificate in PFX format -that includes all certificate chain -, and a separate private key file (as .key format).

PFX format ( PKCS#12 ) is binary format which is usually used in windows to export/import SSL certificates; it stores certificate, intermediate certificates – if there is any – and private key in one file that can be encrypted and signed.

In original C# code, they defined a class that inherits SoapHttpClientProtocol which was used to add SSL certificates to soap request.

Generating SSL files

The existing code was attaching ssl certificate in .pfx format file, so I converted it to .pem format (which is the standard format for openssl) and extracted the key as separate file using openssl commands as following:

openssl pkcs12 -in certificate.pfx -nocerts -out key.pem -nodes
openssl pkcs12 -in certificate.pfx -nokeys -out certificate.pem
openssl rsa -in key.pem -out certificate.key

Those commands will generate public ssl certificate (*.pem) and private ssl key (*.key) file.

To make sure the generated certificates are correct for php, I wrote this basic function to test:

function validatePublicPrivateKeys($public_key_file, $private_key_file) {
    $public = openssl_pkey_get_public(file_get_contents($public_key_file)); 
    $public_error = openssl_error_string();
    if(!empty($public_error)){
        echo "Error in public key:".$public_error."\n";
    }else{
        echo "public key is valid\n";
    }

    $private = openssl_pkey_get_private(file_get_contents($private_key_file), 'passphrase-here');
    $private_error = openssl_error_string();
    if(!empty($private_error)){
        echo "Error in private key:".$private_error;
    }else{
        echo "private key is valid\n";
    }
}

Extending SoapClient Class

Normally SSL certificate can be used in php SOAP request by setting `local_cert` parameter in SoapClient Constructor. however I found this option somehow limited, because there is no ability for a private key to be attached as separate file in the request.

so what we did is to extend soap client and override __doRequest method to be based on curl to send soap request as HTTP message as following:

class MySoapClient extends \SoapClient{

      public function __doRequest($request, $location, $action, $version, $one_way = FALSE) {
            $curl = curl_init($location);
            //setting curl options and data here...
            //...
            
    }
}

Depending in SOAP version, override to curl header values is needed, in my case version used is SOAP 1.2 , so the headers will be as following:

$curl = curl_init($location);
    $headers = array(
        "Content-type: test/xml;charset=\"utf-8\";action=\"" . $location . '/' . $action . "\"",
        "Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
        "Cache-Control: no-cache",
        "Pragma: no-cache",
        "Content-length: " . strlen($request),
    ); 
        
    curl_setopt($curl, CURLOPT_HEADER, TRUE);
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);

Attaching SSL certificates

I set ssl certificate public file, key file – generated earlier – and passphrase to curl request as following:

curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
        
    curl_setopt($curl, CURLOPT_SSLKEYPASSWD, 'passphrase');
    curl_setopt($curl, CURLOPT_SSLKEY, 'your-private-key.key');
    curl_setopt($curl, CURLOPT_SSLCERT, 'your-certificate.pem');

In case curl error “Peer certificate cannot be authenticated with known CA certificates” appeared – which is usually happens in windows- you shall download CA certificate bundle from Mozilla – or other trusted source -, and save it to your system and set CURLOPT_CAINFO option in curl:

curl_setopt($curl, CURLOPT_CAINFO, “C:\full-path-to\cacert.pem”);

or better set it globally in your php.ini:

curl.cainfo=c:\full-path-to\cacert.pem

The final class implementation will be like the following snippet:

class MySoapClient extends \SoapClient{
      public $certificate_ssl_location = "/full/path/to/your-ssl-public-certificate";
      public $private_key_location = "/full/path/to/your-ssl-private-certificate";
      public $ssl_passphrase = "password";
      public $ca_cert_file = "/full/path/to/ca-file"
      
      public function __doRequest($request, $location, $action, $version, $one_way = FALSE) {

        // Call via Curl and use the timeout
        $curl = curl_init($location);

        $headers = array(
            "Content-type: test/xml;charset=\"utf-8\";action=\"" . $location . '/' . $action . "\"",
            "Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5",
            "Cache-Control: no-cache",
            "Pragma: no-cache",
            "Content-length: " . strlen($request),
        ); //SOAPAction: your op URL

        curl_setopt($curl, CURLOPT_VERBOSE, TRUE);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($curl, CURLOPT_POST, TRUE);
        curl_setopt($curl, CURLOPT_POSTFIELDS, $request);
        curl_setopt($curl, CURLOPT_HEADER, TRUE);
        curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);

        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
        
        curl_setopt($curl, CURLOPT_SSLKEYPASSWD, $this->ssl_passphrase);
        curl_setopt($curl, CURLOPT_SSLKEY, $this->private_key_location);
        curl_setopt($curl, CURLOPT_SSLCERT, $this->certificate_ssl_location);
        curl_setopt($curl, CURLOPT_CAINFO, $this->ca_cert_file);

        $response = curl_exec($curl);

        if (curl_errno($curl)) {
            throw new Exception(curl_error($curl));
        }
        curl_close($curl);

        $soap_start = strpos($response, "<soapenv:Envelope");
        $soap_response = substr($response, $soap_start);

        if (!$one_way) {
            return $soap_response;
        }
    }
}

you can call soap request normally by initializing object from MySoapClient class (instead of native SoapClient class)

and now the integration with secured WFC web service works fine





About List