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

  1. Seguridad: Las llaves privadas nunca salen del servidor

  2. Estabilidad: Entorno controlado y consistente

  3. Compatibility: Mejor soporte para librerías criptográficas

  4. Performance: Más eficiente para operaciones pesadas

  5. SAT compliance: Cumple con los requerimientos del SAT

📦 Instalación de Dependencias

bash
# 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
<?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
<?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
<?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

html
<!-- 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)

text
# 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
SSLRequireSSL

nginx.conf

text
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

AspectoPHPJavaScript
Seguridad✅ Alta❌ Riesgosa
Performance✅ Alta⚠️ Media
Compatibilidad SAT✅ Total❌ Limitada
Control servidor✅ Total❌ Nulo
Facilidad desarrollo✅ Alta⚠️ Media

🚀 Recomendaciones Finales

  1. Usa PHP para todo el proceso de firma

  2. Mantén los PFX en ubicaciones seguras del servidor

  3. Implementa logging de todas las operaciones

  4. Usa HTTPS obligatoriamente

  5. Valida entradas exhaustivamente

  6. Haz backups regularmente de certificados

¿Necesitas que profundice en alguna parte específica o tienes dudas sobre la implementación

Comentarios

Entradas más populares de este blog

firma digitales PKI utilizando solo javascript y node,

convertir un certificado .key a formato PEM utilizando solo la Web Crypto API

Pilares de la firma electronica-pki-RSA