Sign SOAP messages with xmlseclibs
Requirements
- PHP 7.4+
- OpenSSL
Generating Public and Private Keys
First we have to create a private key for signature creation, and a public key for verification. This means that it's fine to distribute your public key. However, the private key should remain secret.
Generate the private key with this OpenSSL command (enter a password):
openssl genrsa -out private.pem 2048
The private key is generated and saved in a file named "private.pem", located in the same directory.
The number "2048" in the above command indicates the size of the private key. You can choose one of these sizes: 512, 758, 1024, 2048 or 4096 (these numbers represent bits). The larger sizes offer greater security, but this is offset by a penalty in CPU performance.
Generating the Public Key
Later we need a public key for the token verification. To extract the public key file, type the following:
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
The public key is saved in a file named public.pem
located in the same directory.
Generating a Certificate
In order to append a X509 certificate to the SOAP signature you have to generate a cert file using the private key:
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout private.pem -out certificate.crt
PFX
Extract private key from a PFX file:
openssl pkcs12 -in filename.pfx -nocerts -out private.pem
Extract certificate from a PFX file:
openssl pkcs12 -in filename.pfx -clcerts -nokeys -out certificate.crt
Installation
Run:
composer require robrichards/xmlseclibs
Code
<?php
namespace Acme\XmlDSig\Soap\Test;
use DOMDocument;
use DOMXPath;
use PHPUnit\Framework\TestCase;
use RobRichards\XMLSecLibs\XMLSecurityDSig;
use RobRichards\XMLSecLibs\XMLSecurityKey;
/**
* Test.
*/
class XmlDsigSoapTest extends TestCase
{
/**
* Test.
*
* @return void
*/
public function testSignAndVerify()
{
$privateKeyPath = __DIR__ . '/private.pem';
$certificatePath = __DIR__ . '/certificate.crt';
$dom = $this->createSoapXmlMessage();
$dsig = new XMLSecurityDSig();
$dsig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);
$dsig->addReference(
$dom,
XMLSecurityDSig::SHA256,
['http://www.w3.org/2000/09/xmldsig#enveloped-signature'],
['force_uri' => true]
);
$dsigKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, ['type' => 'private']);
// Optional
//$dsigKey->passphrase = 'secret';
$dsigKey->loadKey(file_get_contents($privateKeyPath), false, false);
$dsig->sign($dsigKey);
// Add the associated public key to the signature
$options = [
'issuerSerial' => 'serial',
'subjectName' => 'subject',
];
$dsig->add509Cert(file_get_contents($certificatePath), true, false, $options);
// Append signature to root element
//$objDSig->appendSignature($dom->documentElement);
// Append signature to RegisterTCRRequest element
$dsig->appendSignature($dom->getElementsByTagName('RegisterTCRRequest')->item(0));
// Register all namespace to find the Reference element
$xpath = new DOMXPath($dom);
$xpath->registerNamespace('SOAP-ENV', 'http://schemas.xmlsoap.org/soap/envelope/');
$xpath->registerNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#');
$xpath->registerNamespace('xmlns', 'https://efi.tax.gov.me/fs/schema/');
$xpath->registerNamespace('ns2', 'http://www.w3.org/2000/09/xmldsig#');
// Set reference URI
$elements = $xpath->query('//ds:Signature/ds:SignedInfo/ds:Reference');
$elements->item(0)->attributes->item(0)->nodeValue = '#Request';
// Get raw XML
$xmlRaw = $dom->saveXML();
$this->assertStringContainsString('<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">', $xmlRaw);
$this->assertStringContainsString('ds:DigestValue', $xmlRaw);
$this->assertStringContainsString('ds:SignatureValue', $xmlRaw);
$this->assertStringContainsString('ds:X509Certificate', $xmlRaw);
$this->assertStringContainsString('<ds:Reference URI="#Request">', $xmlRaw);
}
/**
* Create soap message.
*
* @return DOMDocument The dom
*/
private function createSoapXmlMessage(): DOMDocument
{
$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;
$envelope = $dom->createElementNS('http://schemas.xmlsoap.org/soap/envelope/', 'SOAP-ENV:Envelope');
$dom->appendChild($envelope);
$soapHeader = $dom->createElement('SOAP-ENV:Header');
$envelope->appendChild($soapHeader);
$body = $dom->createElement('SOAP-ENV:Body');
$envelope->appendChild($body);
$request = $dom->createElement('RegisterTCRRequest');
$body->appendChild($request);
$request->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns', 'https://efi.tax.gov.me/fs/schema/');
$request->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:ns2', 'http://www.w3.org/2000/09/xmldsig#');
$request->setAttribute('Id', 'Request');
$request->setAttribute('Version', '1');
$requestHeader = $dom->createElement('Header');
$request->appendChild($requestHeader);
$requestHeader->setAttribute('SendDateTime', '2021-05-22T13:21:01+01:00');
$requestHeader->setAttribute('UUID', '53f0616c-3078-4678-8716-59353e7475ee');
$tcr = $dom->createElement('TCR');
$request->appendChild($tcr);
$tcr->setAttribute('BusinUnitCode', 'cx614cf575');
$tcr->setAttribute('IssuerTIN', '03204308');
$tcr->setAttribute('MaintainerCode', 'ov571vp018');
$tcr->setAttribute('SoftCode', 'ca181vu261');
$tcr->setAttribute('TCRIntID', '1');
$tcr->setAttribute('ValidFrom', '2021-05-21');
$tcr->setAttribute('Type', 'REGULAR');
return $dom;
}
}
Testing
composer test