lenincrdva / xades-bes-signer
PHP library to sign and verify XML documents under the XAdES-BES standard, compliant with the SRI (Ecuador) electronic invoicing requirements.
Requires
- php: >=8.1
- ext-dom: *
- ext-json: *
- ext-openssl: *
Requires (Dev)
- phpunit/phpunit: ^10.5 || ^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-13 05:46:06 UTC
README
Librería PHP para firmar y verificar documentos XML bajo el estándar XAdES-BES, compatible con los requisitos de facturación electrónica del SRI de Ecuador (misma estructura que genera la implementación de referencia XAdES4J que el SRI acepta).
Características
- Firma enveloped XAdES-BES sobre la raíz del documento (XML-DSig +
XAdES
QualifyingProperties). - Estructura espejo de la referencia aceptada por el SRI:
- 3 referencias en
SignedInfoen orden:SignedProperties→KeyInfo→ documento. ds:KeyInfocon certificado (ds:X509Certificate) y clave pública (ds:KeyValue/ds:RSAKeyValue).xades:Description= "contenido comprobante" yxades:MimeTypeenDataObjectFormat.- Base64 envuelto a 76 caracteres por línea.
- Namespaces
ds/xadesdeclarados una sola vez ends:Signature.
- 3 referencias en
- SHA-1 por defecto (
rsa-sha1, digestsha1yCertDigestsha1), tal como exige el SRI; SHA-256 disponible de forma opcional. - Verificación exhaustiva: firma RSA sobre
SignedInfo, digest del documento, deSignedPropertiesy deKeyInfo, coincidencia deCertDigest/IssuerSerialy vigencia del certificado. - Soporte de certificados PKCS#12 (
.p12/.pfx) locales. - Interfaz
PrivateKeySignerInterfacepara firmar con HSM/remoto o cualquier otro origen de clave. - Firmado por lotes (
BatchSigner). - CLI:
bin/xades-bes. - Fixtures y validación contra los XSD oficiales del SRI.
Requisitos
- PHP >= 8.1
- Extensiones:
dom,openssl,json - OpenSSL 3.x con el proveedor legacy activado (ver más abajo).
Instalación
composer require lenincrdva/xades-bes-signer
OpenSSL 3.x: activar el proveedor legacy
Importante. Los certificados .p12 emitidos por las entidades
certificadoras ecuatorianas (por ejemplo los tokens del SRI o del BCE)
suelen proteger la clave privada con algoritmos legacy como 3DES o RC2.
OpenSSL 3.x desactiva esos algoritmos por defecto, por lo que
P12CertificateLoader fallará al leer el contenedor con un error similar a:
Could not decrypt PKCS#12 container (wrong password or corrupt file?).
Para poder leer estos certificados es obligatorio activar el proveedor
legacy de OpenSSL. Incluimos un fichero de configuración listo para usar en
docker/openssl.cnf:
openssl_conf = openssl_init [openssl_init] providers = provider_sect [provider_sect] default = default_sect legacy = legacy_sect [default_sect] activate = 1 [legacy_sect] activate = 1
Docker
Monta el fichero sobre la configuración de OpenSSL del contenedor PHP y reinicia el servicio:
services: php: build: . volumes: - ./docker/openssl.cnf:/etc/ssl/openssl.cnf:ro
Las imágenes PHP oficiales con OpenSSL compilado de forma estática leen la configuración desde una ruta predefinida (p. ej.
/ssl/openssl.cnf), que puede no existir. En ese caso monta el fichero también ahí:volumes: - ./docker/openssl.cnf:/ssl/openssl.cnf:roTen en cuenta que una variable de entorno
OPENSSL_CONFestablecida dentro del proceso PHP puede llegar demasiado tarde cuando OpenSSL ya inicializó sus proveedores; el montaje del fichero en la ruta esperada es el método más fiable.
Host local
Apunta OPENSSL_CONF al fichero antes de arrancar el proceso PHP:
export OPENSSL_CONF=/ruta/a/docker/openssl.cnf
php -S localhost:8000
Si usas PHP-FPM o Apache, exporta la variable en la configuración del servicio y reinícialo.
Comprobación
php -r 'var_dump(openssl_get_provider_configs());'
Debe aparecer el proveedor legacy. También puedes verificar la lectura
directa de tu certificado:
php -r 'require "vendor/autoload.php"; $s = \XadesBesSigner\Certificate\P12CertificateLoader::fromFile("token.p12", "clave"); echo $s->getCertificate()->getCommonName(), PHP_EOL;'
Uso rápido
Firmar un documento
<?php require 'vendor/autoload.php'; use XadesBesSigner\Certificate\P12CertificateLoader; use XadesBesSigner\Signature\SignatureContext; use XadesBesSigner\Signer; $key = P12CertificateLoader::fromFile('token.p12', 'clave-de-token'); // SHA-1 por defecto (compatible con el SRI). // Para SHA-256: new SignatureContext(DigestCalculator::SHA256); $context = new SignatureContext(); $signer = new Signer($key); $signedXml = $signer->signFromFile('factura.xml', $context); file_put_contents('factura-firmada.xml', $signedXml);
Verificar un documento firmado
<?php require 'vendor/autoload.php'; use XadesBesSigner\Verification\Verifier; $result = (new Verifier())->verifyFromFile('factura-firmada.xml'); if ($result->isValid()) { echo 'Firma válida: ' . $result->getSignerCommonName() . PHP_EOL; echo 'Fecha: ' . $result->getSigningTime()?->format('c') . PHP_EOL; } else { foreach ($result->getErrors() as $error) { echo "- {$error}" . PHP_EOL; } }
Algoritmo de firma
| Algoritmo | Digest | SignatureMethod | Uso |
|---|---|---|---|
sha1 (default) |
sha1 |
rsa-sha1 |
Compatibilidad total con el SRI |
sha256 |
sha256 |
rsa-sha256 |
Alternativa más fuerte |
Selección explícita:
use XadesBesSigner\Xml\DigestCalculator; $key = P12CertificateLoader::fromFile('token.p12', 'clave', DigestCalculator::SHA256); $context = new SignatureContext(DigestCalculator::SHA256);
Nota: el algoritmo debe ser el mismo al cargar la clave y al crear el
SignatureContext, porque el algoritmo de firma RSA se toma delSignatureMethody debe coincidir con el que usó el proveedor de clave.
Firmado por lotes
use XadesBesSigner\BatchSigner; use XadesBesSigner\Certificate\P12CertificateLoader; $key = P12CertificateLoader::fromFile('token.p12', 'clave'); $batch = BatchSigner::with($key); // Firma todos los *.xml del directorio; cada archivo se escribe como // <nombre>-signed.xml en la carpeta de salida. $results = $batch->signDirectory('facturas/', 'facturas/firmadas/');
Firmado remoto / HSM
P12CertificateLoader devuelve un PrivateKeySignerInterface. Para usar un
HSM, un API o un KMS, implementa la misma interfaz con tu token o servicio:
use XadesBesSigner\Certificate\Certificate; use XadesBesSigner\KeyProvider\PrivateKeySignerInterface; final class HsmSigner implements PrivateKeySignerInterface { public function sign(string $data): string { // Delega en tu HSM/API: `$data` es el SignedInfo canónico (bytes). } public function getCertificate(): Certificate { // Certificado del token, p. ej. Certificate::fromPem(...). } }
El pipeline de firma y verificación es idéntico al modo local.
CLI
# Firmar un archivo (default SHA-1); genera <nombre>-signed.xml bin/xades-bes sign token.p12 clave factura.xml # Firmar todos los XML de un directorio bin/xades-bes sign token.p12 clave facturas/ facturas/firmadas/ # Firmar con SHA-256 bin/xades-bes sign --sha256 token.p12 clave factura.xml # Verificar bin/xades-bes verify factura-firmada.xml
Pruebas
composer install composer fixtures:generate # genera tests/fixtures/generated/test-cert.p12 composer test
Compatibilidad con el SRI
La estructura generada es la misma que produce la implementación de referencia XAdES4J aceptada por el SRI:
SignedInfocon 3 referencias en orden exacto:SignedProperties,KeyInfoy documento.ds:KeyInfoconId, certificado embebido yKeyValue/RSAKeyValue.xades:Description=contenido comprobanteyxades:MimeType=text/xml.- Base64 a 76 columnas y namespaces normalizados.
- SHA-1 en firma, digest y
CertDigest.
Licencia
MIT