Skip to main content

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:
  1. Certificate file (.crt or .pem)
  2. 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);
1

Public TSA

Use a free public timestamp authority:
->timestamp('https://freetsa.org/tsr')
->timestamp('http://timestamp.digicert.com')
2

Commercial TSA with Authentication

Many commercial TSAs require credentials:
->timestamp(
    url: 'https://tsa.example.com/tsr',
    user: 'myUsername',
    password: 'myPassword'
)
3

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.

Certificate Formats

Converting Certificates

If you have certificates in other formats:
# 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):
  1. Extract the certificate from the <X509Certificate> element
  2. Verify the certificate chain against trusted CAs
  3. Recalculate digests for document, KeyInfo, and SignedProperties
  4. Verify signature value using the certificate’s public key
  5. Check timestamp (if present) against TSA certificate
Most XML signature libraries (xmlsec1, Java JSR 105, etc.) can verify XAdES-EPES signatures.
Verify with xmlsec1
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

MethodParametersReturnsDescription
pfx()string $path, ?string $passphrasePkcs12SignerCreate signer from PKCS#12
pem()string $certPath, string $keyPath, ?string $passphrasePkcs12SignerCreate signer from PEM files

Pkcs12Signer Methods

MethodParametersReturnsDescription
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

MethodParametersReturnsDescription
sign()InvoiceSigner $signerselfSet signer for invoice
toXml()NonestringExport 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

Build docs developers (and LLMs) love