Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Xmldsig Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

Minimal Setup

  1. Installation:

    composer require greenter/xmldsig
    

    Ensure vendor/autoload.php is autoloaded (Laravel handles this via composer dump-autoload).

  2. Certificate Preparation:

    • Convert your .pfx certificate to .pem (private key + certificate) using the provided guide.
    • Store the .pem file in a secure location (e.g., storage/app/certificates/).
  3. 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'));
    

What's New in v5.1.0

  • Configurable signature and digest algorithms (PR #13). You can now override the default 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.
  • GitHub Actions CI (PR #15). Regression coverage now exists for the library itself; treat it as an indicator that signature output is more tightly monitored release-to-release.
  • No breaking changes — existing code paths (.setCertificateFromFile()->signXml()) behave exactly as before. Defaults are unchanged.

Implementation Patterns

Core Workflow

  1. Signing XML:

    • Files: Use signFromFile() for direct file operations.
    • Strings: Use 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]);
    
  2. Configurable Signature & Digest Algorithms (new in v5.1.0):

    • Set the sign/digest algorithms per instance before signing. Check PR #13 for the exact setter names on your installed version; the pattern is:
    $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);
    
    • Drive this from config so you can swap algorithms without code changes:
    // config/xmldsig.php
    return [
        'signature_algorithm' => env('XMLDSIG_SIG_ALGO', XMLSecurityKey::RSA_SHA256),
        'digest_algorithm'    => env('XMLDSIG_DIGEST_ALGO', XMLSecurityDSig::SHA256),
    ];
    
  3. Integration with Laravel Services:

    • Service Provider: Bind the signer to the container for dependency injection:
      // 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;
          });
      }
      
    • Usage in Controllers:
      public function generateInvoice(Request $request)
      {
          $xml = $this->generateXmlFromRequest($request);
          $signedXml = app(SignedXml::class)->signXml($xml);
          return response($signedXml)->header('Content-Type', 'application/xml');
      }
      
  4. Batch Processing:

    • Process multiple invoices in a queue job:
      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()]);
          }
      }
      
  5. Algorithm-Specific Service Bindings:

    • When you sign for multiple recipients (e.g., SUNAT and a legacy partner), register named singletons with different algorithms:
    $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));
    
    • Note: SignedXml configuration is per-instance, so sharing a singleton across algorithms will not work — always create a separate instance per configuration.
  6. Validation:

    • Verify signed XML before processing (e.g., in a middleware or service):
    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);
    }
    

Gotchas and Tips

Pitfalls

  1. Certificate Format:

    • Error: XMLSecurityKey::loadKey() fails silently if the .pem file is malformed.
    • Fix: Validate the .pem file using OpenSSL:
      openssl rsa -in certificate.pem -check
      
    • Common Issue: Missing -----BEGIN PRIVATE KEY----- or -----BEGIN CERTIFICATE----- headers. Use the CONVERT.md guide strictly.
  2. Algorithm Mismatch (v5.1.0+):

    • Symptom: New algorithm setters are ignored, or signatures fail verification on the receiving side.
    • Cause: The default 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.
    • Fix: Set both algorithms before 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.
    • Regression guard: After changing algorithms, verify with OpenSSL/xmlsec1 before deploying; the library will happily sign with a mismatched pair and only the consumer will fail.
  3. XML Structure:

    • Error: signXml() may fail if the XML lacks a root node or has malformed namespaces.
    • Fix: Ensure the XML is well-formed and conforms to SUNAT's schema. Pre-validate:
      $xml = simplexml_load_string($xmlString);
      if ($xml === false) {
          throw new \InvalidArgumentException("Invalid XML structure");
      }
      
  4. Performance:

    • Issue: Signing large XML files (e.g., >1MB) may time out or consume excessive memory.
    • Workaround: Stream the XML file in chunks or use Laravel's Storage facade to handle large files, and consider raising memory_limit for the queue worker rather than the request.
  5. Instance Sharing:

    • Warning: 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.
  6. CI / Regression Awareness:

    • v5.1.0 introduces GitHub Actions (PR #15). If you pin to a tag, prefer ^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.

Debugging Tips

  1. Inspect the Signature Algorithm in the XML:

    • After signing, check that the algorithm actually used matches what you configured:
    grep -o 'Algorithm="[^"]*"' signed.xml
    
    • You should see both SignatureMethod and DigestMethod entries. If they don't match your setSignatureAlgorithm()/setDigestAlgorithm() calls, the setters were called after signing or on a different instance.
  2. Enable Verbose Logging:

    • Temporarily simplify the output when debugging signature issues:
    $signer->setCanonicalizationMethod(XMLSecurityDSig::EXCL_C14N);
    
    • Check Laravel logs for XMLSecurityKey or XMLSecurityDSig errors.
  3. Compare Signatures with xmlsec1:

    • The most reliable ground truth is a third-party tool. Extract the signature block and verify:
    openssl dgst -sha256 -verify certificate.pem -signature signature.bin invoice.xml
    
    • This confirms whether the algorithm the library wrote is the algorithm the consumer will expect.
  4. Namespace Handling:

    • SUNAT XML often uses the ds namespace. Ensure the signed output includes:
    <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
    
    • If missing, add it via schema validation before signing.

Extension Points

  1. Custom Canonicalization:

    • Override the canonicalization method for SUNAT-specific requirements:
    $signer->setCanonicalizationMethod(XMLSecurityDSig::EXCL_C14N_OMIT_COMMENTS);
    
  2. Per-Algorithm SignedXml Subclass (v5.1.0+):

    • When you need a well-defined algorithm policy in code rather than at every call site, subclass:
    class SunatBetaSignedXml extends SignedXml
    {
        public function __construct()
        {
            parent::__construct();
            $this->setSignatureAlgorithm(XMLSecurityKey::RSA_SHA256);
            $this->setDigestAlgorithm(XMLSecurityDSig::SHA256);
        }
    }
    
  3. Add Metadata:

    • Extend 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);
        }
    }
    
  4. Support for .pfx Files Directly:

    • Add methods to handle .pfx by integrating phpseclib or openssl:
    public function setCertificateFromPfx($pfxPath, $passphrase)
    {
        $privateKey = file_get_contents($pfxPath);
        $this->setCertificate($this->convertPfxToPem($privateKey, $passphrase));
    }
    
  5. Laravel Artisan Command:

    • Create a dedicated command for signing invoices, exposing algorithm overrides:
    // 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!');
        }
    }
    
    • Run with:
    php artisan invoice:sign 1
    php artisan invoice:sign 1 --sig=RSA_SHA1 --digest=SHA1
    
  6. CI Signature Verification (recommended with v5.1.0):

    • Add a lightweight test that signs a fixture and asserts both algorithm attributes in the resulting XML, plus a 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);
    }
    
    • This catches algorithm configuration drift the moment you upgrade, which the library's own CI cannot guarantee for your specific receiver.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style