final class XmlService
{
/**
* @param string $xmlContent
* @param int $signatureAlg
* @return bool
* @throws \Exception
*/
public function validateXml(string $xmlContent, $signatureAlg = OPENSSL_ALGO_SHA256): bool
{
$document = $this->getDOMDocument($xmlContent);
$signXpath = $this->getDOMXpath($document);
return $this->checkSign($document, $signXpath, $signatureAlg) && $this->matchDigest($document, $signXpath);
}
/**
* @param \DOMDocument $document
* @param \DOMXPath $signXpath
* @param int $signatureAlg
* @return bool
* @throws \Exception
*/
private function checkSign(\DOMDocument $document, \DOMXPath $signXpath, $signatureAlg = OPENSSL_ALGO_SHA256): bool
{
$signature = $signXpath->query('.//ds:Signature', $document)->item(0);
$signedInfo = $signXpath->query('./ds:SignedInfo', $signature)->item(0);
$signedInfo = $signedInfo->C14N(false, true);
$signValue = $signXpath->query('./ds:SignatureValue', $signature)->item(0);
$signedText = base64_decode(trim($signValue->textContent));
$loadedCert = $this->getCertificateResource($document, $signXpath);
if (!openssl_verify($signedInfo, $signedText, $loadedCert, $signatureAlg)) {
$openSslErrors = [];
while ($error = openssl_error_string()) {
$openSslErrors[] = $error;
}
throw new \Exception('Openssl errors: ' . json_encode($openSslErrors));
}
return true;
}
/**
* @param \DOMDocument $document
* @param \DOMXPath $signXpath
* @return bool
* @throws \Exception
*/
private function matchDigest(\DOMDocument $document, \DOMXPath $signXpath): bool
{
$signature = $signXpath->query('.//ds:Signature', $document)->item(0);
$signedInfo = $signXpath->query('./ds:SignedInfo', $signature)->item(0);
$reference = $signXpath->query('./ds:Reference', $signedInfo)->item(0);
$refDigest = $signXpath->query('./ds:DigestValue', $reference)->item(0);
$rootDoc = $document->childNodes->item(0);
$rootDoc->removeChild($signature); // the node will be deleted everywhere
$c14nDocument = $rootDoc->C14N(false, false);
$matchingDigest = base64_encode(hash('sha256', $c14nDocument, true));
if ($matchingDigest !== $refDigest->textContent) {
throw new \Exception('Content was changed');
}
return true;
}
/**
* @param \DOMDocument $document
* @param \DOMXPath $signXpath
* @return resource
*/
private function getCertificateResource(\DOMDocument $document, \DOMXPath $signXpath)
{
$signature = $signXpath->query('.//ds:Signature', $document)->item(0);
$signKeyInfo = $signXpath->query('./ds:KeyInfo', $signature)->item(0);
$x509Cert = $signXpath->query('./ds:X509Data/ds:X509Certificate', $signKeyInfo)->item(0);
$certificate = $x509Cert->textContent;
$certificate = str_replace(["\n", "\r", ' '], '', $certificate);
$certificate = "-----BEGIN CERTIFICATE-----\n" . chunk_split($certificate, 64, "\n") . "-----END CERTIFICATE-----\n";
return openssl_x509_read($certificate);
}
/**
* @param string $xmlContent
* @return \DOMDocument
*/
private function getDOMDocument(string $xmlContent): \DOMDocument
{
$domDocument = new \DOMDocument();
$domDocument->loadXML($xmlContent);
return $domDocument;
}
/**
* @param \DOMDocument $document
* @return \DOMXPath
*/
private function getDOMXpath(\DOMDocument $document): \DOMXPath
{
$signXpath = new \DOMXPath($document);
$signXpath->registerNamespace('ds', 'http://www.w3.org/2000/09/xmldsig#');
return $signXpath;
}
}
Добрый день, коллеги. Вот пример с нативным php, тестировал только для sha256. Проверял на xml, которую можно сгенерить в "NCALayer\commonbundle_sample\index.html". Выкладываю как есть, пишите если есть предложения. Используйте на свой страх и риск, т.к. выше сказали что юридическую силу в спорных ситуациях имеют только оф. либы, которые, к сожалению, пока только для windows есть.