Add RFC 3161 timestamp to existing PKCS #7/CMS signature in PHP

I have created a simple (and possibly buggy) library that would allow you to add an RFC 3161 timestamp to an existing PKCS #7 or CMS signature.

Note that it has been tested only on Ubuntu 16.04 and it depends on curl package installed with apt-get.

So with this library, for adding the timestamp you only need to do something like:

<?php
$updatedCms = CmsTimestamper::addTimestampToCms($originalCmsAsPem, "http://tsa.starfieldtech.com");

The library and a demonstration class can be found in https://github.com/hablutzel1/phpcmstimestamper.

Finally, to verify the previous timestamp you could save the updated CMS and verify it with the following set of commands:

# Extract CMS signature value. See RFC 3161, "APPENDIX A".
$ openssl asn1parse -noout -in cms_updated_with_ts.pem -out cms_updated_with_ts.der && dd bs=1 skip=1164 count=256 if=cms_updated_with_ts.der > cms_signature.bin
# Extract TimeStampToken from CMS.
$ openssl asn1parse -noout -in cms_updated_with_ts.pem -offset 1445 -length 1931 -out tst.der
# Verify TimeStampToken against CMS signature value.
$ openssl ts -verify -data cms_signature.bin -in tst.der -token_in -CAfile Starfield_Class_2_Certification_Authority.crt
# Display timestamp details.
$ openssl ts -reply -token_in -in tst.der -text

 

Signing and verifying XML with xmlsec1

Let’s say we have a certification path like the following:

  • Root CA
    • Intermediate CA 1
      • Intermediate CA 2
        • End entity signer

And we want to sign sample.xml:

<?xml version="1.0" encoding="UTF-8"?>
<sample>
    <data>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</data>
</sample>

You will need to modify sample.xml adding to it the highlighted signature template:

<?xml version="1.0" encoding="UTF-8"?>
<sample>
    <data>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</data>
    <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
            <Reference>
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                </Transforms>
                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                <DigestValue/>
            </Reference>
        </SignedInfo>
        <SignatureValue/>
        <KeyInfo>
            <X509Data/>
        </KeyInfo>
    </Signature>
</sample>

Then you can sign it using the following command:

$ xmlsec1 --sign --privkey-pem end_entity_privkey.pem,end_entity_cert.pem --output sample-signed.xml sample.xml

And verify the signature with the following command:

$ xmlsec1 --verify --trusted-pem root_ca.pem --untrusted-pem intermediate_ca_1.pem --untrusted-pem intermediate_ca_2.pem sample-signed.xml
OK
SignedInfo References (ok/all): 1/1
Manifests References (ok/all): 0/0

From the previous command note that you have to add only one --trusted-pem for the root CA and one --untrusted-pem for each intermediate CA.

It is very important to realize that xmlsec1 is expecting only one certificate in each PEM file, irrespective of the common practice to group several certificates in one PEM file and because of this --untrusted-pem is repeated for every intermediate CA.

Another useful post on this topic can be found on http://sgros.blogspot.pe/2013/01/signing-xml-document-using-xmlsec1.html.

Note: xmlsec1 in Windows

From some time already xmlsec1 can be easily installed on Windows as a regular Cygwin package:

See http://cygwin.1069669.n5.nabble.com/ANNOUNCEMENT-xmlsec1-1-2-22-1-td129718.html.

Problema con el certificado digital en Facturador SUNAT v1.5

Después de haber importado un certificado digital en el Facturador SUNAT v1.5 y al intentar generar un comprobante se podría obtener un error como el siguiente:

content[0] is not a valid X509Data type

En cuyo caso es posible que se deba a que la entrada de la clave en el PFX importado tiene un nombre que incluye espacios, como se muestra en el ejemplo a continuación, donde la entrada de clave posee el nombre “juan carlos perez diaz”:

>keytool -list -keystore certificado.pfx
...
juan carlos perez diaz, Jul 22, 2017, PrivateKeyEntry,
Certificate fingerprint (SHA1): D7:5C:99:FC:CE:00:90:D8:02:21:56:40:D5:A5:8E:7C:D7:55:1D:BC

Y esta condición explota un “bug” en el Facturador SUNAT v1.5, que no está soportando apropiadamente las entradas con espacios en su nombre.

De cualquier manera, un sencillo “workaround” consiste en reemplazar aquel nombre por uno que no contenga espacios, para nuestro ejemplo esto se obtendría con el siguiente comando:

>keytool -changealias -keystore certificado.pfx -alias "juan carlos perez diaz" -destalias "juancarlosperezdiaz"

Después de lo cual se debería importar el certificado nuevamente al Facturador SUNAT y volver a generar el comprobante.

Como mencioné anteriormente, esto se debe a un “bug” en la aplicación Facturador SUNAT y si resulta de interés para alguno, el problema se encuentra en el siguiente método, pe.gob.sunat.servicio2.registro.service.BandejaDocumentosServiceImpl#importarCertificado, específicamente donde se resalta a continuación:

salida = FacturadorUtil.executeCommand("keytool -importkeystore -srcalias " + aliasPfx + " -srckeystore " + rutaCertificado + " -srcstoretype pkcs12 -srcstorepass " + passPrivateKey + " -destkeystore " + this.comunesService.obtenerRutaTrabajo("ALMC") + "FacturadorKey.jks -deststoretype JKS -destalias certContribuyente -deststorepass **********");

Cuya solución (rápida pero lejos de lo ideal) podría ser envolver el valor de la variable aliasPfx en comillas dobles como se muestra a continuación:

salida = FacturadorUtil.executeCommand("keytool -importkeystore -srcalias \"" + aliasPfx + "\" -srckeystore " + rutaCertificado + " -srcstoretype pkcs12 -srcstorepass " + passPrivateKey + " -destkeystore " + this.comunesService.obtenerRutaTrabajo("ALMC") + "FacturadorKey.jks -deststoretype JKS -destalias certContribuyente -deststorepass **********");

Por último, me gustaría comunicar esta incidencia a SUNAT para que lo solucionen de su lado, pero no conozco el canal correcto de reporte de incidencias a SUNAT, por lo que agradecería aquella información.