greenter/xmldsig
PHP library for signing SUNAT electronic invoices in Peru. Load a PEM certificate and sign XML files or strings, adding the required ds:Signature block with transforms, digest and RSA signature to your comprobantes.
Installation:
composer require greenter/xmldsig
Ensure vendor/autoload.php is autoloaded (Laravel handles this via composer dump-autoload).
Certificate Preparation:
.pfx certificate to .pem (private key + certificate) using the provided guide..pem file in a secure location (e.g., storage/app/certificates/).First Use Case: Sign an XML file (e.g., a SUNAT-compliant invoice) with a single line:
$signedXml = app(\Greenter\XMLSecLibs\Sunat\SignedXml::class)
->setCertificateFromFile(storage_path('app/certificates/certificate.pem'))
->signFromFile(public_path('invoices/invoice.xml'));
RSA_SHA256 / SHA256 pair per-signer — useful when integrating with legacy SUNAT providers or non-standard validators that require SHA-1 or different RSA variants..setCertificateFromFile()->signXml()) behave exactly as before. Defaults are unchanged.Signing XML:
signFromFile() for direct file operations.signXml() for in-memory XML strings (e.g., from a database or API response).// Example: Sign XML from a database record
$xmlString = Invoice::find($id)->xml_content;
$signedXml = $signer->signXml($xmlString);
Invoice::find($id)->update(['xml_content' => $signedXml]);
Configurable Signature & Digest Algorithms (new in v5.1.0):
$signer = new SignedXml();
$signer->setSignatureAlgorithm(XMLSecurityKey::RSA_SHA256); // or RSA_SHA1 / RSA_SHA512
$signer->setDigestAlgorithm(XMLSecurityDSig::SHA256); // or SHA1 / SHA512
$signer->setCertificateFromFile(storage_path('app/certificates/certificate.pem'));
$signedXml = $signer->signXml($xmlString);
// config/xmldsig.php
return [
'signature_algorithm' => env('XMLDSIG_SIG_ALGO', XMLSecurityKey::RSA_SHA256),
'digest_algorithm' => env('XMLDSIG_DIGEST_ALGO', XMLSecurityDSig::SHA256),
];
Integration with Laravel Services:
// app/Providers/AppServiceProvider.php
public function register()
{
$this->app->singleton(SignedXml::class, function ($app) {
$signer = new SignedXml();
$signer->setSignatureAlgorithm(config('xmldsig.signature_algorithm'));
$signer->setDigestAlgorithm(config('xmldsig.digest_algorithm'));
$signer->setCertificateFromFile(storage_path('app/certificates/certificate.pem'));
return $signer;
});
}
public function generateInvoice(Request $request)
{
$xml = $this->generateXmlFromRequest($request);
$signedXml = app(SignedXml::class)->signXml($xml);
return response($signedXml)->header('Content-Type', 'application/xml');
}
Batch Processing:
public function handle()
{
$invoices = Invoice::whereNull('signed_at')->get();
foreach ($invoices as $invoice) {
$signedXml = app(SignedXml::class)->signXml($invoice->xml_content);
$invoice->update(['xml_content' => $signedXml, 'signed_at' => now()]);
}
}
Algorithm-Specific Service Bindings:
$this->app->singleton('xmldsig.sunat', fn () => (new SignedXml())
->setSignatureAlgorithm(XMLSecurityKey::RSA_SHA256));
$this->app->singleton('xmldsig.legacy', fn () => (new SignedXml())
->setSignatureAlgorithm(XMLSecurityKey::RSA_SHA1)
->setDigestAlgorithm(XMLSecurityDSig::SHA1));
SignedXml configuration is per-instance, so sharing a singleton across algorithms will not work — always create a separate instance per configuration.Validation:
use Greenter\XMLSecLibs\XMLSecurityKey;
public function verifySignature($xmlString)
{
$doc = new \DOMDocument();
$doc->loadXML($xmlString);
$key = XMLSecurityKey::instance(XMLSecurityKey::RSA_SHA256);
$key->loadKey(storage_path('app/certificates/certificate.pem'), true);
return $key->verify($doc);
}
Certificate Format:
XMLSecurityKey::loadKey() fails silently if the .pem file is malformed..pem file using OpenSSL:
openssl rsa -in certificate.pem -check
-----BEGIN PRIVATE KEY----- or -----BEGIN CERTIFICATE----- headers. Use the CONVERT.md guide strictly.Algorithm Mismatch (v5.1.0+):
RSA_SHA256/SHA256 may have already been baked in if you call signXml() before the setters, or the receiving party expects a different algorithm pair than you set.setCertificateFromFile()/signXml(), and make sure the receiver (SUNAT, WSDL validator, partner endpoint) supports the pair you choose. SHA-1 is still accepted by SUNAT's beta endpoint for older schemas but not for newer ones — prefer SHA-256 unless you have a hard requirement.xmlsec1 before deploying; the library will happily sign with a mismatched pair and only the consumer will fail.XML Structure:
signXml() may fail if the XML lacks a root node or has malformed namespaces.$xml = simplexml_load_string($xmlString);
if ($xml === false) {
throw new \InvalidArgumentException("Invalid XML structure");
}
Performance:
Storage facade to handle large files, and consider raising memory_limit for the queue worker rather than the request.Instance Sharing:
SignedXml holds per-instance state (certificate, algorithms, canonicalization). Avoid sharing a singleton across differently-configured signers or across parallel jobs where algorithm configuration differs. Container singletons are safe for a single fixed configuration only.CI / Regression Awareness:
^5.1 so you pick up fixes that keep the test matrix green; but run your own signature verification in CI (see below) because the library's CI does not exercise SUNAT end-to-end.Inspect the Signature Algorithm in the XML:
grep -o 'Algorithm="[^"]*"' signed.xml
SignatureMethod and DigestMethod entries. If they don't match your setSignatureAlgorithm()/setDigestAlgorithm() calls, the setters were called after signing or on a different instance.Enable Verbose Logging:
$signer->setCanonicalizationMethod(XMLSecurityDSig::EXCL_C14N);
XMLSecurityKey or XMLSecurityDSig errors.Compare Signatures with xmlsec1:
openssl dgst -sha256 -verify certificate.pem -signature signature.bin invoice.xml
Namespace Handling:
ds namespace. Ensure the signed output includes:<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
Custom Canonicalization:
$signer->setCanonicalizationMethod(XMLSecurityDSig::EXCL_C14N_OMIT_COMMENTS);
Per-Algorithm SignedXml Subclass (v5.1.0+):
class SunatBetaSignedXml extends SignedXml
{
public function __construct()
{
parent::__construct();
$this->setSignatureAlgorithm(XMLSecurityKey::RSA_SHA256);
$this->setDigestAlgorithm(XMLSecurityDSig::SHA256);
}
}
Add Metadata:
SignedXml to include custom attributes in the signature (e.g., signing timestamp):class CustomSignedXml extends SignedXml
{
public function signXml($xml)
{
$this->addObject('http://uri.id/signing-time', now()->toAtomString());
return parent::signXml($xml);
}
}
Support for .pfx Files Directly:
.pfx by integrating phpseclib or openssl:public function setCertificateFromPfx($pfxPath, $passphrase)
{
$privateKey = file_get_contents($pfxPath);
$this->setCertificate($this->convertPfxToPem($privateKey, $passphrase));
}
Laravel Artisan Command:
// app/Console/Commands/SignInvoice.php
class SignInvoice extends Command
{
protected $signature = 'invoice:sign {id}
{--sig=RSA_SHA256 : Signature algorithm constant}
{--digest=SHA256 : Digest algorithm constant}';
public function handle()
{
$invoice = Invoice::findOrFail($this->argument('id'));
$signer = app(SignedXml::class);
$signer->setSignatureAlgorithm(constant("Greenter\\XMLSecLibs\\XMLSecurityKey::{$this->option('sig')}"));
$signer->setDigestAlgorithm(constant("Greenter\\XMLSecLibs\\XMLSecurityDSig::{$this->option('digest')}"));
$signedXml = $signer->signXml($invoice->xml_content);
$invoice->update(['xml_content' => $signedXml]);
$this->info('Invoice signed successfully!');
}
}
php artisan invoice:sign 1
php artisan invoice:sign 1 --sig=RSA_SHA1 --digest=SHA1
CI Signature Verification (recommended with v5.1.0):
xmlsec1 verification step:public function test_signature_algorithm_is_configurable()
{
$signer = new SignedXml();
$signer->setSignatureAlgorithm(XMLSecurityKey::RSA_SHA256);
$signer->setDigestAlgorithm(XMLSecurityDSig::SHA256);
$signer->setCertificateFromFile(storage_path('app/certificates/test.pem'));
$signed = $signer->signXml(file_get_contents(base_path('tests/fixtures/invoice.xml')));
$this->assertStringContainsString('Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"', $signed);
$this->assertStringContainsString('Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"', $signed);
}
How can I help you explore Laravel packages today?