Overview
PHP FacturaE supports digital signing of invoices with XAdES-EPES (XML Advanced Electronic Signatures - Explicit Policy-based Electronic Signatures) compliant with the Spanish FacturaE signing policy v3.1.
Digital signatures:
Prove the invoice’s authenticity
Ensure document integrity (detect tampering)
Provide non-repudiation
Are required for some B2G (business-to-government) invoices
Quick Start
Sign an invoice with a PKCS#12 certificate:
use PhpFacturae\ Invoice ;
use PhpFacturae\ Signer ;
$invoice = Invoice :: create ( '2024-001' )
-> seller ( $seller )
-> buyer ( $buyer )
-> line ( 'Service' , price : 100 , vat : 21 )
-> transferPayment ( 'ES12 3456 7890 1234 5678 9012' )
-> sign ( Signer :: pfx ( 'path/to/certificate.pfx' , 'password' ))
-> export ( 'signed-invoice.xml' );
The signature is applied automatically during export.
Creating Signers
PKCS#12 Certificates (.pfx / .p12)
The most common format for digital certificates:
use PhpFacturae\ Signer ;
// With password
$signer = Signer :: pfx ( 'certificate.pfx' , 'mySecurePassword' );
// Without password (rare)
$signer = Signer :: pfx ( 'certificate.pfx' );
$invoice -> sign ( $signer );
PKCS#12 files bundle:
Private key
Public certificate
Certificate chain (CA certificates)
This is the recommended format for most use cases.
PEM Certificates
For separate certificate and key files:
$signer = Signer :: pem (
certPath : 'certificate.pem' ,
keyPath : 'private-key.pem' ,
passphrase : 'keyPassword' // Optional
);
$invoice -> sign ( $signer );
PEM format requires two separate files :
Certificate file (.crt or .pem)
Private key file (.key or .pem)
If your private key is encrypted, provide the passphrase.
XAdES-EPES Signatures
PHP FacturaE generates XAdES-EPES signatures that include:
XML-DSIG envelope signature (enveloped)
SignedProperties with certificate and policy info
Policy reference to FacturaE signing policy v3.1
Signer role (supplier)
Timestamp (optional, via TSA)
Signature Structure
The generated signature includes:
< ds:Signature >
< ds:SignedInfo >
< ds:Reference URI = "" > <!-- Document digest --> </ ds:Reference >
< ds:Reference URI = "#KeyInfo" > <!-- Certificate digest --> </ ds:Reference >
< ds:Reference URI = "#SignedProperties" > <!-- Properties digest --> </ ds:Reference >
</ ds:SignedInfo >
< ds:SignatureValue > ... </ ds:SignatureValue >
< ds:KeyInfo >
< ds:X509Data >
< ds:X509Certificate > ... </ ds:X509Certificate >
</ ds:X509Data >
</ ds:KeyInfo >
< ds:Object >
< xades:QualifyingProperties >
< xades:SignedProperties >
< xades:SigningTime > 2024-03-09T12:00:00Z </ xades:SigningTime >
< xades:SignaturePolicyIdentifier >
< xades:Description > Política de Firma FacturaE v3.1 </ xades:Description >
</ xades:SignaturePolicyIdentifier >
</ xades:SignedProperties >
</ xades:QualifyingProperties >
</ ds:Object >
</ ds:Signature >
FacturaE Signing Policy
The signer automatically references:
This ensures compliance with Spanish e-invoicing regulations.
Timestamping (TSA)
Add a trusted timestamp to prove when the signature was created:
$signer = Signer :: pfx ( 'certificate.pfx' , 'password' )
-> timestamp ( 'https://freetsa.org/tsr' );
$invoice -> sign ( $signer );
Public TSA
Use a free public timestamp authority: -> timestamp ( 'https://freetsa.org/tsr' )
-> timestamp ( 'http://timestamp.digicert.com' )
Commercial TSA with Authentication
Many commercial TSAs require credentials: -> timestamp (
url : 'https://tsa.example.com/tsr' ,
user : 'myUsername' ,
password : 'myPassword'
)
RFC 3161 Compliance
The timestamp request follows RFC 3161 (Internet X.509 PKI Time-Stamp Protocol):
SHA-256 message digest
ASN.1 DER encoding
TimeStampToken embedded in UnsignedProperties
Why use timestamps?
Proves the signature was created at a specific time
Remains valid even if the certificate expires later
Required for long-term archival (10+ years)
Recommended for legal disputes
If the TSA server is unavailable or returns an error, signing continues without the timestamp. The signature remains valid, but without time proof.
Converting Certificates
If you have certificates in other formats:
Converting PEM to PKCS#12
Converting PKCS#12 to PEM
Removing PEM Key Passphrase
# Combine certificate and key into .pfx file
openssl pkcs12 -export \
-in certificate.pem \
-inkey private-key.pem \
-out certificate.pfx \
-name "My Certificate"
Certificate Requirements
Your certificate must:
Be issued by a trusted Certificate Authority (CA)
Support digital signatures (key usage: digitalSignature)
Use RSA keys (recommended: 2048-bit or higher)
Not be expired or revoked
For testing, you can create self-signed certificates. For production, obtain certificates from:
Spanish qualified providers (FNMT, Camerfirma, etc.)
International CAs (DigiCert, GlobalSign, etc.)
Cryptographic Details
Algorithms Used
Canonicalization : C14N (Canonical XML 1.0)
Digest algorithm : SHA-256
Signature algorithm : RSA-SHA256
Transform : Enveloped signature
Security Features
Three-Reference Signature Signs document content, certificate info, and signed properties separately for maximum integrity.
Certificate Embedding Embeds the full X.509 certificate in the signature for verification without external lookups.
Policy Binding References Spanish FacturaE signing policy v3.1 for regulatory compliance.
Qualified Timestamp Optional RFC 3161 timestamp proves signature creation time.
Complete Examples
Basic Signing
use PhpFacturae\ Invoice ;
use PhpFacturae\ Party ;
use PhpFacturae\ Signer ;
$seller = Party :: company ( 'B12345678' , 'Acme Corp' );
$buyer = Party :: company ( 'B87654321' , 'Client Ltd' );
$invoice = Invoice :: create ( '2024-001' )
-> seller ( $seller )
-> buyer ( $buyer )
-> line ( 'Consulting' , price : 1000 , vat : 21 )
-> transferPayment ( 'ES12 3456 7890 1234 5678 9012' )
-> sign ( Signer :: pfx ( 'certificate.pfx' , 'password' ))
-> export ( 'signed-invoice.xml' );
echo "Signed invoice exported successfully." ;
Signing with Timestamp
$signer = Signer :: pfx ( 'certificate.pfx' , 'password' )
-> timestamp ( 'https://freetsa.org/tsr' );
$invoice = Invoice :: create ( '2024-002' )
-> seller ( $seller )
-> buyer ( $buyer )
-> line ( 'Annual license' , price : 5000 , vat : 21 )
-> transferPayment ( 'ES12 3456 7890 1234 5678 9012' )
-> sign ( $signer )
-> export ( 'timestamped-invoice.xml' );
PEM Certificate Signing
$signer = Signer :: pem (
certPath : '/path/to/cert.pem' ,
keyPath : '/path/to/key.pem' ,
passphrase : 'keyPassword'
);
$invoice = Invoice :: create ( '2024-003' )
-> seller ( $seller )
-> buyer ( $buyer )
-> line ( 'Development' , price : 3000 , vat : 21 , irpf : 15 )
-> transferPayment ( 'ES12 3456 7890 1234 5678 9012' )
-> sign ( $signer );
// Get signed XML string
$signedXml = $invoice -> toXml ();
file_put_contents ( 'output.xml' , $signedXml );
Signing for FACe (Public Administration)
// Public administration buyer with administrative centers
$publicBuyer = Party :: company ( 'Q2819002D' , 'Ministerio de Hacienda' )
-> address ( 'Calle Alcalá 5' , '28014' , 'Madrid' , 'Madrid' )
-> centre ( '01' , 'L01281901' , 'Oficina Contable' )
-> centre ( '02' , 'L01281902' , 'Unidad Tramitadora' )
-> centre ( '03' , 'L01281903' , 'Oficina Gestora' );
$signer = Signer :: pfx ( 'qualified-cert.pfx' , 'password' )
-> timestamp ( 'https://tsa.example.com/rfc3161' );
$invoice = Invoice :: create ( '2024-FACE-001' )
-> series ( 'FACE' )
-> seller ( $seller )
-> buyer ( $publicBuyer )
-> line ( 'Professional services' , price : 2500 , vat : 21 , irpf : 15 )
-> transferPayment ( 'ES12 3456 7890 1234 5678 9012' , dueDate : '+30 days' )
-> sign ( $signer )
-> export ( 'face-signed-invoice.xml' );
// Upload to FACe portal
FACe submissions require:
Qualified electronic signatures (certificates from approved Spanish providers)
Valid administrative center codes
Proper tax withholdings (IRPF) for services
Compliance with public procurement regulations
Verifying Signatures
To verify a signed invoice (outside PHP FacturaE):
Extract the certificate from the <X509Certificate> element
Verify the certificate chain against trusted CAs
Recalculate digests for document, KeyInfo, and SignedProperties
Verify signature value using the certificate’s public key
Check timestamp (if present) against TSA certificate
Most XML signature libraries (xmlsec1, Java JSR 105, etc.) can verify XAdES-EPES signatures.
xmlsec1 --verify signed-invoice.xml
Troubleshooting
Certificate Load Failures
// Error: Failed to read PKCS#12 file
// Solution: Check password and file format
try {
$signer = Signer :: pfx ( 'cert.pfx' , 'password' );
} catch ( RuntimeException $e ) {
echo "Certificate error: " . $e -> getMessage ();
}
Private Key Password Issues
// PEM keys with passphrase
$signer = Signer :: pem (
certPath : 'cert.pem' ,
keyPath : 'encrypted-key.pem' ,
passphrase : 'keyPassword' // Required for encrypted keys
);
Timestamp Failures
Timestamp failures are non-fatal :
// If TSA is down, signature proceeds without timestamp
$signer = Signer :: pfx ( 'cert.pfx' , 'password' )
-> timestamp ( 'https://unreachable-tsa.example/tsr' );
// Invoice is still signed, just without timestamp proof
$invoice -> sign ( $signer ) -> export ( 'invoice.xml' );
Monitor your application logs for timestamp failures. While non-fatal, consistent failures may indicate TSA issues that need addressing.
Method Reference
Signer Facade
Method Parameters Returns Description pfx()string $path, ?string $passphrasePkcs12SignerCreate signer from PKCS#12 pem()string $certPath, string $keyPath, ?string $passphrasePkcs12SignerCreate signer from PEM files
Pkcs12Signer Methods
Method Parameters Returns Description pfx()string $path, ?string $passphraseselfStatic: Load PKCS#12 pem()string $certPath, string $keyPath, ?string $passphraseselfStatic: Load PEM timestamp()string $url, ?string $user, ?string $passwordselfAdd TSA timestamp sign()string $xmlstringSign XML document
Invoice Signing
Method Parameters Returns Description sign()InvoiceSigner $signerselfSet signer for invoice toXml()None stringExport signed XML export()string $pathselfSave signed XML to file
Source Reference
Signing implementation:
src/Signer.php:9-27 - Signer facade
src/Signer/Pkcs12Signer.php:26-651 - XAdES-EPES signer
src/Signer/InvoiceSigner.php:7-13 - Signer interface
src/Invoice.php:409-413 - Sign method