Remove "ds:" in signed XML tree namespace in python's signxml
Question:
I am working with Python’s lxml and signxml to generate an xml file and sign it with a pem certificate and private key.
I am required to validate the signed xml in the followign website validate XML. For some reason in this website the signed XML files with the "ds" namespace in signature tags do not recognize the file as signed.
I will not focus much on the generated xml file with lxml. The code to sign the xml file has the following form:
def _get_xml_tree_root(self):
root = ET.Element('facturaElectronicaCompraVenta' , attrib={location_attribute: invoice_sector + '.xsd'})
xml_header = ET.SubElement(root, 'header')
xml_detail = ET.SubElement(root, 'detail')
return root
def _get_signed_xml(self):
signed_root = XMLSigner().sign(
self._get_xml_tree_root(),
key=base64.b64decode(io.TextIOWrapper(BytesIO(electronic_key)).read()),
cert=base64.b64decode(io.TextIOWrapper(BytesIO(electronic_certificate)).read())
)
return signed_root
The problem is that the xml file that I generate in the signature section has following form:
<facturaElectronicaCompraVenta ><facturaElectronicaCompraVenta xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="facturaElectronicaCompraVenta.xsd">
<header></header>
<detail></detail>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>WmFvnKBZIr9D37PaYuxM3aoXVu9nDZT+2MI1I+RUh8s= </DigestValue>
</Reference>
</SignedInfo>
<SignatureValue> itb123fGGhh12DpFDFas34ASDAPpSSSSadasDasAS1smkRsj5ksdjasd8asdkasjd8asdkas8asdk21v a1qf+kBKLwF39mj+5zKo1qf+kBKLD42qD/+yxSMMS6DM5SywPxO1oyjnSZtObIe/45fdS4sE9+aNOn UncYUlSDAPpSSSSadasgIMWwlX2XMJ4SDAPpSSSSadas6qihJt/3dEIdta1RETSDAPpSSSSadas9S2W ALbT3VV8pjLqikVLcSDAPpSSSSadaseCKG8abcdssM0Wm8p+5grNNpSDAPpSSSSadasy4TvT4C3xS 70zSbKWeBUUglRcU8FECEcacu+UJaBCgRW0S3Q== </SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>
CertificateStuff..
</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
</facturaElectronicaCompraVenta>
I not sure why the site do not recognize the signature with the "ds:" namespace. I have previously struggled with xml namespaces and I do not understand them very well.
But, how could I sign the XML file without the "ds:" namespace without changing the signxml library source code?
Answers:
Solution 1:
I came up with a function that changes namespace ns_from
to ns_to
. Removing a namespace can just be realized by setting ns_to
to ""
.
(However, this solution seems to be problematic according the comments by the OP)
def replace_namespace(root, ns_from, ns_to):
"""
Merge the namespace ns_from to ns_to in the tree rooted at the root, everything that belongs to ns_from will be in
ns_to
To do so, I
1. change the tag of root
Change the namespace of root from ns_from to ns_to
2. change the attribute
Change the namespace of root from ns_from to ns_to
3. change the nsmap
delete the ns_from space
4. keep other property
"""
# change the tag of root
tag = etree.QName(root.tag)
# if there are attributes belong to namespace ns_from, update to namespace of the
# attributes to namespace ns_to
if tag.namespace == ns_from:
root.tag = '{%s}%s' % (ns_to, tag.localname)
# change the attribute of root
# if there are attributes belong to namespace ns_from, update to namespace of the
# attributes to namespace ns_to
root_attrib_dict = dict(root.attrib)
new_attrib_dict = {}
for key, value in root_attrib_dict.items():
key_QName = etree.QName(key)
if key_QName.namespace == ns_from:
new_key = '{%s}%s' % (ns_to, key_QName.localname)
new_attrib_dict[new_key] = value
else:
new_attrib_dict[key] = value
# set the new nsmap
new_nsmap = root.nsmap.copy()
for ns_key, ns_value in root.nsmap.items():
if ns_value == ns_from:
del new_nsmap[ns_key]
# make the updated root
new_root = etree.Element(root.tag, attrib=new_attrib_dict, nsmap=new_nsmap)
# copy other properties
new_root.text = root.text
new_root.tail = root.tail
# call recursively
for old_root in root[:]:
new_root.append(replace_namespace(old_root, ns_from, ns_to))
return new_root
Test Codes:
input_xml_string = """
<facturaElectronicaCompraVenta )
Which gives:
<?xml version='1.0' encoding='UTF-8'?>
<facturaElectronicaCompraVenta >this answser from the following two aspects:
- I use a recursive function. The original answer only reset
nsmap
for the root node. So there will be a % (target_nt, tag.localname)
# change the attribute of root
# if there are attributes belong to other namespace, update to namespace of the
# attributes to target_nt
root_attrib_dict = dict(root.attrib)
new_attrib_dict = {}
for key, value in root_attrib_dict.items():
key_QName = etree.QName(key)
if key_QName.namespace is not None:
new_key = '{%s}%s' % (target_nt, key_QName.localname)
else:
new_key = key
new_attrib_dict[new_key] = value
# set the new nsmap
# only keep the target_nt, set to default namespace
new_nsmap = {None: target_nt}
# make the updated root
new_root = etree.Element(root.tag, attrib=new_attrib_dict, nsmap=new_nsmap)
# copy other properties
new_root.text = root.text
new_root.tail = root.tail
# call recursively
for old_root in root[:]:
new_root.append(set_namsespace(old_root, target_nt))
return new_root
Test Codes:
root = etree.fromstring(input_xml_string)
target_ns = "http://www.w3.org/2001/XMLSchema-instance"
new_root = set_namsespace(root, target_ns)
# create a new elementtree with new_root so that we can use the
# .write method.
tree = etree.ElementTree()
tree._setroot(new_root)
tree.write('done.xml',
pretty_print=True, xml_declaration=True, encoding='UTF-8')
Which gives:
<?xml version='1.0' encoding='UTF-8'?>
<facturaElectronicaCompraVenta _nt:
del new_nsmap[ns_key]
new_nsmap[None] = target_nt
# make the updated root
root_attrib_dict = dict(root.attrib)
new_root = etree.Element(root.tag, attrib=root_attrib_dict, nsmap=new_nsmap)
# copy other properties
new_root.text = root.text
new_root.tail = root.tail
# call recursively
for old_root in root[:]:
new_root.append(set_namsespace(old_root, target_nt))
return new_root
Using:
root = etree.fromstring(input_xml_string)
target_ns = "http://www.w3.org/2000/09/xmldsig#"
new_root = set_namsespace(root, target_ns)
Which gives:
<?xml version='1.0' encoding='UTF-8'?>
<facturaElectronicaCompraVenta ,nsmap={ None: 'http://www.w3.org/2000/09/xmldsig#' })
signed_root = XMLSigner(c14n_algorithm='http://www.w3.org/2001/10/xml-exc-c14n#').sign(
root,
key=open('example.key').read(),
cert=open('example.pem').read()
)
return signed_root
if __name__ == '__main__':
root = get_xml_tree_root()
signed_root = get_signed_xml(root)
signed_xml_file = open('signed.xml','wb')
signed_xml_file.write(etree.tostring(signed_root,encoding='UTF-8'))
signed_xml_file.close()
print(etree.tostring(signed_root,encoding='UTF-8',pretty_print=True).decode('UTF-8'))
you get the following output:
<facturaElectronicaCompraVenta>
<header/>
<detail/>
<Signature rel="nofollow noreferrer">https://github.com/XML-Security/signxml/issues/171
I am working with Python’s lxml and signxml to generate an xml file and sign it with a pem certificate and private key.
I am required to validate the signed xml in the followign website validate XML. For some reason in this website the signed XML files with the "ds" namespace in signature tags do not recognize the file as signed.
I will not focus much on the generated xml file with lxml. The code to sign the xml file has the following form:
def _get_xml_tree_root(self):
root = ET.Element('facturaElectronicaCompraVenta' , attrib={location_attribute: invoice_sector + '.xsd'})
xml_header = ET.SubElement(root, 'header')
xml_detail = ET.SubElement(root, 'detail')
return root
def _get_signed_xml(self):
signed_root = XMLSigner().sign(
self._get_xml_tree_root(),
key=base64.b64decode(io.TextIOWrapper(BytesIO(electronic_key)).read()),
cert=base64.b64decode(io.TextIOWrapper(BytesIO(electronic_certificate)).read())
)
return signed_root
The problem is that the xml file that I generate in the signature section has following form:
<facturaElectronicaCompraVenta ><facturaElectronicaCompraVenta xmlns_xsi="http://www.w3.org/2001/XMLSchema-instance" xsi_noNamespaceSchemaLocation="facturaElectronicaCompraVenta.xsd">
<header></header>
<detail></detail>
<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
<SignedInfo>
<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
<Reference URI="">
<Transforms>
<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
<Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
</Transforms>
<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
<DigestValue>WmFvnKBZIr9D37PaYuxM3aoXVu9nDZT+2MI1I+RUh8s= </DigestValue>
</Reference>
</SignedInfo>
<SignatureValue> itb123fGGhh12DpFDFas34ASDAPpSSSSadasDasAS1smkRsj5ksdjasd8asdkasjd8asdkas8asdk21v a1qf+kBKLwF39mj+5zKo1qf+kBKLD42qD/+yxSMMS6DM5SywPxO1oyjnSZtObIe/45fdS4sE9+aNOn UncYUlSDAPpSSSSadasgIMWwlX2XMJ4SDAPpSSSSadas6qihJt/3dEIdta1RETSDAPpSSSSadas9S2W ALbT3VV8pjLqikVLcSDAPpSSSSadaseCKG8abcdssM0Wm8p+5grNNpSDAPpSSSSadasy4TvT4C3xS 70zSbKWeBUUglRcU8FECEcacu+UJaBCgRW0S3Q== </SignatureValue>
<KeyInfo>
<X509Data>
<X509Certificate>
CertificateStuff..
</X509Certificate>
</X509Data>
</KeyInfo>
</Signature>
</facturaElectronicaCompraVenta>
I not sure why the site do not recognize the signature with the "ds:" namespace. I have previously struggled with xml namespaces and I do not understand them very well.
But, how could I sign the XML file without the "ds:" namespace without changing the signxml library source code?
Solution 1:
I came up with a function that changes namespace ns_from
to ns_to
. Removing a namespace can just be realized by setting ns_to
to ""
.
(However, this solution seems to be problematic according the comments by the OP)
def replace_namespace(root, ns_from, ns_to):
"""
Merge the namespace ns_from to ns_to in the tree rooted at the root, everything that belongs to ns_from will be in
ns_to
To do so, I
1. change the tag of root
Change the namespace of root from ns_from to ns_to
2. change the attribute
Change the namespace of root from ns_from to ns_to
3. change the nsmap
delete the ns_from space
4. keep other property
"""
# change the tag of root
tag = etree.QName(root.tag)
# if there are attributes belong to namespace ns_from, update to namespace of the
# attributes to namespace ns_to
if tag.namespace == ns_from:
root.tag = '{%s}%s' % (ns_to, tag.localname)
# change the attribute of root
# if there are attributes belong to namespace ns_from, update to namespace of the
# attributes to namespace ns_to
root_attrib_dict = dict(root.attrib)
new_attrib_dict = {}
for key, value in root_attrib_dict.items():
key_QName = etree.QName(key)
if key_QName.namespace == ns_from:
new_key = '{%s}%s' % (ns_to, key_QName.localname)
new_attrib_dict[new_key] = value
else:
new_attrib_dict[key] = value
# set the new nsmap
new_nsmap = root.nsmap.copy()
for ns_key, ns_value in root.nsmap.items():
if ns_value == ns_from:
del new_nsmap[ns_key]
# make the updated root
new_root = etree.Element(root.tag, attrib=new_attrib_dict, nsmap=new_nsmap)
# copy other properties
new_root.text = root.text
new_root.tail = root.tail
# call recursively
for old_root in root[:]:
new_root.append(replace_namespace(old_root, ns_from, ns_to))
return new_root
Test Codes:
input_xml_string = """
<facturaElectronicaCompraVenta )
Which gives:
<?xml version='1.0' encoding='UTF-8'?>
<facturaElectronicaCompraVenta >this answser from the following two aspects:
- I use a recursive function. The original answer only reset
nsmap
for the root node. So there will be a % (target_nt, tag.localname)
# change the attribute of root
# if there are attributes belong to other namespace, update to namespace of the
# attributes to target_nt
root_attrib_dict = dict(root.attrib)
new_attrib_dict = {}
for key, value in root_attrib_dict.items():
key_QName = etree.QName(key)
if key_QName.namespace is not None:
new_key = '{%s}%s' % (target_nt, key_QName.localname)
else:
new_key = key
new_attrib_dict[new_key] = value
# set the new nsmap
# only keep the target_nt, set to default namespace
new_nsmap = {None: target_nt}
# make the updated root
new_root = etree.Element(root.tag, attrib=new_attrib_dict, nsmap=new_nsmap)
# copy other properties
new_root.text = root.text
new_root.tail = root.tail
# call recursively
for old_root in root[:]:
new_root.append(set_namsespace(old_root, target_nt))
return new_root
Test Codes:
root = etree.fromstring(input_xml_string)
target_ns = "http://www.w3.org/2001/XMLSchema-instance"
new_root = set_namsespace(root, target_ns)
# create a new elementtree with new_root so that we can use the
# .write method.
tree = etree.ElementTree()
tree._setroot(new_root)
tree.write('done.xml',
pretty_print=True, xml_declaration=True, encoding='UTF-8')
Which gives:
<?xml version='1.0' encoding='UTF-8'?>
<facturaElectronicaCompraVenta _nt:
del new_nsmap[ns_key]
new_nsmap[None] = target_nt
# make the updated root
root_attrib_dict = dict(root.attrib)
new_root = etree.Element(root.tag, attrib=root_attrib_dict, nsmap=new_nsmap)
# copy other properties
new_root.text = root.text
new_root.tail = root.tail
# call recursively
for old_root in root[:]:
new_root.append(set_namsespace(old_root, target_nt))
return new_root
Using:
root = etree.fromstring(input_xml_string)
target_ns = "http://www.w3.org/2000/09/xmldsig#"
new_root = set_namsespace(root, target_ns)
Which gives:
<?xml version='1.0' encoding='UTF-8'?>
<facturaElectronicaCompraVenta ,nsmap={ None: 'http://www.w3.org/2000/09/xmldsig#' })
signed_root = XMLSigner(c14n_algorithm='http://www.w3.org/2001/10/xml-exc-c14n#').sign(
root,
key=open('example.key').read(),
cert=open('example.pem').read()
)
return signed_root
if __name__ == '__main__':
root = get_xml_tree_root()
signed_root = get_signed_xml(root)
signed_xml_file = open('signed.xml','wb')
signed_xml_file.write(etree.tostring(signed_root,encoding='UTF-8'))
signed_xml_file.close()
print(etree.tostring(signed_root,encoding='UTF-8',pretty_print=True).decode('UTF-8'))
you get the following output:
<facturaElectronicaCompraVenta>
<header/>
<detail/>
<Signature rel="nofollow noreferrer">https://github.com/XML-Security/signxml/issues/171