Come inviare una fattura elettronica verso l’SdI con PHP

Logo fattura elettronica

Prima di poter interagire con il sistema di interscambio (SdI) bisogna fare l’accreditamento del canale. Una volta fatto ciò ti verrà fornito un “Kit di Test” contenete i seguenti certificati:

  • testservizi.fatturapa.it.cer
  • SistemaInterscambioFatturaPATest.cer
  • servizi.fatturapa.it.cer
  • SistemaInterscambioFatturaPA.cer
  • caentrate.der
  • CAEntratetest.cer

E, sempre dalla pagina di gestione del canale, potrai scaricare due certificati (per client e server) chiamati SDI-<PartitaIVA>.cer

Oltre a tutto ciò avrai a disposizione i file *.key e *.cer che hai utilizzato per l’accreditamento del canale.

Come si usano questi certificati?

Per utilizzare questi certificati all’interno di un client PHP è necessario ottenere delle chiavi derivate che PHP sia in grado di trattare.

In particolare il file SDI-<PartitaIVA>.cer del client (per comodità ora mi riferirò a questo file indicandolo SDI-12345678-client.cer) dovrà essere convertito nel formato .pem tramite il seguente comando:

openssl x509 -inform der -in SDI-12345678-client.cer -out SDI-12345678-client.pem

Inoltre dovrai unire i certificati caentrate.der e CAEntratetest.cer con questo comando:

cat caentrate.der <(echo) CAEntratetest.cer > CA_Agenzia_delle_Entrate_all.pem
E’ fondamentale che inizio e fine dei singoli certificati siano su linee differenti

NOTA: aprendo il file CA_Agenzia_delle_Entrate_all.pem con un editor di testo è fondamentale verificare che END CERTIFICATE e BEGIN CERTIFICATE siano su due linee separate.

A questo punto io ho esteso il SoapClient di PHP per poter utilizzare curl in modo da avere la possibilità di fare il debug di tutto ciò che accade durante la chiamata.

<?php

class DebugSoapClient extends \SoapClient
{
    /**
     * @inheritdoc
     */
    public function __doRequest($request, $location, $action, $version, $one_way = null)
    {
        $soap_request = $request;

        $certspath = __dir__ .  "/certs/";
        //CA file
        $cafile = "CA_Agenzia_delle_Entrate_all.pem";
        //PRIVATE KEY client file
        $keyFile = "private-client.key";
        //Client CERT file
        $clientCertFile = "SDI-12345678-client.pem";

        $header = [
            'Content-type: text/xml;charset="utf-8"',
            'Accept: text/xml',
            'Cache-Control: no-cache',
            'Pragma: no-cache',
            "SOAPAction: {$action}",
            'Content-length: ' . strlen($soap_request),
        ];

        $soap_do = curl_init();

        $url = $location;

        $options = [
            CURLOPT_SSL_VERIFYPEER => true,
            CURLOPT_SSL_VERIFYHOST => 2,
            CURLOPT_SSLKEY => $certspath . $keyFile,
            CURLOPT_SSLCERT => $certspath . $clientCertFile,
            CURLOPT_CAINFO => $certspath . $cafile,

            CURLOPT_SSL_ENABLE_ALPN => false,

            CURLOPT_TIMEOUT => 60,

            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_HEADER         => true,
            CURLOPT_FOLLOWLOCATION => true,

            CURLOPT_USERAGENT      => 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)',
            CURLOPT_VERBOSE        => true,
            CURLOPT_URL            => $url,

            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $soap_request,
            CURLOPT_HTTPHEADER => $header,
        ];

        curl_setopt_array($soap_do, $options);

        $output = curl_exec($soap_do);
        var_dump('curl output = ');
        var_dump($output);
        $info = curl_getinfo($soap_do);
        var_dump('curl info = ');
        var_dump($info);
        var_dump('curl http code = ' . $info['http_code']);
        if ($output === false) {
            $err_num = curl_errno($soap_do);
            $err_desc = curl_error($soap_do);
            $httpcode = curl_getinfo($soap_do, CURLINFO_HTTP_CODE);
            var_dump("—CURL FAIL RESPONSE:\ndati={$output}\nerr_num={$err_num}\nerr_desc={$err_desc}\nhttpcode={$httpcode}");
        } else {
            ///Operation completed successfully
            var_dump('success');
        }
        curl_close($soap_do);

        return $output;
    }
}

Il codice che vedi qui sopra lo puoi trovare anche in questo repository su GitHub.

Conclusione

Questo codice è il frutto di diverse ore spese a capire come funziona il SdI. Un particolare ringraziamento va agli utenti del forum Italia. E’ proprio grazie al loro contributo se sono riuscito a creare questo script  👏

Fonti: Accreditamento SDICoop: configurazione SSL su ApacheInstallazione certificati canale SDICOOP

