- Método de firmado en VB.Net
- Método de firmado en TypeScript
- Método de firmado en Java
- Método de firmado en PHP
///
/// Método utilizado para el firmado de XML con un certificado digital.
///
///XML a firmar.
///Ubicacion del certificacion digital
///Contraseña del certificado digital
///
static XmlDocument Signed(XmlDocument xmlDoc, string pathCert, string passCert)
{
try
{
if (!File.Exists(pathCert)) throw new Exception("El certificado para firma no existe");
var cert = new X509Certificate2(pathCert, passCert, X509KeyStorageFlags.Exportable);
var exportedKeyMaterial = cert.PrivateKey.ToXmlString(true);
var key = new RSACryptoServiceProvider(new CspParameters(24));
key.PersistKeyInCsp = false;
key.FromXmlString(exportedKeyMaterial);
SignedXml signedXml = new SignedXml(xmlDoc);
signedXml.SigningKey = key;
signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
// Se agrega la referencia del algoritmo de firma utilizado.
Reference reference = new Reference();
reference.AddTransform(new XmlDsigEnvelopedSignatureTransform());
reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256";
reference.Uri = "";
signedXml.AddReference(reference);
// Se agrega la información del certificado utilizado para la firma.
KeyInfo keyInfo = new KeyInfo();
keyInfo.AddClause(new KeyInfoX509Data(cert));
signedXml.KeyInfo = keyInfo;
// Generate the signature.
signedXml.ComputeSignature();
//Obtenemos la representación del XML firmado y la guardamos en un XmlElement object
XmlElement xmlFirmaDigital = signedXml.GetXml();
//Adicionamos el elemento de la firma al documento XML.
xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(xmlFirmaDigital, true));
return xmlDoc;
}
catch (Exception ex)
{
throw ex;
}
}
El método de firma a través de VB.net queda de la siguiente manera:
Signed (ByVal xmlDoc As XmlDocument, ByVal pathCert As String, ByVal
passCert As String) As XmlDocument
Try
If Not File.Exists(pathCert) Then Throw New Exception("El certificado para firma no existe")
Dim cert = New X509Certificate2(pathCert, passCert, X509KeyStorageFlags.Exportable)
Dim exportedKeyMaterial = cert.PrivateKey.ToXmlString(True)
Dim key = New RSACryptoServiceProvider(New CspParameters(24))
key.PersistKeyInCsp = False
key.FromXmlString(exportedKeyMaterial)
Dim signedXml As SignedXml = New SignedXml(xmlDoc)
signedXml.SigningKey = key
signedXml.SignedInfo.SignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
Dim reference As Reference = New Reference()
reference.AddTransform(New XmlDsigEnvelopedSignatureTransform())
reference.DigestMethod = "http://www.w3.org/2001/04/xmlenc#sha256"
reference.Uri = ""
signedXml.AddReference(reference)
Dim keyInfo As KeyInfo = New KeyInfo()
keyInfo.AddClause(New KeyInfoX509Data(cert))
signedXml.KeyInfo = keyInfo
signedXml.ComputeSignature()
Dim xmlFirmaDigital As XmlElement = signedXml.GetXml()
xmlDoc.DocumentElement.AppendChild(xmlDoc.ImportNode(xmlFirmaDigital, True))
Return xmlDoc
Catch ex As Exception
Throw ex
End Try
End Function
El método de firma utilizando TypeSript queda de la siguiente manera:
import { Injectable } from '@angular/core';
import * as forge from 'node-forge';
import * as xmldom from 'xmldom';
/*Referencias de librerias
Node-forge: https://www.npmjs.com/package/node-forge#pkcs12
Xmldom: https://www.npmjs.com/package/xmldom
P12-pem:https://www.npmjs.com/package/p12-pem
// Special character normalization:
// - https://www.w3.org/TR/xml-c14n#ProcessingModel (Attribute / Text)
// - https://www.w3.org/TR/xml-c14n#Example-Chars
Estas librerias fueron probadas Angular CLI v.10.0.8.
Note.js v.14.16.0
*/
@Injectable({
providedIn: 'root'
})
export class FirmaXMLService {
constructor() { }
/* El parametro xml:un string formateado en xml:
("E310000000001 ").
El parametro certificado: tipo ArrayBuffer, puede provenir de un control fileUpload.
El Password:es el valor de la clave del certificado.
*/
//Método utilizado para generar la firma del xml.
public Firmar(xml:any,certifado: ArrayBuffer,password:string) {
//Se Genera el PEM & PrivateKey del certificado
let pem = this.convertToPem(certifado, password);
let privateKey = forge.pki.privateKeyFromPem(pem.pemKey);
//-----------------------------------------------------------
//Se prepara la encriptacion del contenido del xml
let md = forge.md.sha256.create();
var xmlData2 = (new xmldom.DOMParser()).parseFromString(xml);
//Canolizacion del tag SignedInfo para poder generar correctamente la firma del documento.
var xmlCanolizadoData2=this.c14nCanonicalization(xmlData2.firstChild,null);
md.update(xmlCanolizadoData2.toString(), 'utf8');
//------------------------------------------------------------
/* Se obtiene solo el contenido del en formato encode64
para luego asignar este valor a el tag DigestValue
*/
let messageDigest = forge.util.encode64(md.digest().data);
//------------------------------------------------------------
//Se construye el xml con el tag de firma integrado en este punto aun no se genera el tag
SignatureValue
let pemXMLFormat=this.obtenerPEMXMLFormat(pem.pemCertificate);
let xmlSinFirmado=
this.agregarEstructuraFirma(xmlCanolizadoData2.toString(),pemXMLFormat,messageDigest);
/*Se convierte a formato XML la transformacion anterior. Esta transformacion es necesaria
para poder obtener la canolizacion del XML.
*/
var xmlData = (new xmldom.DOMParser()).parseFromString(xmlSinFirmado);
//Canolizacion del tag SignedInfo para poder generar correctamente la firma del documento.
var
xmlCanolizado=this.c14nCanonicalization(xmlData.getElementsByTagName("SignedInfo")[0],{defau
ltNsForPrefix:{ds: 'http://www.w3.org/2000/09/xmldsig#'}});
//Generación de firma,
md = forge.md.sha256.create();
md.update(xmlCanolizado.toString(), 'utf8');
let signature = privateKey.sign(md);
//-----------------------------------------------------------------------------
//Agregando firma a el xml generado con el formato de firma.
let signatureValue = '' + forge.util.encode64(signature) + ' ';
let indiceFirma = xmlSinFirmado.search('');
let xmlFirmado = xmlSinFirmado.substring(0, indiceFirma) + signatureValue +
xmlSinFirmado.substring(indiceFirma);
let resultadoFirma={
'xmlFirmadoString':xmlFirmado,
'xmlFirmadoBlob':this.generarBlobArchivoFirmado(xmlFirmado),
}
return resultadoFirma;
}
// Método que recibe un xml y retorna un archivo blob.
private generarBlobArchivoFirmado(xmlFirmado:string)
{
let blob = new Blob([xmlFirmado],
{ type: "text/xml;charset=utf-8" });
return blob;
}
//Sirve para obtener el formato del Pem
private obtenerPEMXMLFormat(PEM:any)
{
let PEMToXmlFormat = PEM.replace('-----BEGIN CERTIFICATE-----', '');
PEMToXmlFormat = PEMToXmlFormat.replace('-----END CERTIFICATE-----', '');
return PEMToXmlFormat;
}
//Permite generar la estructura del area de la firma en formato de la estructura del xml
private agregarEstructuraFirma(xml:string,PEM:any,digestValue:any)
{
let signatureXmlFormat = '';
let keyinfoXmlFormat = '' +
'' +
'' + PEM + ' ' +
' ' +
' ';
let signedInfoXmlFormat = ' '+
' '+
''+
''+
' '+
' '+
' '+
'' + digestValue + ' '+
' '+
' ';
let firmado = signatureXmlFormat + signedInfoXmlFormat + keyinfoXmlFormat + ' ';
let indice = xml.search ('');
let xmlSinFirmado = xml.substring(0, indice) + firmado + xml.substring(indice);
return xmlSinFirmado;
}
//Permite convertir a Pem
private convertToPem(p12ArrayBuffer, password) {
let p12Asn1 = forge.asn1.fromDer(p12ArrayBuffer);
let p12 = forge.pkcs12.pkcs12FromAsn1(p12Asn1, false, password);
let pemKey = this.getKeyFromP12(p12, password);
let _a = this.getCertificateFromP12(p12), pemCertificate = _a.pemCertificate, commonName =
_a.commonName;
return { pemKey: pemKey, pemCertificate: pemCertificate, commonName: commonName };
}
// Obtiene la llave de un p12
private getKeyFromP12(p12, password) {
var keyData = p12.getBags({ bagType: forge.pki.oids.pkcs8ShroudedKeyBag },
password);
var pkcs8Key = keyData[forge.pki.oids.pkcs8ShroudedKeyBag][0];
if (typeof pkcs8Key === 'undefined') {
pkcs8Key = keyData[forge.pki.oids.keyBag][0];
}
if (typeof pkcs8Key === 'undefined') {
throw new Error('Unable to get private key.');
}
var pemKey = forge.pki.privateKeyToPem(pkcs8Key.key);
pemKey = pemKey.replace(/\r\n/g, '');
return pemKey;
}
// Para obtener el certificado de un p12
private getCertificateFromP12(p12) {
var certData = p12.getBags({ bagType: forge.pki.oids.certBag });
var certificate = certData[forge.pki.oids.certBag][0];
var pemCertificate = forge.pki.certificateToPem(certificate.cert);
pemCertificate = pemCertificate.replace(/\r\n/g, '');
var commonName = certificate.cert.subject.attributes[0].value;
return { pemCertificate: pemCertificate, commonName: commonName };
}
// Utilizado para poder realizar la canonicalización de etiquetas del xml.
private c14nCanonicalization = function (node, options) {
options = options || {};
var defaultNs = options.defaultNs || "";
var defaultNsForPrefix = options.defaultNsForPrefix || {};
var ancestorNamespaces = options.ancestorNamespaces || [];
var res = this.c14nCanonicalizationInterno(node, [], defaultNs, defaultNsForPrefix,
ancestorNamespaces);
return res;
};
private c14nCanonicalizationInterno = function (node, prefixesInScope, defaultNs,
defaultNsForPrefix, ancestorNamespaces) {
if (node.nodeType === 8) { return this.renderComment(node); }
if (node.data) { return this.encodeSpecialCharactersInText(node.data); }
var i, pfxCopy
, ns = this.renderNs(node, prefixesInScope, defaultNs, defaultNsForPrefix,
ancestorNamespaces)
, res = ["<", node.tagName, ns.rendered, this.renderAttrs(node, ns.newDefaultNs), ">"];
for (i = 0; i < node.childNodes.length; ++i) {
pfxCopy = prefixesInScope.slice(0);
res.push(this.c14nCanonicalizationInterno(node.childNodes[i], pfxCopy, ns.newDefaultNs,
defaultNsForPrefix, []));
}
res.push("", node.tagName, ">");
return res.join("");
};
private renderComment = function (node) {
if (!this.includeComments) { return ""; }
var isOutsideDocument = (node.ownerDocument === node.parentNode),
isBeforeDocument = null,
isAfterDocument = null;
if (isOutsideDocument) {
var nextNode = node,
previousNode = node;
while (nextNode !== null) {
if (nextNode === node.ownerDocument.documentElement) {
isBeforeDocument = true;
break;
}
nextNode = nextNode.nextSibling;
}
while (previousNode !== null) {
if (previousNode === node.ownerDocument.documentElement) {
isAfterDocument = true;
break;
}
previousNode = previousNode.previousSibling;
}
}
return (isAfterDocument ? "\n" : "") + "" + (isBeforeDocument ? "\n" : "");
};
private renderAttrs = function(node, defaultNS) {
var a, i, attr
, res = []
, attrListToRender = [];
if (node.nodeType===8) { return this.renderComment(node); }
if (node.attributes) {
for (i = 0; i < node.attributes.length; ++i) {
attr = node.attributes[i];
// Para ignorar los atributos de definición del espacio de nombres.
if (attr.name.indexOf("xmlns") === 0) { continue; }
attrListToRender.push(attr);
}
}
attrListToRender.sort(this.attrCompare);
for (a in attrListToRender) {
if (!attrListToRender.hasOwnProperty(a)) { continue; }
attr = attrListToRender[a];
res.push(" ", attr.name, '="', this.encodeSpecialCharactersInAttribute(attr.value), '"');
}
return res.join("");
};
private renderNs = function(node, prefixesInScope, defaultNs, defaultNsForPrefix,
ancestorNamespaces) {
var a, i, p, attr
, res = []
, newDefaultNs = defaultNs
, nsListToRender = []
, currNs = node.namespaceURI || "";
// Utilizado para manejar el espacio de nombres del propio nodo
if (node.prefix) {
if (prefixesInScope.indexOf(node.prefix)==-1) {
nsListToRender.push({"prefix": node.prefix, "namespaceURI": node.namespaceURI ||
defaultNsForPrefix[node.prefix]});
prefixesInScope.push(node.prefix);
}
}
else if (defaultNs!=currNs) {
//nuevo por defecto ns.
newDefaultNs = node.namespaceURI;
res.push(' xmlns="', newDefaultNs, '"');
}
// Utilizado para manejar el espacio de nombres de los atributos.
if (node.attributes) {
for (i = 0; i < node.attributes.length; ++i) {
attr = node.attributes[i];
// Para manejar todos los atributos prefijados que están incluidos en la lista de prefijos y
donde el prefijo aún no está definido.
// Los nuevos prefijos solo se pueden definir mediante `xmlns:`.
if (attr.prefix === "xmlns" && prefixesInScope.indexOf(attr.localName) === -1) {
nsListToRender.push({"prefix": attr.localName, "namespaceURI": attr.value});
prefixesInScope.push(attr.localName);
}
//Manejar todos los atributos prefijados que no son definiciones xmlns.
//Donde el prefijo no está definido .
if (attr.prefix && prefixesInScope.indexOf(attr.prefix)==-1 && attr.prefix!="xmlns" &&
attr.prefix!="xml") {
nsListToRender.push({"prefix": attr.prefix, "namespaceURI": attr.namespaceURI});
prefixesInScope.push(attr.prefix);
}
}
}
if(Array.isArray(ancestorNamespaces) && ancestorNamespaces.length > 0){
// Eliminar espacios de nombres que ya están presentes en nsListToRender
for(var p1 in ancestorNamespaces){
if(!ancestorNamespaces.hasOwnProperty(p1)) continue;
var alreadyListed = false;
for(var p2 in nsListToRender){
if(nsListToRender[p2].prefix === ancestorNamespaces[p1].prefix
&& nsListToRender[p2].namespaceURI === ancestorNamespaces[p1].namespaceURI)
{
alreadyListed = true;
}
}
if(!alreadyListed){
nsListToRender.push(ancestorNamespaces[p1]);
}
}
}
nsListToRender.sort(this.nsCompare);
//renderizar espacios de nombres
for (a in nsListToRender) {
if (!nsListToRender.hasOwnProperty(a)) { continue; }
p = nsListToRender[a];
res.push(" xmlns:", p.prefix, '="', p.namespaceURI, '"');
}
return {"rendered": res.join(""), "newDefaultNs": newDefaultNs};
};
// Obtiene el texto de caracteres encodeados
private static get_xml_special_to_encoded_text(attributeValue) {
var xml_special_to_encoded_attribute = {
'&': '&',
'<': '<',
'"': '"',
'\r': '',
'\n': '',
'\t': ' ',
"'" : ''',
'>' : '>'
}
return xml_special_to_encoded_attribute[attributeValue];
};
// Encodear caracteres especiales en texto
private encodeSpecialCharactersInText(text) {
var _text = text;
return text
.replace(/([&<>\r])/g, function (str, item) {
return FirmaXMLService.get_xml_special_to_encoded_text(item)
})
};
// Encodear caracteres especiales en atributos
private encodeSpecialCharactersInAttribute(attributeValue) {
return attributeValue
.replace(/([&<"\r\n\t])/g, function (str, item) {
return FirmaXMLService.get_xml_special_to_encoded_text(item)
})
};
El método de firma utilizando Java queda de la siguiente manera:
package dgii.lib;
import java.io.InputStream;
import java.io.Serializable;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory;
import javax.xml.crypto.dsig.keyinfo.X509Data;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import org.w3c.dom.Element;
//Ref:https://repo1.maven.org/maven2/com/oracle/database/xml/xmlparserv2/21.8.0.0/xmlparserv2-21.8.0.0.jar
import oracle.xml.parser.v2.DOMParser;
import oracle.xml.parser.v2.XMLDocument;
//Fin Ref
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureFactory;
/*
* Referencias
* JDK
https://www.oracle.com/java/technologies/javase/javase8u211-later-archive-downloads.html
*
* https://community.oracle.com/tech/developers/discussion/1539728/digital-signing-validating
*
* https://docs.oracle.com/javase/7/docs/api/javax/xml/parsers/DocumentBuilderFactory.html
*
* https://www.oracle.com/technical-resources/articles/wang-whitespace.html
*
* Parser XML v2-21.8.0.0
* xmlparserv2-21.8.0.0.jar =>
https://repo1.maven.org/maven2/com/oracle/database/xml/xmlparserv2/21.8.0.0/xmlparserv2-
21.8.0.0.jar
* https://repo1.maven.org/maven2/com/oracle/database/xml/xmlparserv2/
* Si usa VS Code como entorno para desarrollar estas extensiones serán de utilidad
*
* https://code.visualstudio.com/docs/java/extensions
* https://code.visualstudio.com/docs/java/java-tutorial
* Solo instalar el extensionPack for Java y el Coding Pack for Java para el JDK recomendamos
Oracle Java SE
*/
public class SignManager{
private static final String algoritmoSignatureMethod = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256";
/*
* Método para el firmado un XML utilizando un certificado digital (.p12)
* @param streamXML InputStream del xml que será firmado.
* @param pathCerticate ruta del certificado digital (archivo extension .p12)
* @param passwordCertificate contraseña del certificado digital
* @param pathFirmado Ruta donde sera almacenado el XML firmado
* @throws Exception
*/
public Element SignXML(InputStream streamXML, InputStream streamCerticate,String
passwordCertificate) throws Exception
{
try {
// Creamos un DOM XMLSignatureFactory. Se usará para generar la firma
XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");
Reference ref = fac.newReference("", fac.newDigestMethod(DigestMethod.SHA256, null),
Collections.singletonList(fac.newTransform(Transform.ENVELOPED,
(TransformParameterSpec) null)),
null,
null);
// Creamos el objeto SignedInfo
SignedInfo si =
fac.newSignedInfo(fac.newCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE,
(C14NMethodParameterSpec) null),
fac.newSignatureMethod(algoritmoSignatureMethod, null),
Collections.singletonList(ref));
// Cargamos el archivo p12 desde disco
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(streamCerticate, passwordCertificate.toCharArray());
String param = ks.aliases().nextElement();
// Extraemos los datos del archivo p12
KeyStore.PrivateKeyEntry keyEntry = (KeyStore.PrivateKeyEntry) ks
.getEntry(param, new
KeyStore.PasswordProtection(passwordCertificate.toCharArray()));
X509Certificate cert = (X509Certificate) keyEntry.getCertificate();
KeyInfoFactory kinfoFactory = fac.getKeyInfoFactory();
// Cargamos nuestro certificado
List x509Content = new ArrayList();
x509Content.add(cert);
// Objetos para extraer la llave privada
X509Data x509d = kinfoFactory.newX509Data(x509Content);
KeyInfo kinfo = kinfoFactory.newKeyInfo(Collections.singletonList(x509d));
DOMParser parser = new DOMParser();
parser.setPreserveWhitespace(false);
parser.parse(streamXML);
XMLDocument xml =parser.getDocument();
Element xmlRoot = xml.getDocumentElement();
// Crea el contexto de firmma y especifica las llaves privadas
DOMSignContext dsc = new DOMSignContext(keyEntry.getPrivateKey(), xmlRoot);}
// crea los nodos de firmas xml
XMLSignature signature = fac.newXMLSignature(si, kinfo);
// en esta etapa dsc modifica el objeto xmlRoot insertándole la firma
signature.sign(dsc);
return xmlRoot;
} catch (Exception e) {
throw e;
}
}
El método de firma utilizando PHP queda de la siguiente manera:
preserveWhiteSpace = true; cambiar a $xml->preserveWhiteSpace = false;
**Por otro lado
$canonicalData = $element->C14N(true, false); cambiar a $canonicalData =
$element->C14N(false, false);
puede dejarlos sin parámetros puesto que sus valores por defecto son false, es decir puede ser
=> $canonicalData = $element->C14N()
**En la función appendSignature puede comentar las líneas 154 hasta la 170, los tag KeyValue,
RSAKeyValue, Exponent no son necesarios
**Recuerde habilitar la extensión openssl en su archivo php.ini, en algunas distribuciones esta
deshabilitado por defecto.
*/
final class SignManager
{
/**
* The constructor.
*
* @param string $cert_store contenido del archivo p12
* @param string $password contraseña para acceder a la información contenida en el
certificado
* @param string $xml contenido del archivo xml
*/
public function sing(string $cert_store, string $password, string $xml): string
{
if (!openssl_pkcs12_read($cert_store , $certs, $password)) {
echo "Error: No fue posible leer el contenido del certificado.\n";
exit;
}
$pem_file_contents = $certs['cert'] . $certs['pkey'];
$privateKeyStore = new PrivateKeyStore();
$privateKeyStore->loadFromPem($pem_file_contents, $password);
$privateKeyStore->addCertificatesFromX509Pem($pem_file_contents);
$algorithm = new Algorithm(Algorithm::METHOD_SHA256);
$cryptoSigner = new CryptoSigner($privateKeyStore, $algorithm);
$xmlSigner = new XmlSigner($cryptoSigner);
$xmlSigner->setReferenceUri('');
$signedXml = $xmlSigner->signXml($xml);
return $signedXml;
}
}
¿Cuándo entran en vigencia?
El formato de documento de Firmado de Comprobantes Fiscales Electrónicos (e-CF) se encuentra disponible en el portal de la Dirección General de Impuestos Internos para su descarga.
Más información:
¿Tiene dudas sobre estos cambios?
Si quiere ver esta u otras noticias regulatorias dentro de la región, lo invitamos a visitar nuestro Centro de Recursos, o si tiene alguna consulta relacionada con el comunicado que acaba de recibir, por favor escríbanos a info@gosocket.net
Sobre Gosocket: Gosocket es un proveedor líder Latinoamericano que ofrece soluciones de emisión y recepción de documentos electrónicos basados en firma digital, incluyendo factura electrónica. Gosocket funciona bajo un modelo de red empresarial abierta que integra Compradores, Proveedores, Entidades Tributarias y Terceros en un ecosistema que facilita los negocios, genera eficiencias, ofrece servicios de valor agregado para todos los actores de la cadena de suministro y garantiza el cumplimiento de los requisitos técnicos relacionados con la regulación y las normas en materia de los modelos de factura electrónica en el país donde opere su organización.
Aviso de privacidad: Gosocket no se hace responsable por las acciones u omisiones de terceros que puedan implicar responsabilidades ante las autoridades tributarias. La información aquí descrita solo constituye nuestra interpretación de la información publica disponible en el momento de publicación. Si usted desea asesoría personalizada le invitamos a contactar con nuestro equipo de consultoría.