I was able to solve this issue after a lot of time waste, and I want to share the root cause and the exact fix for others who might face the same problem.
Many developers still get the signed-properties-hashing
error even when the XML structure and indentation are 100% correct.
The hidden cause in my case was line endings.
On Windows, newlines are stored as \r\n
(carriage return + line feed).
On Linux/macOS, newlines are stored as just \n
.
On Windows, when generating the SignedProperties
block, line breaks are saved as \r\n
(two bytes). These extra \r
characters become part of the byte sequence that gets hashed, which breaks validation.
Before you calculate the hash of the SignedProperties
block, normalize the string by replacing \r\n
with \n
.
In PHP:
$signaturePart = str_replace("\r\n", "\n", $signaturePart);
After this fix, the hashing will be consistent across all environments (Windows, Linux, macOS) and ZATCA validation will succeed.
Here’s the complete process you should follow to generate a valid SignedProperties
section for ZATCA:
Build the SignedProperties block with correct spacing:
Example skeleton:
<xades:SignedProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Id="xadesSignedProperties">
<xades:SignedSignatureProperties>
<xades:SigningTime>SIGNING_TIME_PLACEHOLDER</xades:SigningTime>
<xades:SigningCertificate>
<xades:Cert>
<xades:CertDigest>
<ds:DigestMethod xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue xmlns:ds="http://www.w3.org/2000/09/xmldsig#">DIGEST_PLACEHOLDER</ds:DigestValue>
</xades:CertDigest>
<xades:IssuerSerial>
<ds:X509IssuerName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">ISSUER_PLACEHOLDER</ds:X509IssuerName>
<ds:X509SerialNumber xmlns:ds="http://www.w3.org/2000/09/xmldsig#">SERIAL_PLACEHOLDER</ds:X509SerialNumber>
</xades:IssuerSerial>
</xades:Cert>
</xades:SigningCertificate>
</xades:SignedSignatureProperties>
</xades:SignedProperties>
Replace placeholders with your actual values.
SIGNING_TIME_PLACEHOLDER
→ Signing timestamp (ISO 8601, e.g. 2025-08-16T12:34:56Z
).
DIGEST_PLACEHOLDER
→ SHA-256 (Base64) of your signing certificate bytes.
ISSUER_PLACEHOLDER
→ Issuer DN in the expected format.
SERIAL_PLACEHOLDER
→ Certificate serial number.
Normalize line endings to LF - Don't Canonicalize.
On Windows the block often contains \r\n
. Replace \r\n
→ \n
before hashing to avoid digest mismatches:
$signedPropertiesXml = str_replace("\r\n", "\n", $signedPropertiesXml);
Note that, don't canonicalize, and use the exact same template, as mentioned above, without any spacing or attributes changes etc.
Hash the $SignedPropertiesXml
with SHA-256 and Base64-encode.
$signedPropsDigest = base64_encode(hash('sha256', $signedPropertiesXml));
Reference it correctly inside <ds:SignedInfo>
.
Add a <ds:Reference>
that points to your Id
and uses the correct Type
:
<ds:Reference Type="http://www.w3.org/2000/09/xmldsig#SignatureProperties" URI="#xadesSignedProperties">
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<ds:DigestValue>$signedPropsDigest</ds:DigestValue>
</ds:Reference>
This process will correctly generate the signed properties digest.
I’m using a PHP library, and I contributed a fix for this exact newline hashing issue there. If you are working in PHP, I’d recommend using that library since the fix is already merged: PHP ZATCA XML – Pull Request #4.