Error signed-properties-hashing

Hello.
I successfully signed and reported to ZATCA using .NET. However, when I tried using PHP, I failed to report my simplified signed invoice to ZATCA in the simulation environment. I followed all the steps, but I am still receiving this error: …
errorMessages":[{“type”:“ERROR”,“code”:“signed-properties-hashing”,“category”:“CERTIFICATE_ERRORS”,“message”:“Invalid signed properties hashing, SignedProperties with id=‘xadesSignedProperties’”,“status”:“ERROR”}],
please help. I searched everywhere nobody posted a solution for this error in PHP.

Dear @ralfarhan,

If you are willing to implement the signing function from your end, then you need to review the signing process documentation carefully to apply the same logic in your PHP application.

What we have as a suggestions to accelerate solving the issue above is the following:

1- Please refer to the detailed technical guideline “signing process” section, the signing process is described manually in detail.
2- Try to sign the invoice manually following the same steps to have a better understanding of the signing process.
3- Reflect the same logic to your PHP application.
4-If you are stuck on any step, please sign the invoice with the .NET SDK and compare it your application’s output to detect where is the difference, then amend your application as needed.

Also, it’s described in the detailed technical guideline how you can generate the signed properties tag, which it has to be included in the simplified XML tax documents.

Regards,

https://zatca.gov.sa/en/E-Invoicing/Introduction/Guidelines/Documents/E-invoicing-Detailed-Technical-Guideline.pdf

Sir. Check it yourself: I follow the steps carefully 1000 times

setting cert and privatekey (plain security token, plain private key)

private function setCertificates(string $certificate): void
    {
        $this->encodedCertificate = $certificate;
        $this->decodedCertificate = base64_decode($this->encodedCertificate);
        $this->Certificate = "-----BEGIN CERTIFICATE-----\n" .
            chunk_split($this->decodedCertificate, 64, "\n") .
            "-----END CERTIFICATE-----\n";
         $this->certInfo = openssl_x509_parse($this->Certificate);
        $hashBytes = hash('sha256', $this->decodedCertificate);
        $this->x509CertificateContent = base64_encode($hashBytes);


    }
    private function setPrivateKey(string $privateKey, $privateKeyType): void
    {
        $beginMarker = "-----BEGIN $privateKeyType-----";
        $endMarker = "-----END $privateKeyType-----";
        if (strpos($privateKey, $beginMarker) === false || strpos($privateKey, $endMarker) === false) {
            
            $this->privateKeypem = "-----BEGIN $privateKeyType-----\n" .
                chunk_split($privateKey, 64, "\n") .
                "-----END $privateKeyType-----\n";
            $this->plainPrivateKey = $privateKey;
                                    
        } else {

            $this->privateKeypem = $privateKey;
            $this->plainPrivateKey = preg_replace(
                '/-----BEGIN [A-Z ]+-----|-----END [A-Z ]+-----/',
                '',
                $privateKey
            );
        }
        $this->validatePem($this->privateKeypem, $privateKeyType);
    }

Hashing procdure:

$proc = new XSLTProcessor();
$proc->importStylesheet($xsl);

$transformedXml = $proc->transformToDoc($this->invoiceXml);
if (!$transformedXml) {
    throw new Exception("XSL Transformation failed.");
}

// Step 1: Remove the XML declaration first
$xmlStringWithoutDeclaration = preg_replace('/^<\?xml.*?\?>/s', '', $transformedXml->saveXML());

// Step 2: Canonicalize the XML (C14N11)
$doc = new DOMDocument();
$doc->loadXML($xmlStringWithoutDeclaration);
$canonicalizedXml = $doc->C14N(
    false,
    false,
    null,
    ['http://www.w3.org/2006/12/xml-c14n11']
);

// Step 3: Normalize the line breaks
$normalizedXml = str_replace("\r\n", "\n", $canonicalizedXml);

// Step 4: Trim any leading or trailing whitespace
$trimmedXml = trim($normalizedXml);
        $notEcodedHash = hash('sha256', $trimmedXml, true);
        $hexHash = bin2hex($notEcodedHash);
        $base64Hash = base64_encode(hex2bin($hexHash));
  $base64Invoice = base64_encode($trimmedXml);

Up to this point, the standard invoice has been accepted and successfully cleared. However, signing a simplified invoice causes the issue, and here are the main steps:

Getting the digital signature:

private static function getDigitalSignature($notEncoded, $privateKeyContent)
    {
     //   $hashBytes = base64_decode($xmlHashing);

        if ($notEncoded === false) {
            throw new Exception("Failed to decode the base64-encoded XML hashing.");
        }

        //$privateKeyContent = str_replace(["\n", "\t"], '', $privateKeyContent);
        
        $privateKey = openssl_pkey_get_private($privateKeyContent);

        if ($privateKey === false) {
            throw new Exception("Failed to reads private key. $privateKeyContent");
        }

        $signature = '';

        if (!openssl_sign($notEncoded, $signature, $privateKey, OPENSSL_ALGO_SHA256)) {
            throw new Exception("Failed to sign the data.");
        }

        return base64_encode($signature);
    }

hashing sign tag block:

private static function getSignedPropertiesHash($digestValue, $x509IssuerName, $x509SerialNumber)
    {
        $localTimezone = new DateTimeZone('Asia/Riyadh');  // Saudi Arabia's timezone
$signatureTimestamp = (new DateTime())->setTimezone($localTimezone)->format('Y-m-d\TH:i:s');

        $xml = <<<XML
<xades:SignedProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Id="xadesSignedProperties">
    <xades:SignedSignatureProperties>
        <xades:SigningTime>$signatureTimestamp</xades:SigningTime>
        <xades:SigningCertificate>
            <xades:Cert>
                <xades:CertDigest>
                    <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
                    <ds:DigestValue>$digestValue</ds:DigestValue>
                </xades:CertDigest>
                <xades:IssuerSerial>
                    <ds:X509IssuerName>$x509IssuerName</ds:X509IssuerName>
                    <ds:X509SerialNumber>$x509SerialNumber</ds:X509SerialNumber>
                </xades:IssuerSerial>
            </xades:Cert>
        </xades:SigningCertificate>
    </xades:SignedSignatureProperties>
</xades:SignedProperties>
XML;

$dom = new DOMDocument();
$dom->preserveWhiteSpace = false;
$dom->loadXML($xml);

$xpath = new DOMXPath($dom);
$xpath->registerNamespace('xades', 'http://uri.etsi.org/01903/v1.3.2#');

// Extract the <xades:SignedProperties> element
$signedProps = $xpath->query('//xades:SignedProperties')->item(0);
if (!$signedProps) {
    throw new Exception("SignedProperties element not found.");
}

// Canonicalize using Exclusive C14N (no comments)
//$canonical = $signedProps->C14N(true, false);
$canonical = $signedProps->C14N(
            false,
            false,
            null,
            ['http://www.w3.org/2006/12/xml-c14n11']
        );
// Hash and encode
$hash = bin2hex(hash('sha256', $canonical, true));
$base64Digest = base64_encode($hash);

 return [
            'base64Hash' => $base64Digest,
            'signatureTimestamp' => $signatureTimestamp,
        ];

    }

I tried to change these tag spaces and each tag’s XADES URL (with and without). no success
thanks

Thanks. Solved myself.