Firmado de documentos XML


Requisitos:

 

Lenguaje de Programación

JAVA

IDE

Eclipse

Acceso a Internet

Solo para descarga de librerías

 

Pasos para la configuración:

 

·  Dependencias: Para el firmado de documentos XML necesitamos las siguientes dependencias:

 


pom.xml

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>

    <groupId>bo.sin</groupId>

    <artifactId>TestFirma</artifactId>

    <version>1.0-SNAPSHOT</version>

    <packaging>jar</packaging>

     <dependencies>

        <dependency>

            <groupId>junit</groupId>

            <artifactId>junit</artifactId>

            <version>4.12</version>

            <scope>test</scope>

        </dependency>

        <dependency>

            <groupId>org.hamcrest</groupId>

            <artifactId>hamcrest-core</artifactId>

            <version>1.3</version>

            <scope>test</scope>

        </dependency>

        <dependency>

            <groupId>org.bouncycastle</groupId>

            <artifactId>bcprov-jdk15on</artifactId>

            <version>1.56</version>

        </dependency>

        <dependency>

            <groupId>org.bouncycastle</groupId>

            <artifactId>bcpkix-jdk15on</artifactId>

            <version>1.56</version>

        </dependency>

        <dependency>

            <groupId>org.apache.santuario</groupId>

            <artifactId>xmlsec</artifactId>

            <version>2.0.5</version>

            <type>jar</type>

        </dependency>

        <dependency>

            <groupId>commons-io</groupId>

            <artifactId>commons-io</artifactId>

            <version>2.5</version>

            <scope>test</scope>

            <type>jar</type>

        </dependency>

    </dependencies>

    <properties>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <maven.compiler.source>1.8</maven.compiler.source>

        <maven.compiler.target>1.8</maven.compiler.target>

    </properties>

    <build>

        <resources>

            <resource>

                <directory>src/main/resources</directory>

            </resource>

        </resources>

    </build>

</project>

 

Implementación de código:

 

La clase principal tiene la siguiente forma:

 

Firmador.java

/*

 * To change this license header, choose License Headers in Project Properties.

 * To change this template file, choose Tools | Templates

 * and open the template in the editor.

 */

package bo.sin.testfirma;

/**

 *

 * @author marcelo.romero

 */

import java.io.BufferedReader;

import java.io.ByteArrayInputStream;

import java.io.ByteArrayOutputStream;

import java.io.FileInputStream;

import java.io.FileReader;

import java.io.IOException;

import java.security.GeneralSecurityException;

import java.security.KeyFactory;

import java.security.PrivateKey;

import java.security.PublicKey;

import java.security.Security;

import java.security.cert.CertificateException;

import java.security.cert.CertificateFactory;

import java.security.cert.X509Certificate;

import java.security.interfaces.RSAPrivateKey;

import java.security.interfaces.RSAPublicKey;

import java.security.spec.PKCS8EncodedKeySpec;

import java.security.spec.X509EncodedKeySpec;

import javax.xml.parsers.DocumentBuilder;

import javax.xml.parsers.DocumentBuilderFactory;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.commons.codec.binary.Base64;

import org.apache.xml.security.Init;

import org.apache.xml.security.algorithms.MessageDigestAlgorithm;

import org.apache.xml.security.exceptions.XMLSecurityException;

import org.apache.xml.security.signature.XMLSignature;

import org.apache.xml.security.transforms.Transforms;

import org.apache.xml.security.utils.Constants;

import org.apache.xml.security.utils.ElementProxy;

import org.apache.xml.security.utils.XMLUtils;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

import org.w3c.dom.Document;

import org.w3c.dom.Element;

import org.xml.sax.SAXException;

/**

 *

 * @author

 */

public class Firmador {

    // http://stackoverflow.com/questions/7224626/how-to-sign-string-with-private-key

    private static Firmador instancia;

    private String ALG = "SHA1withRSA";

    static {

        Init.init();

        Security.addProvider(new BouncyCastleProvider());

    }

    /**

     * Obtener un firmador por defecto.

     *

     * @return un Firmador.

     */

    public static Firmador getInstance() {

        if (instancia == null) {

            instancia = new Firmador();

        }

        return instancia;

    }

    private Firmador() {

    }

    //// Todo: Colocar en un solo directorio la llave privada con la publica

    /**

     * Esta funcion añade una firma a un documento XML.

     *

     * @param datos Documento a firmar <i>XML</i>.

     * @param priv Clave privada.

     * @param cert Certificado del firmante.

     * @return Retorna el documento con una firma.

     * @throws ParserConfigurationException

     * @throws IOException

     * @throws SAXException

     * @throws XMLSecurityException

     */

    public static byte[] firmarDsig(byte[] datos, PrivateKey priv, X509Certificate... cert) throws ParserConfigurationException, IOException, SAXException, XMLSecurityException {

        ElementProxy.setDefaultPrefix(Constants.SignatureSpecNS, "");

        Document documento = leerXML(datos);

        Element root = (Element) documento.getFirstChild();

        documento.setXmlStandalone(false);

        XMLSignature signature = new XMLSignature(documento, null, XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256);

        root.appendChild(signature.getElement());

        Transforms transforms = new Transforms(documento);

        transforms.addTransform(Transforms.TRANSFORM_ENVELOPED_SIGNATURE);

        transforms.addTransform(Transforms.TRANSFORM_C14N_WITH_COMMENTS);

        signature.addDocument("", transforms, MessageDigestAlgorithm.ALGO_ID_DIGEST_SHA256);

        if (cert != null) {

            signature.addKeyInfo(cert[0]);

        }

        signature.sign(priv);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        XMLUtils.outputDOMc14nWithComments(documento, baos);

        return baos.toString().getBytes();

    }

