Firma Digital con PHP para México (FIEL/SAT)
Firma Digital con PHP para México (FIEL/SAT)
Sí, PHP es MUCHO más recomendable que JavaScript para manejar firmas digitales con FIEL porque:
✅ Ventajas de PHP sobre JavaScript
Seguridad: Las llaves privadas nunca salen del servidor
Estabilidad: Entorno controlado y consistente
Compatibility: Mejor soporte para librerías criptográficas
Performance: Más eficiente para operaciones pesadas
SAT compliance: Cumple con los requerimientos del SAT
📦 Instalación de Dependencias
# Extensiones PHP necesarias
sudo apt-get install php openssl php-xml php-zip
# Verificar instalación
php -m | grep openssl
php -m | grep xml🛠️ Implementación Completa en PHP
1. Clase principal para manejo de FIEL
<?php
// FirmaElectronica.php
class FirmaElectronica {
private $certificado;
private $llavePrivada;
private $password;
private $rutaPfx;
public function __construct($rutaPfx, $password) {
$this->rutaPfx = $rutaPfx;
$this->password = $password;
$this->cargarCertificado();
}
private function cargarCertificado() {
if (!file_exists($this->rutaPfx)) {
throw new Exception("Archivo PFX no encontrado: " . $this->rutaPfx);
}
// Leer contenido del PFX
$pfxContent = file_get_contents($this->rutaPfx);
// Abrir el certificado
if (!openssl_pkcs12_read($pfxContent, $certInfo, $this->password)) {
throw new Exception("Error al leer PFX: Contraseña incorrecta o archivo inválido");
}
$this->certificado = $certInfo['cert'];
$this->llavePrivada = $certInfo['pkey'];
}
public function getCertificado() {
return $this->certificado;
}
public function getLlavePrivada() {
return $this->llavePrivada;
}
public function getNoCertificado() {
$certificado = openssl_x509_read($this->certificado);
$info = openssl_x509_parse($certificado);
return $info['serialNumber'] ?? '';
}
public function getCertificadoBase64() {
$certificado = $this->getCertificado();
$certificado = preg_replace('/-----(BEGIN|END) CERTIFICATE-----/', '', $certificado);
$certificado = preg_replace('/\s+/', '', $certificado);
return $certificado;
}
public function firmarCadena($cadena) {
$firma = null;
// Firmar la cadena
openssl_sign($cadena, $firma, $this->llavePrivada, OPENSSL_ALGO_SHA256);
return base64_encode($firma);
}
public function verificarFirma($cadena, $firma) {
$firmaBinaria = base64_decode($firma);
$resultado = openssl_verify($cadena, $firmaBinaria, $this->certificado, OPENSSL_ALGO_SHA256);
return $resultado === 1;
}
}
?>2. Clase específica para CFDI (Facturación SAT)
<?php
// FirmadorCFDI.php
class FirmadorCFDI {
private $firmaElectronica;
public function __construct(FirmaElectronica $firmaElectronica) {
$this->firmaElectronica = $firmaElectronica;
}
public function firmarXML($xmlContent) {
$dom = new DOMDocument();
$dom->loadXML($xmlContent);
// Generar cadena original (simplificado)
$cadenaOriginal = $this->generarCadenaOriginal($dom);
// Firmar la cadena original
$firma = $this->firmaElectronica->firmarCadena($cadenaOriginal);
// Insertar firma en el XML
$this->insertarFirmaXML($dom, $firma);
return $dom->saveXML();
}
private function generarCadenaOriginal(DOMDocument $dom) {
// Aquí implementarías la transformación XSLT específica del SAT
// Esto es un ejemplo simplificado
$xslt = new XSLTProcessor();
$xsl = new DOMDocument();
$xsl->load('cadenaoriginal_3_3.xslt'); // XSLT del SAT
$xslt->importStylesheet($xsl);
return $xslt->transformToXml($dom);
}
private function insertarFirmaXML(DOMDocument $dom, $firma) {
$comprobante = $dom->getElementsByTagName('Comprobante')->item(0);
// Crear elemento Complemento
$complemento = $dom->createElement('cfdi:Complemento');
// Crear elemento TimbreFiscalDigital (simplificado)
$tfd = $dom->createElement('tfd:TimbreFiscalDigital');
$tfd->setAttribute('Version', '1.1');
$tfd->setAttribute('UUID', $this->generarUUID());
$tfd->setAttribute('FechaTimbrado', date('Y-m-d\TH:i:s'));
$tfd->setAttribute('selloCFD', $firma);
$tfd->setAttribute('noCertificadoSAT', $this->firmaElectronica->getNoCertificado());
$tfd->setAttribute('selloSAT', $firma);
$complemento->appendChild($tfd);
$comprobante->appendChild($complemento);
}
private function generarUUID() {
return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000,
mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
}
}
?>3. Controlador para manejar peticiones web
<?php
// firmar.php
require_once 'FirmaElectronica.php';
require_once 'FirmadorCFDI.php';
header('Content-Type: application/json');
try {
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
throw new Exception('Método no permitido');
}
// Obtener archivos y datos
$archivoPfx = $_FILES['pfx']['tmp_name'] ?? null;
$password = $_POST['password'] ?? '';
$xmlContent = $_POST['xml'] ?? '';
if (!$archivoPfx || !$password || !$xmlContent) {
throw new Exception('Datos incompletos');
}
// Mover PFX a ubicación segura
$rutaPfxSegura = '/ruta/segura/' . uniqid() . '.pfx';
move_uploaded_file($archivoPfx, $rutaPfxSegura);
// Inicializar firma electrónica
$firma = new FirmaElectronica($rutaPfxSegura, $password);
$firmador = new FirmadorCFDI($firma);
// Firmar XML
$xmlFirmado = $firmador->firmarXML($xmlContent);
// Limpiar archivo temporal
unlink($rutaPfxSegura);
// Devolver resultado
echo json_encode([
'success' => true,
'xmlFirmado' => $xmlFirmado,
'noCertificado' => $firma->getNoCertificado()
]);
} catch (Exception $e) {
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
?>4. Formulario HTML seguro
<!-- form-firma.html -->
<!DOCTYPE html>
<html>
<head>
<title>Firma Digital CFDI</title>
</head>
<body>
<h2>Firmar CFDI con FIEL</h2>
<form id="formFirma" enctype="multipart/form-data">
<div>
<label>Archivo PFX/FIEL:</label>
<input type="file" name="pfx" accept=".pfx,.p12" required>
</div>
<div>
<label>Contraseña:</label>
<input type="password" name="password" required>
</div>
<div>
<label>XML a firmar:</label>
<textarea name="xml" rows="10" cols="50" required></textarea>
</div>
<button type="submit">Firmar Documento</button>
</form>
<div id="resultado"></div>
<script>
document.getElementById('formFirma').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
try {
const response = await fetch('/firmar.php', {
method: 'POST',
body: formData
});
const resultado = await response.json();
if (resultado.success) {
document.getElementById('resultado').innerHTML = `
<h3>XML Firmado Exitosamente</h3>
<textarea rows="10" cols="50" readonly>${resultado.xmlFirmado}</textarea>
<p>Número de certificado: ${resultado.noCertificado}</p>
`;
} else {
alert('Error: ' + resultado.error);
}
} catch (error) {
alert('Error de conexión: ' + error.message);
}
});
</script>
</body>
</html>🔒 Configuración de Seguridad en Apache/Nginx
.htaccess (Apache)
# Prevenir acceso directo a archivos PFX
<Files ~ "\.(pfx|p12)$">
Order allow,deny
Deny from all
</Files>
# Limitar tamaño de upload
php_value upload_max_filesize 10M
php_value post_max_size 10M
# SSL obligatorio
SSLRequireSSLnginx.conf
server {
listen 443 ssl;
server_name tu-dominio.com;
ssl_certificate /ruta/certificado.crt;
ssl_certificate_key /ruta/llave-privada.key;
# Seguridad adicional
location ~ \.(pfx|p12)$ {
deny all;
return 403;
}
client_max_body_size 10M;
}📊 Comparativa PHP vs JavaScript
| Aspecto | PHP | JavaScript |
|---|---|---|
| Seguridad | ✅ Alta | ❌ Riesgosa |
| Performance | ✅ Alta | ⚠️ Media |
| Compatibilidad SAT | ✅ Total | ❌ Limitada |
| Control servidor | ✅ Total | ❌ Nulo |
| Facilidad desarrollo | ✅ Alta | ⚠️ Media |
🚀 Recomendaciones Finales
Usa PHP para todo el proceso de firma
Mantén los PFX en ubicaciones seguras del servidor
Implementa logging de todas las operaciones
Usa HTTPS obligatoriamente
Valida entradas exhaustivamente
Haz backups regularmente de certificados
¿Necesitas que profundice en alguna parte específica o tienes dudas sobre la implementación
Comentarios
Publicar un comentario