wes4m / zatca-xml-js

An implementation of Saudi Arabia ZATCA's E-Invoicing requirements, processes, and standards in TypeScript.
MIT License
70 stars 59 forks source link

Invalid signed properties hashing #44

Open tawwfik opened 1 year ago

tawwfik commented 1 year ago

Error Invalid signed properties hashing, Signed Properties with id=\'xadesSignedProperties\' during onboarding

fuadhasni commented 1 year ago

Can you please share context, what type of invoice, which environment and if possible generated xml

Also, steps you’ve taken to sign your invoice?

On Tue, Aug 22, 2023 at 2:52 AM, tawfik @.***> wrote:

Error Invalid signed properties hashing, Signed Properties with id='xadesSignedProperties' during onboarding

— Reply to this email directly, view it on GitHub https://github.com/wes4m/zatca-xml-js/issues/44, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABPK2CMNLEHLDPCBAGQCGWTXWPKAFANCNFSM6AAAAAA3Y7QC4Q . You are receiving this because you are subscribed to this thread.Message ID: @.***>

tawwfik commented 1 year ago

` $template = "

SET_SIGN_TIMESTAMP SET_CERTIFICATE_HASH SET_CERTIFICATE_ISSUER SET_CERTIFICATE_SERIAL_NUMBER "; $template = str_replace('SET_CERTIFICATE_HASH', $cert_info['hash'], $template); $template = str_replace('SET_SIGN_TIMESTAMP', $time, $template); $template = str_replace('SET_CERTIFICATE_SERIAL_NUMBER', $cert_info['serial_number'], $template); $template = str_replace('SET_CERTIFICATE_ISSUER', $cert_info['issuer'], $template); $hash=hash('sha256',$template); $signed_properties_hash = base64_encode($hash);` these properties invalid hash the template is invalid this response from zatca api `{ "type" => "ERROR", "code" => "signed-properties-hashing", "category" => "CERTIFICATE_ERRORS", "message" => "Invalid signed properties hashing, SignedProperties with id='xadesSignedProperties'", "status" => "ERROR" }`
fawadsaboor commented 11 months ago

@tawwfik Did you able to fix that issue?

ktnasar commented 11 months ago

i have same issue can anybody help on it

thaifanisalla commented 11 months ago

I have same issue in simulation mode

"errorMessages":[{"type":"ERROR","code":"GENERAL","category":"BUSINESS_RULES","message":"Unable to execute Business Rules validation ->BR-KSA-19","status":"ERROR"},{"type":"ERROR","code":"signed-properties-hashing","category":"CERTIFICATE_ERRORS","message":"Invalid signed properties hashing, SignedProperties with id='xadesSignedProperties'","status":"ERROR"}],"status":"ERROR"},"reportingStatus":"NOT_REPORTED","clearanceStatus":null,"qrSellertStatus":null,"qrBuyertStatus":null
tawwfik commented 11 months ago

Hi guys. I fixed the problem by register Namespace to Invoice Xml file. after that get the properties from invoice file. this code laravel php solve problem.

                   $xml = new DOMDocument("1.0", "utf-8");
                     $xml->loadXML($invoice)// invoice file after populate the properties;
                   //use domPath to register this namespace
                   $xpath = new DOMXPath($xml);
                   //  register  namespace
                   $xpath->registerNamespace('default-ns', "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2");
                   $xpath->registerNamespace('sig', "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-                                      
                   2");
                   $xpath->registerNamespace('sac',                    
                   "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2");
                   $xpath->registerNamespace('sbc', "urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2");
                    $xpath->registerNamespace('ds', "http://www.w3.org/2000/09/xmldsig#");
                   $xpath->registerNamespace('xades', "http://uri.etsi.org/01903/v1.3.2#");
                   // path of SignedProperties
                   $SignedProperties = "//default-ns:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties";
                   // get SignedProperties by path query
                   $SignedPropertiesValue = $xpath->query($SignedProperties);
                    // convert SignedProperties node to c14n standerd.
                   $canonicalizationInvoiceXML = $SignedPropertiesValue[0]->C14N(\true);
                   // replace tag to rquired in zatca.
                   $canonicalizationInvoiceXML = str_replace('></ds:DigestMethod>', '/>', $canonicalizationInvoiceXML);
                   // hash SignedProperties
                   $signed_properties_hash = base64_encode(hash('sha256', $canonicalizationInvoiceXML));
Yottskry commented 7 months ago

Sorry to resurrect an old thread, but I'm having the same issue. Could you show exactly what the SignedProperties section looks like just before you hash it, please? I'm still getting "Invalid signed properties hashing" every time.

ajaybalachandran commented 7 months ago

@Yottskry @fuadhasni @tawwfik @fawadsaboor @thaifanisalla

Issue 1: Inconsistency in E-Invoicing Statistics

After successfully integrating and following the official documentation provided by ZATCA, I have encountered an inconsistency in the e-invoicing statistics displayed on the Fatoora portal's simulation section. Despite receiving a "status":"PASS", "reportingStatus": "REPORTED" for submitted simplified invoices via the API endpoint "https://gw-fatoora.zatca.gov.sa/e-invoicing/simulation/invoices/reporting/single" and despite receiving a "status": "PASS", "clearanceStatus": "CLEARED" for standard invoices via "https://gw-fatoora.zatca.gov.sa/e-invoicing/simulation/invoices/clearance/single", the total count of submitted documents does not reflect accurately on the portal. This issue persists even after multiple checks over several days. Also, if I submitted and got a successful response from the API as the document is accepted and while checking on the Fatoora statistics page sometimes it is counted in rejected documents. Why is this mismatch happening? If I received REPORTED, CLEARED status with no error or warning messages, can I confirm that my document is submitted and accepted by ZATCA successfully? Please clarify this.

Issue 2: Lack of Clarity on Tax Amount Calculation

Additionally, I seek clarification on the calculation of the total tax amount payable to ZATCA for the submitted and accepted invoice documents. As there is no provision to retrieve this information from the ZATCA side, could you please confirm whether we need to calculate this amount internally based on the submitted invoices? For instance, if five invoices are successfully submitted and accepted, how do we determine the total tax amount owed to ZATCA?

Could someone please clarify these doubts?

ajaybalachandran commented 7 months ago

@tawwfik I followed steps to obtain Production CSID in CORE API's from the official documentation and I got issuer name as given below <ds:X509IssuerName>CN=PRZEINVOICESCA4-CA, DC=extgazt, DC=gov, DC=local</ds:X509IssuerName> is this correct issuer name?

owncommander commented 6 months ago

hello i'am facing the same problem with the same error message : "Invalid signed properties hashing, SignedProperties with id='xadesSignedProperties" but i'am using .NET DLL from zatca , (Zatca.EInvoice.SDK.dll + Zatca.EInvoice.SDK.Contracts.dll) everything we need in these dll , hashing and signing invoice , This happen when calling API for invoice Complaince in Fatoora Simulation Portal in Developer Portal never happen this error can any one help me with that ? And I thank you in advance

Yottskry commented 6 months ago

hello i'am facing the same problem with the same error message : "Invalid signed properties hashing, SignedProperties with id='xadesSignedProperties" but i'am using .NET DLL from zatca , (Zatca.EInvoice.SDK.dll + Zatca.EInvoice.SDK.Contracts.dll) everything we need in these dll , hashing and signing invoice , This happen when calling API for invoice Complaince in Fatoora Simulation Portal in Developer Portal never happen this error can any one help me with that ? And I thank you in advance

I also used the .net SDK and eventually got this working. The problem is that the XML has to be formatted exactly as Zatca expects because they also perform the hash at their end, so if we don't make the same manipulations they make they will get a different hash to us.

The steps I followed were:

  1. Sign the document using the SDK. Then obtain the xades:QualifyingProperties node using XPath
  2. Then get the InnerXML property as a string. You'll need to do string manipulation rather than relying on XML objects because you need to guarantee the order of attributes. We'll call this XmlString for now.
  3. Remove all xades:QualifyingProperties' children. This has the side effect of also removing its attributes, so you'll need to add in the Target property again (signode is the node found at 2):
    signode.RemoveAll();
    var xatt = xmldoc.CreateAttribute("Target");
    xatt.Value = "signature";
    signode.Attributes.Append(xatt);
  4. Ensure the attributes for XmlString's elements are in the right order. xmlns:ds="http://www.w3.org/2000/09/xmldsig#" must come first:
                  <xades:SignedProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Id="xadesSignedProperties">
                    <xades:SignedSignatureProperties>
                      <xades:SigningTime>2024-04-16T09:50:23</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#">YmJmMDdjZWQ0ZThkNTM3MjU2YTlhOGIwNTM4ZjBjMWQwYzMzNDJkZmNhZTBhOTUxNDE5NWFjM2RhZDg5NDczMQ==</ds:DigestValue>
                          </xades:CertDigest>
                          <xades:IssuerSerial>
                            <ds:X509IssuerName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">CN=eInvoicing</ds:X509IssuerName>
                            <ds:X509SerialNumber xmlns:ds="http://www.w3.org/2000/09/xmldsig#">1712586866985</ds:X509SerialNumber>
                          </xades:IssuerSerial>
                        </xades:Cert>
                      </xades:SigningCertificate>
                    </xades:SignedSignatureProperties>
                  </xades:SignedProperties>
  1. Remove the space before the slash at the ends of any empty tags. i.e. replace: " \>" with "\>"
  2. Trim any newlines, carriage returns, or white space from the very beginning and end of XmlString (not from the individual lines, but from the string as a whole)
  3. Finally, replace any \r\n combinations with just \n. Lines should end with only newline.
  4. Now you'll need to hash XmlString, but you'll need to do it manually:
    
    string finalstr;

// Hash the signature block. using(var myhash = SHA256.Create()) { var hashval = myhash.ComputeHash(Encoding.UTF8.GetBytes(XmlString)); StringBuilder builder = new StringBuilder(); for (int i = 0; i < hashval.Length; i++) { builder.Append(hashval[i].ToString("x2")); } finalstr = Convert.ToBase64String(Encoding.UTF8.GetBytes(builder.ToString())); }

10. Replace the existing hash value with the one created in step 9:

xpathstr = "//ns:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:SignedInfo/ds:Reference[position()=2]/ds:DigestValue"; signode = xmldoc.DocumentElement.SelectSingleNode(xpathstr, nsmgr); signode.InnerText = finalstr;


One last thing is to remove the extra namespaces from the signature block or it won't validate the document correctly at Zatca, and to replace the entire signature block (removed in step 3) with our signature block. docstr is the entire document as a string (xmldoc.OuterXml):

chararray = new char[] { '\r', '\n' }; XmlString = XmlString.TrimStart(chararray); XmlString = XmlString.Replace(" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"", ""); docstr = docstr.Replace("</xades:QualifyingProperties>", "\n" + signodestr + "</xades:QualifyingProperties>");



It's a lot of hoops to jump through, and it took me about a week of trial and error to get here. I'm not saying this is the only way of doing this or even the best way, but this is what worked for me. I hope it at least puts you on the right track.
owncommander commented 6 months ago

hello i'am facing the same problem with the same error message : "Invalid signed properties hashing, SignedProperties with id='xadesSignedProperties" but i'am using .NET DLL from zatca , (Zatca.EInvoice.SDK.dll + Zatca.EInvoice.SDK.Contracts.dll) everything we need in these dll , hashing and signing invoice , This happen when calling API for invoice Complaince in Fatoora Simulation Portal in Developer Portal never happen this error can any one help me with that ? And I thank you in advance

I also used the .net SDK and eventually got this working. The problem is that the XML has to be formatted exactly as Zatca expects because they also perform the hash at their end, so if we don't make the same manipulations they make they will get a different hash to us.

The steps I followed were:

  1. Sign the document using the SDK. Then obtain the xades:QualifyingProperties node using XPath
  2. Then get the InnerXML property as a string. You'll need to do string manipulation rather than relying on XML objects because you need to guarantee the order of attributes. We'll call this XmlString for now.
  3. Remove all xades:QualifyingProperties' children. This has the side effect of also removing its attributes, so you'll need to add in the Target property again (signode is the node found at 2):
signode.RemoveAll();
var xatt = xmldoc.CreateAttribute("Target");
xatt.Value = "signature";
signode.Attributes.Append(xatt);
  1. Ensure the attributes for XmlString's elements are in the right order. xmlns:ds="http://www.w3.org/2000/09/xmldsig#" must come first:
                  <xades:SignedProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" Id="xadesSignedProperties">
                    <xades:SignedSignatureProperties>
                      <xades:SigningTime>2024-04-16T09:50:23</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#">YmJmMDdjZWQ0ZThkNTM3MjU2YTlhOGIwNTM4ZjBjMWQwYzMzNDJkZmNhZTBhOTUxNDE5NWFjM2RhZDg5NDczMQ==</ds:DigestValue>
                          </xades:CertDigest>
                          <xades:IssuerSerial>
                            <ds:X509IssuerName xmlns:ds="http://www.w3.org/2000/09/xmldsig#">CN=eInvoicing</ds:X509IssuerName>
                            <ds:X509SerialNumber xmlns:ds="http://www.w3.org/2000/09/xmldsig#">1712586866985</ds:X509SerialNumber>
                          </xades:IssuerSerial>
                        </xades:Cert>
                      </xades:SigningCertificate>
                    </xades:SignedSignatureProperties>
                  </xades:SignedProperties>
  1. Remove the space before the slash at the ends of any empty tags. i.e. replace: " \>" with "\>"
  2. Trim any newlines, carriage returns, or white space from the very beginning and end of XmlString (not from the individual lines, but from the string as a whole)
  3. Finally, replace any \r\n combinations with just \n. Lines should end with only newline.
  4. Now you'll need to hash XmlString, but you'll need to do it manually:
string finalstr;

// Hash the signature block.
using(var myhash = SHA256.Create())
{
  var hashval = myhash.ComputeHash(Encoding.UTF8.GetBytes(XmlString));
  StringBuilder builder = new StringBuilder();
  for (int i = 0; i < hashval.Length; i++)
  {
    builder.Append(hashval[i].ToString("x2"));
  }
  finalstr = Convert.ToBase64String(Encoding.UTF8.GetBytes(builder.ToString()));
}
  1. Replace the existing hash value with the one created in step 9:
xpathstr = "//ns:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:SignedInfo/ds:Reference[position()=2]/ds:DigestValue";
signode = xmldoc.DocumentElement.SelectSingleNode(xpathstr, nsmgr);
signode.InnerText = finalstr;

One last thing is to remove the extra namespaces from the signature block or it won't validate the document correctly at Zatca, and to replace the entire signature block (removed in step 3) with our signature block. docstr is the entire document as a string (xmldoc.OuterXml):

chararray = new char[] { '\r', '\n' };
XmlString = XmlString.TrimStart(chararray);
XmlString = XmlString.Replace(" xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\"", "");
docstr = docstr.Replace("</xades:QualifyingProperties>", "\n" + signodestr + "</xades:QualifyingProperties>");

It's a lot of hoops to jump through, and it took me about a week of trial and error to get here. I'm not saying this is the only way of doing this or even the best way, but this is what worked for me. I hope it at least puts you on the right track.

First of all, I would like to thank you very much for the explanation you provided , Second : i found the soluation in another DLL files named "SDKNETFrameWorkLib.dll" , this dll has also Sign and hash Method , and i replaced with the others in "Zatca.EInvoice.SDK.dll" , and it working very well these methos are "EInvoiceSigningLogic" and "HashingValidator" , i expect these DLL's doing what you just explaining .... thanks alot

Yottskry commented 6 months ago

First of all, I would like to thank you very much for the explanation you provided , Second : i found the soluation in another DLL files named "SDKNETFrameWorkLib.dll" , this dll has also Sign and hash Method , and i replaced with the others in "Zatca.EInvoice.SDK.dll" , and it working very well these methos are "EInvoiceSigningLogic" and "HashingValidator" , i expect these DLL's doing what you just explaining .... thanks alot

That's useful to know. We do have those DLL's but they were from an earlier version of the SDK. Perhaps they broke something in the newer versions? I'll have a look at them though, thanks.

tawwfik commented 6 months ago

this code for SignedProperties issue

it's work with me with php language

        $time = $this->dateFormat(new \DateTime());
        $template_xml = str_replace('SET_CERTIFICATE_HASH', $cert_info['hash'], $template_xml);
        $template_xml = str_replace('SET_SIGN_TIMESTAMP', $time, $template_xml);
        $template_xml = str_replace('SET_CERTIFICATE_SERIAL_NUMBER', $cert_info['serial_number'], $template_xml);
        $template_xml = str_replace('SET_CERTIFICATE_ISSUER', $cert_info['issuer'], $template_xml);
        $invoice = str_replace('<xades:SignedProperties/>', ($template_xml), $invoice);
        $invoice = str_replace('<?xml version="1.0"?>', '<?xml version="1.0" encoding="UTF-8"?>', $invoice);
        $xml = new DOMDocument("1.0", "utf-8");
        $xml->loadXML($invoice);
        $xpath = new DOMXPath($xml);
        $xpath->registerNamespace('default-ns', "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2");
        $SignedProperties = "//default-ns:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties";
        $xpath->registerNamespace('sig', "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-2");
        $xpath->registerNamespace('sac', "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2");
        $xpath->registerNamespace('sbc', "urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2");
        $xpath->registerNamespace('ds', "http://www.w3.org/2000/09/xmldsig#");
        $xpath->registerNamespace('xades', "http://uri.etsi.org/01903/v1.3.2#");
        $SignedPropertiesValue = $xpath->query($SignedProperties);
        $canonicalizationInvoiceXML = $SignedPropertiesValue[0]->C14N(\true);
        $canonicalizationInvoiceXML = str_replace('></ds:DigestMethod>', '/>', $canonicalizationInvoiceXML);
        $signed_properties_hash = base64_encode(hash('sha256', $canonicalizationInvoiceXML));
        return [
            'signed_properties_hash' => $signed_properties_hash,
            'invoice' => $invoice,
        ];

$invoice is full invoice without SignedProperties tag $template_xml is SignedProperties tag without data after I add data to SignedProperties add SignedProperties to full invoice
use xpath to get SignedProperties from full invoice not work if you not use xpath you shoud use c14n when get SignedProperties by xpath

tawwfik commented 6 months ago

@tawwfik I followed steps to obtain Production CSID in CORE API's from the official documentation and I got issuer name as given below <ds:X509IssuerName>CN=PRZEINVOICESCA4-CA, DC=extgazt, DC=gov, DC=local</ds:X509IssuerName> is this correct issuer name?

@ajaybalachandran yes is correct

Wagdi-Noman commented 6 months ago

this code for SignedProperties issue

it's work with me with php language

        $time = $this->dateFormat(new \DateTime());
        $template_xml = str_replace('SET_CERTIFICATE_HASH', $cert_info['hash'], $template_xml);
        $template_xml = str_replace('SET_SIGN_TIMESTAMP', $time, $template_xml);
        $template_xml = str_replace('SET_CERTIFICATE_SERIAL_NUMBER', $cert_info['serial_number'], $template_xml);
        $template_xml = str_replace('SET_CERTIFICATE_ISSUER', $cert_info['issuer'], $template_xml);
        $invoice = str_replace('<xades:SignedProperties/>', ($template_xml), $invoice);
        $invoice = str_replace('<?xml version="1.0"?>', '<?xml version="1.0" encoding="UTF-8"?>', $invoice);
        $xml = new DOMDocument("1.0", "utf-8");
        $xml->loadXML($invoice);
        $xpath = new DOMXPath($xml);
        $xpath->registerNamespace('default-ns', "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2");
        $SignedProperties = "//default-ns:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties";
        $xpath->registerNamespace('sig', "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-2");
        $xpath->registerNamespace('sac', "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2");
        $xpath->registerNamespace('sbc', "urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2");
        $xpath->registerNamespace('ds', "http://www.w3.org/2000/09/xmldsig#");
        $xpath->registerNamespace('xades', "http://uri.etsi.org/01903/v1.3.2#");
        $SignedPropertiesValue = $xpath->query($SignedProperties);
        $canonicalizationInvoiceXML = $SignedPropertiesValue[0]->C14N(\true);
        $canonicalizationInvoiceXML = str_replace('></ds:DigestMethod>', '/>', $canonicalizationInvoiceXML);
        $signed_properties_hash = base64_encode(hash('sha256', $canonicalizationInvoiceXML));
        return [
            'signed_properties_hash' => $signed_properties_hash,
            'invoice' => $invoice,
        ];

@tawwfik Can you please provide more context so I can use this code in my project

tawwfik commented 6 months ago

this code for SignedProperties issue it's work with me with php language

        $time = $this->dateFormat(new \DateTime());
        $template_xml = str_replace('SET_CERTIFICATE_HASH', $cert_info['hash'], $template_xml);
        $template_xml = str_replace('SET_SIGN_TIMESTAMP', $time, $template_xml);
        $template_xml = str_replace('SET_CERTIFICATE_SERIAL_NUMBER', $cert_info['serial_number'], $template_xml);
        $template_xml = str_replace('SET_CERTIFICATE_ISSUER', $cert_info['issuer'], $template_xml);
        $invoice = str_replace('<xades:SignedProperties/>', ($template_xml), $invoice);
        $invoice = str_replace('<?xml version="1.0"?>', '<?xml version="1.0" encoding="UTF-8"?>', $invoice);
        $xml = new DOMDocument("1.0", "utf-8");
        $xml->loadXML($invoice);
        $xpath = new DOMXPath($xml);
        $xpath->registerNamespace('default-ns', "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2");
        $SignedProperties = "//default-ns:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties";
        $xpath->registerNamespace('sig', "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-2");
        $xpath->registerNamespace('sac', "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2");
        $xpath->registerNamespace('sbc', "urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2");
        $xpath->registerNamespace('ds', "http://www.w3.org/2000/09/xmldsig#");
        $xpath->registerNamespace('xades', "http://uri.etsi.org/01903/v1.3.2#");
        $SignedPropertiesValue = $xpath->query($SignedProperties);
        $canonicalizationInvoiceXML = $SignedPropertiesValue[0]->C14N(\true);
        $canonicalizationInvoiceXML = str_replace('></ds:DigestMethod>', '/>', $canonicalizationInvoiceXML);
        $signed_properties_hash = base64_encode(hash('sha256', $canonicalizationInvoiceXML));
        return [
            'signed_properties_hash' => $signed_properties_hash,
            'invoice' => $invoice,
        ];

@tawwfik Can you please provide more context so I can use this code in my project

  • What is the contents of the $invoice variable ? is it a full invoice including the UBL extension?.
  • what is the type of $template_xml . thank you in advance افدي الشنب

$invoice is full invoice without SignedPropertiestag $template_xml is SignedProperties tag without data after I add data to SignedProperties add SignedProperties to full invoice
use xpath to get SignedProperties from full invoice not work if you not use xpath you shoud use c14n when get SignedPropertiesby xpath

Wagdi-Noman commented 6 months ago

@tawwfik full invoice including the UBL Extension only without the `SignedProperties'! thank you.

aafi53 commented 5 months ago

Hey , i'm facing this issue , anybody can help me ,code":"invalid-invoice-hash","category":"INVOICE_HASHING_ERRORS","message":"The invoice hash API body does not match the (calculated) Hash of the XML (PHP)

owncommander commented 5 months ago

i face this issue before , the reason was i didn't encode the xml to UTF8 before converting to base64 , because i'am using Arabic Letters

aafi53 commented 5 months ago

i face this issue before , the reason was i didn't encode the xml to UTF8 before converting to base64 , because i'am using Arabic Letters

can you share me some code example, because hash created from my side is not same like created by sdk..

owncommander commented 5 months ago

i face this issue before , the reason was i didn't encode the xml to UTF8 before converting to base64 , because i'am using Arabic Letters

can you share me some code example, because hash created from my side is not same like created by sdk..

after Signing the XML for invoice ... generate hash by using this : HashingValidator ihash = new HashingValidator(); Result hr_g = new Result(); Result hr_v = new Result(); XmlDocument SignedXML = new XmlDocument(); invFileName = vvatno + "" + vdate.Replace("-", "") + "T" + vtime.Replace(":", "") + "_" + vinvno; SignedXMLPath = File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + @"\Invoices\Signed\" + invFileName + ".xml"); SignedXML.LoadXml(SignedXMLPath);

            hr_g = ihash.GenerateEInvoiceHashing(SignedXMLPath);
            hr_v = ihash.ValidateEInvoiceHashing(SignedXMLPath);
            if (hr_v.IsValid)
            {
                ih = hr_g.ResultedValue;
            }

and i saved the uuid and hash into out system database .... then when report or clear the invoice to zatca i used this :: string i = File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + @"\Invoices\Signed\" + invFileName + ".xml"); // For read all xml file into string variable string invb64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(i)); // to Encode all invoice to UTF8 and the convert it to base64


i hope that can help you

aafi53 commented 5 months ago

i face this issue before , the reason was i didn't encode the xml to UTF8 before converting to base64 , because i'am using Arabic Letters

can you share me some code example, because hash created from my side is not same like created by sdk..

after Signing the XML for invoice ... generate hash by using this : HashingValidator ihash = new HashingValidator(); Result hr_g = new Result(); Result hr_v = new Result(); XmlDocument SignedXML = new XmlDocument(); invFileName = vvatno + "" + vdate.Replace("-", "") + "T" + vtime.Replace(":", "") + "_" + vinvno; SignedXMLPath = File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + @"\Invoices\Signed" + invFileName + ".xml"); SignedXML.LoadXml(SignedXMLPath);

            hr_g = ihash.GenerateEInvoiceHashing(SignedXMLPath);
            hr_v = ihash.ValidateEInvoiceHashing(SignedXMLPath);
            if (hr_v.IsValid)
            {
                ih = hr_g.ResultedValue;
            }

and i saved the uuid and hash into out system database .... then when report or clear the invoice to zatca i used this :: string i = File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + @"\Invoices\Signed" + invFileName + ".xml"); // For read all xml file into string variable string invb64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(i)); // to Encode all invoice to UTF8 and the convert it to base64

i hope that can help you

thank you, but still i'm getting same error , do you have any php code example in which you generated hash of invoice from xml

owncommander commented 5 months ago

sorry , but i'am using c#

aafi53 commented 5 months ago

Hi guys. I fixed the problem by register Namespace to Invoice Xml file. after that get the properties from invoice file. this code laravel php solve problem.

                   $xml = new DOMDocument("1.0", "utf-8");
                     $xml->loadXML($invoice)// invoice file after populate the properties;
                   //use domPath to register this namespace
                   $xpath = new DOMXPath($xml);
                   //  register  namespace
                   $xpath->registerNamespace('default-ns', "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2");
                   $xpath->registerNamespace('sig', "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-                                      
                   2");
                   $xpath->registerNamespace('sac',                    
                   "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2");
                   $xpath->registerNamespace('sbc', "urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2");
                    $xpath->registerNamespace('ds', "http://www.w3.org/2000/09/xmldsig#");
                   $xpath->registerNamespace('xades', "http://uri.etsi.org/01903/v1.3.2#");
                   // path of SignedProperties
                   $SignedProperties = "//default-ns:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties";
                   // get SignedProperties by path query
                   $SignedPropertiesValue = $xpath->query($SignedProperties);
                    // convert SignedProperties node to c14n standerd.
                   $canonicalizationInvoiceXML = $SignedPropertiesValue[0]->C14N(\true);
                   // replace tag to rquired in zatca.
                   $canonicalizationInvoiceXML = str_replace('></ds:DigestMethod>', '/>', $canonicalizationInvoiceXML);
                   // hash SignedProperties
                   $signed_properties_hash = base64_encode(hash('sha256', $canonicalizationInvoiceXML));

Bro how you generated InvoiceHash in php ? or3rT7i9zH/YB9b2TkIGT9w1ysISfcckyHpGMSGRVyA= i created invoiceHash by sdk drYi8GYkrOhTk2CzX6MMOUIOtgCz4qJuTaE+FfpZ0lE= this i generated by Php code, when i pass First Hash generated By SDK i didn't get error, but second Invoice Hash shows error, so i get error my error in InvoiceHash, Can you help me in this Regards..

Faheemmcfc commented 5 months ago

I am trying to pass Simplified Invoice (B2C) for compliance. But it's giving me following errors:

Invalid certificate hashing
Invalid signed properties hashing, SignedProperties with id='xadesSignedProperties'

I am using the same procedure for Standard Invoice(B2B) for compliance and it's working very fine. I am using .Net SDK for implementation. Validation of the xml with ZATCA SDK is successful. But when passing the invoice to Compliance Invoice API producing above errors. Only difference in Simplified and Standard invoice is that, simplified invoice have 'InvoiceTypeCode = 020000', but in Standard Invoice it is 'InvoiceTypeCode=010000'.

Trying to get a solution for a long time.

Please help. Thank you..

owncommander commented 5 months ago

I am trying to pass Simplified Invoice (B2C) for compliance. But it's giving me following errors:

Invalid certificate hashing
Invalid signed properties hashing, SignedProperties with id='xadesSignedProperties'

I am using the same procedure for Standard Invoice(B2B) for compliance and it's working very fine. I am using .Net SDK for implementation. Validation of the xml with ZATCA SDK is successful. But when passing the invoice to Compliance Invoice API producing above errors. Only difference in Simplified and Standard invoice is that, simplified invoice have 'InvoiceTypeCode = 020000', but in Standard Invoice it is 'InvoiceTypeCode=010000'.

Trying to get a solution for a long time.

Please help. Thank you..

i was facing these errors before .... the soluation i did is using Old Zatca dll because i'am using .NET like you .... this dll called "SDKNETFrameWorkLib.dll" ... i used this dll for Signing the invoice and "Zatca.EInvoice.SDK.dll" for QR Generating . another note : for complaince using (CCSID) (responce from zatca when send CSR) and for reporting using (PCSID) (when send token+secret+request id from csid) this is link for the old dll : https://www.mediafire.com/file/b7j52okvygc8v3p/SDKNETFrameWorkLib.dll/file i hope that can help best wishes

Faheemmcfc commented 5 months ago

I am trying to pass Simplified Invoice (B2C) for compliance. But it's giving me following errors:

Invalid certificate hashing
Invalid signed properties hashing, SignedProperties with id='xadesSignedProperties'

I am using the same procedure for Standard Invoice(B2B) for compliance and it's working very fine. I am using .Net SDK for implementation. Validation of the xml with ZATCA SDK is successful. But when passing the invoice to Compliance Invoice API producing above errors. Only difference in Simplified and Standard invoice is that, simplified invoice have 'InvoiceTypeCode = 020000', but in Standard Invoice it is 'InvoiceTypeCode=010000'. Trying to get a solution for a long time. Please help. Thank you..

i was facing these errors before .... the soluation i did is using Old Zatca dll because i'am using .NET like you .... this dll called "SDKNETFrameWorkLib.dll" ... i used this dll for Signing the invoice and "Zatca.EInvoice.SDK.dll" for QR Generating . another note : for complaince using (CCSID) (responce from zatca when send CSR) and for reporting using (PCSID) (when send token+secret+request id from csid) this is link for the old dll : https://www.mediafire.com/file/b7j52okvygc8v3p/SDKNETFrameWorkLib.dll/file i hope that can help best wishes

Tried this dll, but hashing function is not working. Code is below: var _IHashingValidator = new SDKNETFrameWorkLib.BLL.HashingValidator(); Result objResult = new Result(); objResult = _IHashingValidator.GenerateEInvoiceHashing(location);

I think this dll doen't have that function. How you are doing the hashing of the xml? Can you help. Thank you..

owncommander commented 5 months ago

this for hashing : SDKNETFrameWorkLib.BLL.HashingValidator ihash = new SDKNETFrameWorkLib.BLL.HashingValidator(); SDKNETFrameWorkLib.GeneralLogic.Result hr_g = new SDKNETFrameWorkLib.GeneralLogic.Result(); SDKNETFrameWorkLib.GeneralLogic.Result hr_v = new SDKNETFrameWorkLib.GeneralLogic.Result(); XmlDocument SignedXML = new XmlDocument(); invFileName = vvatno + "" + vdate.Replace("-", "") + "T" + vtime.Replace(":", "") + "_" + vinvno; SignedXMLPath = File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + @"\Invoices\Signed\" + invFileName + ".xml"); SignedXML.LoadXml(SignedXMLPath);

            hr_g = ihash.GenerateEInvoiceHashing(SignedXMLPath);
            hr_v = ihash.ValidateEInvoiceHashing(SignedXMLPath);
            if (hr_v.IsValid)
            {
                //if (flag == 2) { Console.WriteLine("Invoice Hashing Successfully ....."); }
                ih = hr_g.ResultedValue;
            }
        }
        catch (Exception ErrorHasing)
        {
            File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + @"\Errors\Err_Hash_" + invFileName + ".txt", ErrorHasing.Message);
        }
Faheemmcfc commented 5 months ago

this for hashing : SDKNETFrameWorkLib.BLL.HashingValidator ihash = new SDKNETFrameWorkLib.BLL.HashingValidator(); SDKNETFrameWorkLib.GeneralLogic.Result hr_g = new SDKNETFrameWorkLib.GeneralLogic.Result(); SDKNETFrameWorkLib.GeneralLogic.Result hr_v = new SDKNETFrameWorkLib.GeneralLogic.Result(); XmlDocument SignedXML = new XmlDocument(); invFileName = vvatno + "" + vdate.Replace("-", "") + "T" + vtime.Replace(":", "") + "_" + vinvno; SignedXMLPath = File.ReadAllText(AppDomain.CurrentDomain.BaseDirectory + @"\Invoices\Signed" + invFileName + ".xml"); SignedXML.LoadXml(SignedXMLPath);

            hr_g = ihash.GenerateEInvoiceHashing(SignedXMLPath);
            hr_v = ihash.ValidateEInvoiceHashing(SignedXMLPath);
            if (hr_v.IsValid)
            {
                //if (flag == 2) { Console.WriteLine("Invoice Hashing Successfully ....."); }
                ih = hr_g.ResultedValue;
            }
        }
        catch (Exception ErrorHasing)
        {
            File.WriteAllText(AppDomain.CurrentDomain.BaseDirectory + @"\Errors\Err_Hash_" + invFileName + ".txt", ErrorHasing.Message);
        }

Tried your solution, but couldn't get it right. Hash is not valid after using your method. can I hash the xml without making it as a string.

glow2590 commented 4 months ago

Hi guys. I fixed the problem by register Namespace to Invoice Xml file. after that get the properties from invoice file. this code laravel php solve problem.

                   $xml = new DOMDocument("1.0", "utf-8");
                     $xml->loadXML($invoice)// invoice file after populate the properties;
                   //use domPath to register this namespace
                   $xpath = new DOMXPath($xml);
                   //  register  namespace
                   $xpath->registerNamespace('default-ns', "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2");
                   $xpath->registerNamespace('sig', "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-                                      
                   2");
                   $xpath->registerNamespace('sac',                    
                   "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2");
                   $xpath->registerNamespace('sbc', "urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2");
                    $xpath->registerNamespace('ds', "http://www.w3.org/2000/09/xmldsig#");
                   $xpath->registerNamespace('xades', "http://uri.etsi.org/01903/v1.3.2#");
                   // path of SignedProperties
                   $SignedProperties = "//default-ns:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties";
                   // get SignedProperties by path query
                   $SignedPropertiesValue = $xpath->query($SignedProperties);
                    // convert SignedProperties node to c14n standerd.
                   $canonicalizationInvoiceXML = $SignedPropertiesValue[0]->C14N(\true);
                   // replace tag to rquired in zatca.
                   $canonicalizationInvoiceXML = str_replace('></ds:DigestMethod>', '/>', $canonicalizationInvoiceXML);
                   // hash SignedProperties
                   $signed_properties_hash = base64_encode(hash('sha256', $canonicalizationInvoiceXML));

Thank you so much mate <3

mranonyms022 commented 4 months ago

Hi guys. I fixed the problem by register Namespace to Invoice Xml file. after that get the properties from invoice file. this code laravel php solve problem.

                   $xml = new DOMDocument("1.0", "utf-8");
                     $xml->loadXML($invoice)// invoice file after populate the properties;
                   //use domPath to register this namespace
                   $xpath = new DOMXPath($xml);
                   //  register  namespace
                   $xpath->registerNamespace('default-ns', "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2");
                   $xpath->registerNamespace('sig', "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-                                      
                   2");
                   $xpath->registerNamespace('sac',                    
                   "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2");
                   $xpath->registerNamespace('sbc', "urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2");
                    $xpath->registerNamespace('ds', "http://www.w3.org/2000/09/xmldsig#");
                   $xpath->registerNamespace('xades', "http://uri.etsi.org/01903/v1.3.2#");
                   // path of SignedProperties
                   $SignedProperties = "//default-ns:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties";
                   // get SignedProperties by path query
                   $SignedPropertiesValue = $xpath->query($SignedProperties);
                    // convert SignedProperties node to c14n standerd.
                   $canonicalizationInvoiceXML = $SignedPropertiesValue[0]->C14N(\true);
                   // replace tag to rquired in zatca.
                   $canonicalizationInvoiceXML = str_replace('></ds:DigestMethod>', '/>', $canonicalizationInvoiceXML);
                   // hash SignedProperties
                   $signed_properties_hash = base64_encode(hash('sha256', $canonicalizationInvoiceXML));

Thank you so much mate <3

Will you please provide me some support regarding this I am facing the same Issue I did everything as per the document but I am still getting invoice hashing error.Please help me out

errorMessages": array:1 [ 0 => {#1506 +"type": "ERROR" +"code": "invalid-invoice-hash" +"category": "INVOICE_HASHING_ERRORS" +"message": "The invoice hash API body does not match the (calculated) Hash of the XML" +"status": "ERROR" } ] +"status": "ERROR" }

I am getting this error in laravel

yash-webknot commented 3 weeks ago

Hi guys. I fixed the problem by register Namespace to Invoice Xml file. after that get the properties from invoice file. this code laravel php solve problem.

                   $xml = new DOMDocument("1.0", "utf-8");
                     $xml->loadXML($invoice)// invoice file after populate the properties;
                   //use domPath to register this namespace
                   $xpath = new DOMXPath($xml);
                   //  register  namespace
                   $xpath->registerNamespace('default-ns', "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2");
                   $xpath->registerNamespace('sig', "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-                                      
                   2");
                   $xpath->registerNamespace('sac',                    
                   "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2");
                   $xpath->registerNamespace('sbc', "urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2");
                    $xpath->registerNamespace('ds', "http://www.w3.org/2000/09/xmldsig#");
                   $xpath->registerNamespace('xades', "http://uri.etsi.org/01903/v1.3.2#");
                   // path of SignedProperties
                   $SignedProperties = "//default-ns:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties";
                   // get SignedProperties by path query
                   $SignedPropertiesValue = $xpath->query($SignedProperties);
                    // convert SignedProperties node to c14n standerd.
                   $canonicalizationInvoiceXML = $SignedPropertiesValue[0]->C14N(\true);
                   // replace tag to rquired in zatca.
                   $canonicalizationInvoiceXML = str_replace('></ds:DigestMethod>', '/>', $canonicalizationInvoiceXML);
                   // hash SignedProperties
                   $signed_properties_hash = base64_encode(hash('sha256', $canonicalizationInvoiceXML));

Can anyone please help me with this code. How to implement it in javascript or typescript. I have no idea how to register namespaces in js, ts.

AhmedTarekYA commented 2 weeks ago

i need edit here $template_csr = str_replace('SET_PRODUCTION_VALUE', ($config['production'] ? 'ZATCA-Code-Signing' : 'TSTZATCA-Code-Signing'), $template_csr); i think it is the problem

AhmedTarekYA commented 2 weeks ago

all is ok when use developer mode but when use the simultion opt my EGS not appear in device list !

AhmedTarekYA commented 1 week ago

Hi guys. I fixed the problem by register Namespace to Invoice Xml file. after that get the properties from invoice file. this code laravel php solve problem.

                   $xml = new DOMDocument("1.0", "utf-8");
                     $xml->loadXML($invoice)// invoice file after populate the properties;
                   //use domPath to register this namespace
                   $xpath = new DOMXPath($xml);
                   //  register  namespace
                   $xpath->registerNamespace('default-ns', "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2");
                   $xpath->registerNamespace('sig', "urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-                                      
                   2");
                   $xpath->registerNamespace('sac',                    
                   "urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2");
                   $xpath->registerNamespace('sbc', "urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2");
                    $xpath->registerNamespace('ds', "http://www.w3.org/2000/09/xmldsig#");
                   $xpath->registerNamespace('xades', "http://uri.etsi.org/01903/v1.3.2#");
                   // path of SignedProperties
                   $SignedProperties = "//default-ns:Invoice/ext:UBLExtensions/ext:UBLExtension/ext:ExtensionContent/sig:UBLDocumentSignatures/sac:SignatureInformation/ds:Signature/ds:Object/xades:QualifyingProperties/xades:SignedProperties";
                   // get SignedProperties by path query
                   $SignedPropertiesValue = $xpath->query($SignedProperties);
                    // convert SignedProperties node to c14n standerd.
                   $canonicalizationInvoiceXML = $SignedPropertiesValue[0]->C14N(\true);
                   // replace tag to rquired in zatca.
                   $canonicalizationInvoiceXML = str_replace('></ds:DigestMethod>', '/>', $canonicalizationInvoiceXML);
                   // hash SignedProperties
                   $signed_properties_hash = base64_encode(hash('sha256', $canonicalizationInvoiceXML));

can you help ? public function signInvoice(array $invoice, array $egs_unit, string $certificate, string $private_key) // توقيع الشهادة { require base_path('public/ZATCA/ZATCASimplifiedTaxInvoice.php'); $zatca_simplified_tax_invoice = new ZATCASimplifiedTaxInvoice(); $invoice_xml = $zatca_simplified_tax_invoice->simplifiedTaxInvoice($invoice, $egs_unit); $invoice_hash = $zatca_simplified_tax_invoice->getInvoiceHash($invoice_xml); list($hash, $issuer, $serialNumber, $public_key, $signature) = $zatca_simplified_tax_invoice->getCertificateInfo($certificate); $digital_signature = $zatca_simplified_tax_invoice->createInvoiceDigitalSignature($invoice_hash, $private_key);

    $qr = $zatca_simplified_tax_invoice->generateQR(
        $invoice_xml,
        $digital_signature,
        $public_key,
        $signature,
        $invoice_hash
    );

    $signed_properties_props = [
        'sign_timestamp' => date('Y-m-d\TH:i:s\Z'),
        'certificate_hash' => $hash, // SignedSignatureProperties/SigningCertificate/CertDigest/<ds:DigestValue>SET_CERTIFICATE_HASH</ds:DigestValue>
        'certificate_issuer' => $issuer,
        'certificate_serial_number' => $serialNumber
    ];
    $ubl_signature_signed_properties_xml_string_for_signing = $zatca_simplified_tax_invoice->defaultUBLExtensionsSignedPropertiesForSigning($signed_properties_props);
    $ubl_signature_signed_properties_xml_string = $zatca_simplified_tax_invoice->defaultUBLExtensionsSignedProperties($signed_properties_props);

    $signed_properties_hash = base64_encode(openssl_digest($ubl_signature_signed_properties_xml_string_for_signing, 'sha256'));
    // UBL Extensions

    $ubl_signature_xml_string = $zatca_simplified_tax_invoice->defaultUBLExtensions(
        $invoice_hash, // <ds:DigestValue>SET_INVOICE_HASH</ds:DigestValue>
        $signed_properties_hash, // SignatureInformation/Signature/SignedInfo/Reference/<ds:DigestValue>SET_SIGNED_PROPERTIES_HASH</ds:DigestValue>
        $digital_signature,
        $certificate,
        $ubl_signature_signed_properties_xml_string
    );

    // Set signing elements
    $unsigned_invoice_str = $invoice_xml->saveXML();

    $unsigned_invoice_str = str_replace('SET_UBL_EXTENSIONS_STRING', $ubl_signature_xml_string, $unsigned_invoice_str);
    $unsigned_invoice_str = str_replace('SET_QR_CODE_DATA', $qr, $unsigned_invoice_str);

    $signed_invoice = new DOMDocument();
    $signed_invoice->loadXML($unsigned_invoice_str);

    $signed_invoice_string = $signed_invoice->saveXML();
    //$signed_invoice_string = $zatca_simplified_tax_invoice->signedPropertiesIndentationFix($signed_invoice_string);

    // Define the directory and file path
    $directory_path = storage_path('invoices');
    $name = $invoice['invoice_serial_number'];
    $file_path = $directory_path . "/invoice{$name}.xml";

    // Create the directory if it doesn't exist
    if (!file_exists($directory_path)) {
        mkdir($directory_path, 0777, true);
    }

    // Save the signed invoice XML to a file
    file_put_contents($file_path, $signed_invoice_string);

    return [$signed_invoice_string, $invoice_hash, $qr,"/invoice{$name}.xml"];
}
public function simplifiedTaxInvoice(array $invoice, array $egs_unit)
{
    $populated_template = include(base_path('public/ZATCA/templates/simplified_tax_invoice_template.php'));
    $populated_template = str_replace('SET_INVOICE_TYPE', $this->ZATCAInvoiceTypes[$egs_unit['cancelation']['cancelation_type']], trim($populated_template));
    // if canceled (BR-KSA-56) set reference number to canceled invoice
    if (isset($egs_unit['cancelation']['canceled_invoice_number']) && $egs_unit['cancelation']['canceled_invoice_number']) {
        $populated_template = str_replace('SET_BILLING_REFERENCE', $this->defaultBillingReference($egs_unit['cancelation']['canceled_invoice_number']), $populated_template);
    } else {
        $populated_template = str_replace('SET_BILLING_REFERENCE', '', $populated_template);
    }
    $populated_template = str_replace('SET_INVOICE_SERIAL_NUMBER', $invoice['invoice_serial_number'], $populated_template);
    $populated_template = str_replace('SET_TERMINAL_UUID', $egs_unit['uuid'], $populated_template);
    $populated_template = str_replace('SET_ISSUE_DATE', $invoice['issue_date'], $populated_template);
    $populated_template = str_replace('SET_ISSUE_TIME', $invoice['issue_time'], $populated_template);
    $populated_template = str_replace('SET_PREVIOUS_INVOICE_HASH', $invoice['previous_invoice_hash'], $populated_template);
    $populated_template = str_replace('SET_INVOICE_COUNTER_NUMBER', $invoice['invoice_counter_number'], $populated_template);
    $populated_template = str_replace('SET_COMMERCIAL_REGISTRATION_NUMBER', $egs_unit['CRN_number'], $populated_template);

    $populated_template = str_replace('SET_STREET_NAME', $egs_unit['location']['street'], $populated_template);
    $populated_template = str_replace('SET_BUILDING_NUMBER', $egs_unit['location']['building'], $populated_template);
    $populated_template = str_replace('SET_PLOT_IDENTIFICATION', $egs_unit['location']['plot_identification'], $populated_template);
    $populated_template = str_replace('SET_CITY_SUBDIVISION', $egs_unit['location']['city_subdivision'], $populated_template);
    $populated_template = str_replace('SET_CITY', $egs_unit['location']['city'], $populated_template);
    $populated_template = str_replace('SET_POSTAL_NUMBER', $egs_unit['location']['postal_zone'], $populated_template);

    $populated_template = str_replace('SET_VAT_NUMBER', $egs_unit['VAT_number'], $populated_template);
    $populated_template = str_replace('SET_VAT_NAME', $egs_unit['VAT_name'], $populated_template);
    $populated_template = str_replace('SET_BUYER_NAME', $invoice['buyer_name'], $populated_template);

    $parseLineItems = $this->parseLineItems($invoice['line_items']);
    $populated_template = str_replace('PARSE_LINE_ITEMS', $parseLineItems, $populated_template);
    $document = new DOMDocument();
    $document->loadXML($populated_template);
    return $document;
}

private function defaultBillingReference(string $invoice_number): string
{
    $populated_template = require base_path('public/ZATCA/templates/invoice_billing_reference_template.php');
    return str_replace('SET_INVOICE_NUMBER', $invoice_number, $populated_template);
}

public function getInvoiceHash(DOMDocument $invoice_xml): string
{
    $pure_invoice_string = $this->getPureInvoiceString($invoice_xml);

    $pure_invoice_string = str_replace('<?xml version="1.0" encoding="UTF-8"?>' . "\n", '', $pure_invoice_string);

    $hash = hash('sha256', trim($pure_invoice_string));
    $hash = pack('H*', $hash);

    return base64_encode($hash);
}
 private function getPureInvoiceString(DOMDocument $invoice_xml)
{
    $document = new DOMDocument();
    $document->loadXML($invoice_xml->saveXML());

    while ($element = $document->getElementsByTagName('UBLExtensions')->item(0))
        $element->parentNode->removeChild($element);

    while ($element = $document->getElementsByTagName('Signature')->item(0))
        $element->parentNode->removeChild($element);

    while ($element = $document->getElementsByTagName('AdditionalDocumentReference')->item(2)) // qr code tag remove
        $element->parentNode->removeChild($element);

    return $document->saveXML();
}

public function getCertificateInfo(string $certificate_string): array
{
    $cleaned_certificate_string = $this->cleanUpCertificateString($certificate_string);
    $wrapped_certificate_string = "-----BEGIN CERTIFICATE-----\n{$cleaned_certificate_string}\n-----END CERTIFICATE-----";

    $hash = $this->getCertificateHash($cleaned_certificate_string);
    $x509 = openssl_x509_parse($wrapped_certificate_string);

    // Signature, and public key extraction from x509 PEM certificate (asn1 rfc5280)
    // Crypto module does not have those functionalities so i'm the crypto boy now :(
    // https://github.com/nodejs/node/blob/main/ZATCA/crypto/crypto_x509.cc
    // https://linuxctl.com/2017/02/x509-certificate-manual-signature-verification/
    // https://github.com/junkurihara/js-x509-utils/blob/develop/ZATCA/x509.js
    // decode binary x509-formatted object

    $res = openssl_get_publickey($wrapped_certificate_string);
    $cert = openssl_pkey_get_details($res);

    $public_key = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----'], '', $cert['key']);

    return [
        $hash,
        'CN=' . implode(', ', array_reverse((array) $x509['issuer'])),
        $x509['serialNumber'],
        base64_decode($public_key),
        $this->getCertificateSignature($wrapped_certificate_string),
    ];
}

public function getCertificateSignature(string $cer): string
{
    $res = openssl_x509_read($cer);
    openssl_x509_export($res, $out, FALSE);

    $out = explode('Signature Algorithm:', $out);
    $out = explode('-----BEGIN CERTIFICATE-----', $out[2]);
    $out = explode("\n", $out[0]);
    $out = $out[1] . $out[2] . $out[3] . $out[4];
    $out = str_replace([':', ' '], '', $out);

    return pack('H*', $out);
}

public function extractSignature($certPemString)
{

    $bin = ($certPemString);

    if (empty($certPemString) || empty($bin)) {
        return false;
    }

    $bin = substr($bin, 4);

    while (strlen($bin) > 1) {
        $seq = ord($bin[0]);
        if ($seq == 0x03 || $seq == 0x30) {
            $len = ord($bin[1]);
            $bytes = 0;

            if ($len & 0x80) {
                $bytes = ($len & 0x0f);
                $len = 0;
                for ($i = 0; $i < $bytes; $i++) {
                    $len = ($len << 8) | ord($bin[$i + 2]);
                }
            }

            if ($seq == 0x03) {
                return substr($bin, 3 + $bytes, $len);
            } else {
                $bin = substr($bin, 2 + $bytes + $len);
            }
        } else {
            return false;
        }
    }
    return false;
}

private function getCertificateHash($cleanup_certificate_string): string
{
    $hash = openssl_digest($cleanup_certificate_string, 'sha256');
    return base64_encode($hash);
}

public static function cleanUpCertificateString(string $certificate_string): string
{
    $certificate_string = str_replace('-----BEGIN CERTIFICATE-----', '', $certificate_string);
    $certificate_string = str_replace('-----END CERTIFICATE-----', '', $certificate_string);

    return trim($certificate_string);
}

public function createInvoiceDigitalSignature(string $invoice_hash, string $private_key)
{
    $invoice_hash_bytes = base64_encode($invoice_hash);
    $cleanedup_private_key_string = $this->cleanUpPrivateKeyString($private_key);
    $wrapped_private_key_string = "-----BEGIN EC PRIVATE KEY-----\n{$cleanedup_private_key_string}\n-----END EC PRIVATE KEY-----";

    base64_encode(openssl_sign($invoice_hash_bytes, $binary_signature, $wrapped_private_key_string, 'sha256'));

    return base64_encode($binary_signature);
}

public static function cleanUpPrivateKeyString(string $private_key)
{
    $private_key = str_replace('-----BEGIN EC PRIVATE KEY-----', '', $private_key);
    $private_key = str_replace('-----END EC PRIVATE KEY-----', '', $private_key);

    return trim($private_key);
}