    public static Document leerXML(byte datos[]) throws ParserConfigurationException, IOException, SAXException {

        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();

        DocumentBuilder builder;

 

        factory.setNamespaceAware(true);

        builder = factory.newDocumentBuilder();

        return builder.parse(new ByteArrayInputStream(datos));

    }

    private static String getKey(String filename) throws IOException {

        // Read key from file

        String strKeyPEM = "";

        BufferedReader br = new BufferedReader(new FileReader(filename));

        String line;

        while ((line = br.readLine()) != null) {

            strKeyPEM += line + "\n";

        }

        br.close();

        return strKeyPEM;

    }

    public static RSAPrivateKey getPrivateKey(String filename) throws IOException, GeneralSecurityException {

        String privateKeyPEM = getKey(filename);

        return getPrivateKeyFromString(privateKeyPEM);

    }

    public static RSAPrivateKey getPrivateKeyFromString(String key) throws IOException, GeneralSecurityException {

        String privateKeyPEM = key;

        privateKeyPEM = privateKeyPEM.replace("-----BEGIN PRIVATE KEY-----\n", "");

        privateKeyPEM = privateKeyPEM.replace("-----END PRIVATE KEY-----", "");

        byte[] encoded = Base64.decodeBase64(privateKeyPEM);

        KeyFactory kf = KeyFactory.getInstance("RSA");

        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);

        RSAPrivateKey privKey = (RSAPrivateKey) kf.generatePrivate(keySpec);

        return privKey;

    }

    public static RSAPublicKey getPublicKey(String filename) throws IOException, GeneralSecurityException {

        String publicKeyPEM = getKey(filename);

        return getPublicKeyFromString(publicKeyPEM);

    }

    public static RSAPublicKey getPublicKeyFromString(String key) throws IOException, GeneralSecurityException {

        String publicKeyPEM = key;

        publicKeyPEM = publicKeyPEM.replace("-----BEGIN PUBLIC KEY-----\n", "");

        publicKeyPEM = publicKeyPEM.replace("-----END PUBLIC KEY-----", "");

        byte[] encoded = Base64.decodeBase64(publicKeyPEM);

        KeyFactory kf = KeyFactory.getInstance("RSA");

        RSAPublicKey pubKey = (RSAPublicKey) kf.generatePublic(new X509EncodedKeySpec(encoded));

        return pubKey;

    }   

    public static X509Certificate getX509Certificate(String filename) throws IOException, CertificateException

    {

        CertificateFactory fact = CertificateFactory.getInstance("X.509");

        FileInputStream is = new FileInputStream (filename);

        X509Certificate cer = (X509Certificate) fact.generateCertificate(is);

        PublicKey key = cer.getPublicKey();

       

        return cer;

    }

}

 

Para probar el código se tiene el siguiente Test:


TEST

@Test

    public void firmarXML() throws URISyntaxException, ParserConfigurationException, XMLSecurityException, org.xml.sax.SAXException {

        String xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?><facturaElectronicaEstandar> AQUI VA LA FACTURA XML </fin>";

        byte[] datos = xml.getBytes(StandardCharsets.UTF_8);

        try {

            String path = new File(Firmador.class.getProtectionDomain().getCodeSource().getLocation().toURI()).getPath();

            PrivateKey privateKey = Firmador.getPrivateKey(path + "/vladimir_private.pem");

            X509Certificate cert =  Firmador.getX509Certificate(path + "/vladimir.cer");

            byte[] xmlFirmado = Firmador.firmarDsig(datos, privateKey, cert);

            String respuesta = new String(xmlFirmado);

            System.out.println("facturaFirmada: "+respuesta);

        } catch (IOException | GeneralSecurityException ex) {

            Logger.getLogger(FirmadorUnitTest.class.getName()).log(Level.SEVERE, null, ex);

        }

    }

 

En la línea remarcada en verde se debe agregar el documento fiscal electrónico en formato XML. En la línea remarcada en amarillo se configuran las rutas donde se encuentran el certificado digital y la llave privada.

 

En la línea remarcada en celeste se imprime el resultado que es el documento fiscal electrónico firmado.


Etiquetas que se adicionan al XML una vez firmado

<Signature xmlns="http://www.w3.org/2000/09/xmldsig#">

<SignedInfo>

<CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></CanonicalizationMethod>

<SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"></SignatureMethod>

<Reference URI="">

<Transforms>

<Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></Transform>

<Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"></Transform>

</Transforms>

<DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"></DigestMethod>

<DigestValue> </DigestValue>

</Reference>

</SignedInfo>

<SignatureValue>

</SignatureValue>

<KeyInfo>

<X509Data>

<X509Certificate></X509Certificate>

</X509Data>

</KeyInfo>

</Signature>