[Python] How to sign an XML document with Python?

Discussão em 'Python' iniciado por Stack, Fevereiro 27, 2025.

  1. Stack

    Stack Membro Participativo

    I am doing a project in Django (full Python) specifically a sales system. Now, I want to incorporate electronic invoicing to the system, focusing only on the electronic sales receipt for now. In my country, the regulatory entity requires that to send the invoice, it must be done in an XML file (with a specific format), then, it must be signed with the digital certificate of the company, packed in a .zip file and sent to the testing web service of the entity. But, I don't know how to do the signature using only Python.

    I was researching and I noticed that most of them choose to use languages like Java or C#, mainly. I also found the lxml and cryptography libraries, with which I could make the signature, but when I send it to the web service, I get the error:

    Error of the web service: The entered electronic document has been altered - Detail: Incorrect reference digest value


    I was trying to find out how to fix this error, I don't know if it has to do with the way I'm signing the XML document or the method I'm using.

    I share with you the code to give you an idea of what I used:

    from lxml import etree
    from cryptography.hazmat.backends import default_backend
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import padding
    from cryptography.hazmat.primitives.serialization import load_pem_private_key
    import base64
    import os
    import re

    def sign_xml(xml_path, private_key_path, certificate_path, signed_xml_path):
    """Signs an XML document with a digital certificate."""

    # Load the XML document
    tree = etree. parse(xml_path)
    root = tree. getroot()

    # Load the private key
    with open(private_key_path, "rb") as key_file:
    private_key = load_pem_private_key(
    key_file. read(), password=None, backend=default_backend()
    )

    # Create the Signature element
    ns = {
    "cac": "urn:eek:asis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
    "ds": "http://www.w3.org/2000/09/xmldsig#",
    "ext": "urn:eek:asis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2",
    "sac": "urn:sunat.gob.pe:billdownload:2",
    "cbc":"urn:eek:asis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
    }

    # Create the UBLExtensions element if it does not exist
    extensions = root.find("ext:UBLExtensions", ns)
    if extensions is None:
    extensions = etree.SubElement(root, "{%s}UBLExtensions" % ns['ext'])

    # Create the UBLExtension element
    extension = extensions.find("ext:UBLExtension", ns)
    if extension is None:
    extension = etree.SubElement(extensions, "{%s}UBLExtension" % ns['ext'])

    # Create the ExtensionContent element
    extension_content = extension.find("ext:ExtensionContent", ns)
    if extension_content is None:
    extension_content = etree.SubElement(extension, "{%s}ExtensionContent" % ns['ext'])

    # Clear any existing content in ExtensionContent
    extension_content.clear()

    signature = etree.SubElement(extension_content, "{%s}Signature" % ns['ds'], Id="SignatureSP")
    signed_info = etree.SubElement(signature, "{%s}SignedInfo" % ns['ds'])
    canonical_method = etree.SubElement(signed_info, "{%s}CanonicalizationMethod" % ns['ds'], Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315")
    signature_method = etree.SubElement(signed_info, "{%s}SignatureMethod" % ns['ds'], Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256")
    reference = etree.SubElement(signed_info, "{%s}Reference" % ns['ds'], URI="")
    transformations = etree.SubElement(reference, "{%s}Transforms" % ns['ds'])
    transformation = etree.SubElement(transformations, "{%s}Transform" % ns['ds'], Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature")
    digest_method = etree.SubElement(reference, "{%s}DigestMethod" % ns['ds'], Algorithm="http://www.w3.org/2001/04/xmlenc#sha256")

    # Calculate the DigestValue
    copy_tree = etree.fromstring(etree.tostring(tree))

    reference_to_sign = copy_tree.find(".//ds:Reference", ns)
    if reference_to_sign is not None:
    reference_to_sign.getparent().remove(reference_to_sign)

    xml_serialized = etree.tostring(copy_tree, exclusive=1, inclusive_ns_prefixes=None)
    xml_serialized_without_spaces = re.sub(b'>\s*<', b'><', xml_serialized)

    print("Serialized XML (without spaces):", xml_serialized_without_spaces)

    digest = hashes.Hash(hashes.SHA256(), backend=default_backend())
    digest.update(xml_serialized_without_spaces)
    digest_value = base64.b64encode(digest.finalize()).decode()

    print("DigestValue calculated:", digest_value) # Print the calculated digest value

    etree.SubElement(reference, "{%s}DigestValue" % ns['ds']).text = digest_value

    # Calculate the SignatureValue
    info_firmada_serializado = etree.tostring(signed_info, encoding='UTF-8', method='xml')
    signature = private_key.sign(
    info_firmada_serializado,
    padding.PKCS1v15(),
    hashes.SHA256()
    )
    signature_value = base64.b64encode(signature).decode()
    etree.SubElement(signature, "{%s}SignatureValue" % ns['ds']).text = signature_value

    # Add the certificate information
    key_info = etree.SubElement(signature, "{%s}KeyInfo" % ns['ds'])
    x509_data = etree.SubElement(key_info, "{%s}X509Data" % ns['ds'])

    with open(certificate_path, "rb") as cert_file:
    certificate_content = cert_file.read()

    etree.SubElement(x509_data, "{%s}X509Certificate" % ns['ds']).text = base64.b64encode(certificate_content).decode()

    # Save the signed XML
    tree.write(signed_xml_path, encoding="UTF-8", xml_declaration=True, pretty_print=True)


    And this is the test XML document:

    <?xml version="1.0" encoding="UTF-8"?>
    <Invoice xmlns="urn:eek:asis:names:specification:ubl:schema:xsd:Invoice-2"
    xmlns:cac="urn:eek:asis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
    xmlns:cbc="urn:eek:asis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
    xmlns:ds="http://www.w3.org/2000/09/xmldsig#"
    xmlns:ext="urn:eek:asis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2">
    <ext:UBLExtensions>
    <ext:UBLExtension>
    <ext:ExtensionContent />
    </ext:UBLExtension>
    </ext:UBLExtensions>
    <cbc:UBLVersionID>2.1</cbc:UBLVersionID>
    <cbc:CustomizationID>2.0</cbc:CustomizationID>
    <cbc:ID>B001-1</cbc:ID>
    <cbc:IssueDate>2024-02-25</cbc:IssueDate>
    <cbc:IssueTime>11:16:38</cbc:IssueTime>
    <cbc:InvoiceTypeCode listID="0101">03</cbc:InvoiceTypeCode>
    <cbc:Note languageLocaleID="1000"><![CDATA[SON CIENTO DIECIOCHO CON 00/100 SOLES]]></cbc:Note>
    <cbc:DocumentCurrencyCode>PEN</cbc:DocumentCurrencyCode>
    <cac:Signature>
    <cbc:ID>21422312421</cbc:ID>
    <cac:SignatoryParty>
    <cac:partyIdentification>
    <cbc:ID>21422312421</cbc:ID>
    </cac:partyIdentification>
    <cac:partyName>
    <cbc:Name><![CDATA[INVERSION A Y B S.A.C.]]></cbc:Name>
    </cac:partyName>
    </cac:SignatoryParty>
    <cac:DigitalSignatureAttachment>
    <cac:ExternalReference>
    <cbc:URI>#LIMENA CAE-SIGN</cbc:URI>
    </cac:ExternalReference>
    </cac:DigitalSignatureAttachment>
    </cac:Signature>
    <cac:AccountingSupplierParty>
    <cac:party>
    <cac:partyIdentification>
    <cbc:ID schemeID="6">21422312421</cbc:ID>
    </cac:partyIdentification>
    <cac:partyName>
    <cbc:Name><![CDATA[LIMENA CAE]]></cbc:Name>
    </cac:partyName>
    <cac:partyLegalEntity>
    <cbc:RegistrationName><![CDATA[INVERSION A Y B S.A.C.]]></cbc:RegistrationName>
    <cac:RegistrationAddress>
    <cbc:ID>123123</cbc:ID>
    <cbc:AddressTypeCode>0000</cbc:AddressTypeCode>
    <cbc:CitySubdivisionName>CASUARINAS</cbc:CitySubdivisionName>
    <cbc:CityName>LIMA</cbc:CityName>
    <cbc:CountrySubentity>LIMA</cbc:CountrySubentity>
    <cbc:District>LIMA</cbc:District>
    <cac:AddressLine>
    <cbc:Line><![CDATA[CAL.LOMA SOCOSA NRO. 29 (PISO 5) LIMA - LIMA - SANTIAGO DE SURQUILLO]]></cbc:Line>
    </cac:AddressLine>
    <cac:Country>
    <cbc:IdentificationCode>PE</cbc:IdentificationCode>
    </cac:Country>
    </cac:RegistrationAddress>
    </cac:partyLegalEntity>
    <cac:Contact>
    <cbc:Telephone>01-1231231</cbc:Telephone>
    <cbc:ElectronicMail>bginoza7@gmail.com</cbc:ElectronicMail>
    </cac:Contact>
    </cac:party>
    </cac:AccountingSupplierParty>
    <cac:AccountingCustomerParty>
    <cac:party>
    <cac:partyIdentification>
    <cbc:ID schemeID="1">24516534</cbc:ID>
    </cac:partyIdentification>
    <cac:partyLegalEntity>
    <cbc:RegistrationName><![CDATA[PERSON 1]]></cbc:RegistrationName>
    </cac:partyLegalEntity>
    </cac:party>
    </cac:AccountingCustomerParty>
    <cac:TaxTotal>
    <cbc:TaxAmount currencyID="PEN">18.00</cbc:TaxAmount>
    <cac:TaxSubtotal>
    <cbc:TaxableAmount currencyID="PEN">100.00</cbc:TaxableAmount>
    <cbc:TaxAmount currencyID="PEN">18.00</cbc:TaxAmount>
    <cac:TaxCategory>
    <cac:TaxScheme>
    <cbc:ID>1000</cbc:ID>
    <cbc:Name>IGV</cbc:Name>
    <cbc:TaxTypeCode>VAT</cbc:TaxTypeCode>
    </cac:TaxScheme>
    </cac:TaxCategory>
    </cac:TaxSubtotal>
    </cac:TaxTotal>
    <cac:LegalMonetaryTotal>
    <cbc:LineExtensionAmount currencyID="PEN">100.00</cbc:LineExtensionAmount>
    <cbc:TaxInclusiveAmount currencyID="PEN">118.00</cbc:TaxInclusiveAmount>
    <cbc:payableAmount currencyID="PEN">118.00</cbc:payableAmount>
    </cac:LegalMonetaryTotal>
    <cac:InvoiceLine>
    <cbc:ID>1</cbc:ID>
    <cbc:InvoicedQuantity unitCode="NIU">2</cbc:InvoicedQuantity>
    <cbc:LineExtensionAmount currencyID="PEN">100.00</cbc:LineExtensionAmount>
    <cac:pricingReference>
    <cac:AlternativeConditionPrice>
    <cbc:priceAmount currencyID="PEN">59</cbc:priceAmount>
    <cbc:priceTypeCode>01</cbc:priceTypeCode>
    </cac:AlternativeConditionPrice>
    </cac:pricingReference>
    <cac:TaxTotal>
    <cbc:TaxAmount currencyID="PEN">18.00</cbc:TaxAmount>
    <cac:TaxSubtotal>
    <cbc:TaxableAmount currencyID="PEN">100.00</cbc:TaxableAmount>
    <cbc:TaxAmount currencyID="PEN">18.00</cbc:TaxAmount>
    <cac:TaxCategory>
    <cbc:percent>18</cbc:percent>
    <cbc:TaxExemptionReasonCode>10</cbc:TaxExemptionReasonCode>
    <cac:TaxScheme>
    <cbc:ID>1000</cbc:ID>
    <cbc:Name>IGV</cbc:Name>
    <cbc:TaxTypeCode>VAT</cbc:TaxTypeCode>
    </cac:TaxScheme>
    </cac:TaxCategory>
    </cac:TaxSubtotal>
    </cac:TaxTotal>
    <cac:Item>
    <cbc:Description><![CDATA[PROD 1]]></cbc:Description>
    <cac:SellersItemIdentification>
    <cbc:ID>C023</cbc:ID>
    </cac:SellersItemIdentification>
    </cac:Item>
    <cac:price>
    <cbc:priceAmount currencyID="PEN">50</cbc:priceAmount>
    </cac:price>
    </cac:InvoiceLine>
    </Invoice>

    Continue reading...

Compartilhe esta Página