Python y firma XML
Tags: python, xml, x509, firma
Published: 2023-05-05 21:36:30

Vamos a ver, cómo firmar documentos XML y, cómo validar si el contenido de un documento firmado, se mantiene inalterado o, por el contrario, ha sufrido alguna modificación.

Como no puede ser de otra manera, lo primero que vamos a hacer es crear un nuevo entorno virtual y a activarlo.

```bash
mkdir firmaXML
cd firmaXML
python3 -m venv venv
source venv/bin/activate
```

Ahora instalamos las librerías que vamos a necesitar.
```bash
pip install pip --update
pip install lxml
pip install signxml
```

Para poder proceder a firmar documentos, deberemos disponer de un certificado x509, aunque podemos crear uno autofirmado con el siguiente comando:

```bash
openssl req -x509 -nodes \
        -subj "/CN=Certificado autofirmado" -days 365 \
        -newkey rsa \
        -keyout clave_privada.key \
        -out clave_publica.crt

```
Ya estamos en disposición de crear un ejemplo de como firmar un XML.

```python
from lxml import etree
from signxml import XMLSigner, XMLVerifier 
from signxml.exceptions import InvalidSignature

# Nuestro contenido XML
XML = """
<test>
   Contenido importante
</test>
"""

# Recuperamos nuestras claves de certificado.Para la firma, se necesitan tanto la
# clave pública como la privada. Para la validación, sólo se requiere la pública.
clave_publica = open("clave_publica.crt", "r").read()
clave_privada = open("clave_privada.key", "r").read()

# Construimos un objeto XNL a partir de string con el contenido
nodo_raiz = etree.fromstring(XML)

# Creamos objeto XML firmado
nodo_raiz_firmado = XMLSigner().sign( 
    data=nodo_raiz,
    key=clave_privada,
    cert=clave_publica 
)

# Convertimos objeto XML a formato string y lo guardamos en fichero para comprobar después
# que se produce un error si manipulamos su contenido
XML_firmado = etree.tostring(nodo_raiz_firmado, pretty_print=True).decode()
with open("XML_firmado.xml", "w") as f:
    f.write(XML_firmado)


# Validamos integridad del contenido XML
try:
    XML_validado = XMLVerifier().verify(
        data=etree.fromstring(XML_firmado),
        x509_cert=clave_publica
    ).signed_xml
except InvalidSignature as err:
    print("ERROR validando integridad XML")
    exit()

print("=== XML original =============================================================")
print(XML)

print("=== XML firmado ==============================================================")
print(XML_firmado)

print("=== XML validado =============================================================")
print(etree.tostring(XML_validado, pretty_print=True).decode())

```
Creo que el código es bastante autoexplicativo y no necesita de una mayor esplicación. Tras su ejecución, dispondremos de un fichero llamado **XML_firmado.xml**. Por lo que ahora, ya podemos proceder a comprobar la integridad del fichero.
```python
from lxml import etree
from signxml import XMLSigner, XMLVerifier 
from signxml.exceptions import InvalidSignature

# Recuperamos contenido fichero XML
XML = open("XML_firmado.xml", "r").read()

# Para la validación, sólo se requiere la clave pública.
clave_publica = open("clave_publica.crt", "r").read()

# Construimos un objeto XNL a partir de string con el contenido
nodo_raiz = etree.fromstring(XML)

# Validamos integridad del contenido XML
try:
    XML_validado = XMLVerifier().verify(
        data=etree.fromstring(XML),
        x509_cert=clave_publica
    ).signed_xml
except InvalidSignature as err:
    print("ERROR validando integridad XML")
    exit()

print("=== XML original =============================================================")
print(XML)

print("=== XML validado =============================================================")
print(etree.tostring(XML_validado, pretty_print=True).decode())
```
Si lo ejecutamos, sin haber modificado el fichero, y después de haber realizado alguna modificación del contenido, podremos observar la diferencia.

Como puedes comprobar, la cosa es bastante simple ¿no?