31 risposte a “Come inviare una fattura elettronica verso l’SdI con PHP”

  1. Ciao, volevo ringraziarti per la spiegazione, però mi viene restituito un errore, probabilmente perchè il private-client.key che passo è errato.
    Ho generato i due file per la richiesta seguendo questa guida:
    http://www.fatturapa.gov.it/export/fatturazione/sdi/csr.pdf

    ma usando il key file mi esce questo errore:

    —CURL FAIL RESPONSE: dati= err_num=58 err_desc=unable to set private key file: ‘/certs/private-client.key’ type PEM httpcode=0″ SOAP Fault: (faultstring: {SoapClient::__doRequest() returned non string value:})

    Consigli?

    Grazie

    1. Hai questo errore: [58] => ‘CURLE_SSL_CERTPROBLEM’.
      Potresti verificare che all’interno del file CA_Agenzia_delle_Entrate_all.pem le righe —–END CERTIFICATE—– e —–BEGIN CERTIFICATE—– siano su due linee differenti?

  2. Ciao, ma i files SistemaInterscambioFatturaPATest.cer e
    SistemaInterscambioFatturaPA.cer servono agli utenti?
    Te lo chiedo perché io riesco ad effettuare l’invio delle fatture, ma non riesco a ricevere le notifiche, per un errore “java.io.IOException: Unable to decrypt message”, come se il sdi non riesca a comunicare con il mio endpoint client.
    Allora penso che sia una questione di corretta impostazione del server (il mio è IIS 7.5) e che possano entrare in gioco i due certificati sopra citati….

        1. Guarda io credo che possa esserci un problema nella chiave che usi te per mandare le richieste (nel mio codice uso private-client.key). Purtroppo senza vedere il codice non credo di poterti essere molto più di aiuto di così.

          1. In realtà stiamo parlando della ricezione delle notifiche, quindi non invio nessuna richiesta ma espongo solo i metodi che ricevono le informazioni dal sdi…
            Per questo sono più propenso a pensare che sia un problema di configurazione dell’ endpoint che riceve le notifiche. Se ti servono piu dettagli, te li posso far avere, anche in separata sede…

          2. Io penso che l’errore “Unable to decrypt message” sia relativo al fatto che il messaggio che invii sia cifrato con il certificato sbagliato e quindi il server del SdI non riesce a decifrarlo correttamente

          3. Ma a quale messaggio ti riferisci? Il mio script invia la fattura al sdi, il sdi la riceve, la analizza, la legge regolarmente perché riscontra degli errori che ho inserito deliberatamente (se non leggesse la fattura, non potrebbe segnalarmi gli errori….), e mi invia la notifica di scarto al mio endpoint client! Il mio endpoint client è una pagina php che dovrebbe solo leggere il messaggio di scarto, ma “non invia” alcun messaggio!
            Nel pannello di controllo di sogei trovo gli errori che sono presenti in fattura (come ti dicevo prima) ma trovo anche “Unable to decrypt message” in corrispondenza del mio end point client: sembra che il sdi abbia cifrato il suo di messaggio di scarto ma il mio script non riesce a decifrarlo…..

          4. Guarda io non ti so aiutare in questo caso. Forse ti conviene provare a sentire direttamente il supporto tecnico di sogei.
            Ti posso solo dire che secondo me stai mischiando due problemi:
            1 – quando mandi la fattura al SdI devi utilizzare il certificato client che poi il server valida. Il messaggio di errore “Unable to decrypt message” fa riferimento proprio a questo.
            2 – Il SdI visto che non riesce a validare la fattura che hai mandato dovrebbe mandarti una notifica di scarto all’endpoint che hai specificato. Se questa notifica non ti arriva vuol dire che c’è qualche altro problema.

          1. E, sempre dalla pagina di gestione del canale, potrai scaricare due certificati (per client e server) chiamati SDI-.cer

            Non trovo questi file, mi puoi indicare dove dovrebbero stare? Mi sono loggato ed ho scaricato lo zip di test, ma questu due files mi mancano

          2. Purtroppo non ho più accesso al pannello di gestione del canale. Comunque spulciando su quei menù ti dovrebbe venire fuori un link per scaricare i certificati in questione. Ricordo che il nome del link non era molto indicativo quindi ti consiglio di leggere con attenzione tutte le pagine

  3. Complimenti per l’articolo mi ha davvero risolto tanti problemi iniziali. Vorrei capire ma c’è qualcosa per ricevere sempre in PHP la ricevuta di consegna da parte dello SDI?

    1. Grazie per i complimenti 😀
      Comunque per la ricevuta di consegna non ti so aiutare visto che noi dopo aver implementato l’invio ci siamo resi conto che il gioco non valeva la candela e quindi abbiamo deciso di appoggiarci ad un servizio esterno che ci faccia da tramite verso l’SdI

  4. Ciao scusami, ma solo tu puoi aiutarmi.
    Ho accreditato il canale e generato il tutto, ma quanto lo lancio il sistema mi da questo errore:

    —CURL FAIL RESPONSE: dati= err_num=58 err_desc=unable to set private key file: ‘/certs/certificate.key’ type PEM httpcode=0″

  5. Ciao, ho provato il tuo codice ma il return non funziona, e nella repository su github c’è un die ed il return commentato.

    Come faccio restituire la response?

    1. Non funziona è una risposta abbastanza vaga…
      In teoria se tutto ha funzionato per il verso giusto, togliendo il die() e rimuovendo il commento sul return, ti dovrebbe restituire esattamente la response printata dalla riga 67 (riferita a github)

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *