SDICoop: configurazione PHP SoapClient / SoapServer (Apache) per invio e ricezione di test

Ho appena ricevuto il KitDiTest.zip e sto già impazzendo nel cercare di capire come effetturate l’invio di test

    $cert  = 'certs/SDI-XXXXXXXXX-Client.cer';
    $soap  = 'https://testservizi.fatturapa.it/ricevi_file';
    $wsdl = 'https://testservizi.fatturapa.it/SdI2AccoglienzaWeb/SdIRiceviFile_service/WEB-INF/wsdl/SdIRiceviFile_v1.0.wsdl';
    
    $options = array(
        'trace' => 1,
        'exceptions' => true,
        'cache_wsdl' => WSDL_CACHE_NONE, 
        'location' => $soap,
        'local_cert' => $cert,
        'connection_timeout' => 15,
    );
   $client = new \SoapClient($wsdl, $options);

quello che ottengo è un’eccezione

SoapClient::SoapClient(): Unable to set private key file `certs/SDI-XXXXXXXXX-Client.cer’

ho provato quindi a generare il pem del cer

openssl x509 -inform der -in SDI-XXXXXXXXX-Client.cer -out SDI-XXXXXXXXX-Client.pem

ma l’eccezione rimane…

Dato che in materia di “certificati” ne capisco poco, qualche anima pia può spiegarmi dove sto sbagliando?

UPDATE

Ho condiviso la soluzione che ho adottato in questo post SDICoop: configurazione PHP SoapClient / SoapServer (Apache) per invio e ricezione di test

Ciao @cesco69, ho lo stesso problema, sei riuscito a risolvere?
Alessandro

Scusate… mi sto avvicinando adesso a questo “mondo”… e sto cercando informazioni su come fare un server SDI…

Dove posso reperire il KitDiTest.zip ?

Grazie

@omardini il KitDiTest.zip ti viene fornito dall’Agenzia delle Entrate richiedendo l’accreditamento del canale con questa procedura:

http://sdi.fatturapa.gov.it/SdI2FatturaPAWeb/AccediAlServizioAction.do?pagina=accreditamento_canale

si, ho risolto scrivendo un wrapper del SoapClient di php per supportare i diversi ceritificati e lo standard MTOM .

condivido il tutto così magari mi aiuti ad affinarlo e testarlo…

SdiSoapClient.php

 <?php 

class SdiSoapClient extends \SoapClient
{
    const USER_AGENT = 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)';
    const REGEX_ENV  = '/<soap[\s\S]*nvelope>/i';
    const REGEX_XOP  = '/<xop:include[\s\S]*cid:%s@[\s\S]*?<\/xop:Include>/i';
    const REGEX_CID  = '/cid:([0-9a-zA-Z-]+)@/i';
    const REGEX_CON  = '/Content-ID:[\s\S].+?%s[\s\S].+?>([\s\S]*?)--MIMEBoundary/i';
    
    /**
     * Path al certificato della CA
     * @var string
     */
    public $caCertFile;
    
    /**
     * Path alla chiave privata
     * @var string
     */
    public $privateKeyFile;
    
    /**
     * Path al certificato client
     * @var string
     */
    public $clientCertFile;
    
    /**
     * Url del proxy (es. host:port)
     * @var string
     */
    public $proxyUrl;
    
    /**
     * Autenticazione del proxy (es. username:password)
     * @var string
     */
    public $proxyAuth;
    
    /**
     * Headers dell'ultima richiesta
     * @var array
     */
    private $lastRequestHeaders;
    
    /**
     * Headers dell'ultima risposta
     * @var array
     */
    private $lastResponseHeaders;
    
    /**
     * Body dell'ultima richiesta
     * @var string
     */
    private $lastRequestBody;
    
    /**
     * Body dell'ultima risposta
     * @var string
     */
    private $lastResponseBody;
    
    
    /**
     * @inheritdoc
     */
    public function __doRequest($request, $location, $action, $version, $one_way = null)
    {
        // reset
        $this->lastResponseBody = '';
        $this->lastResponseHeaders = array();

        $this->lastRequestHeaders = array(
            'Content-type: text/xml;charset="utf-8"',
            'Accept: text/xml',
            'Cache-Control: no-cache',
            'Pragma: no-cache',
            'SOAPAction: '.$action,
            'Content-length: ' . strlen($request),
        );
        $this->lastRequestBody = $request;
        
        $ch = curl_init();
        
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        curl_setopt($ch, CURLOPT_SSL_ENABLE_ALPN, false);
        curl_setopt($ch, CURLOPT_SSLKEY, $this->privateKeyFile);
        curl_setopt($ch, CURLOPT_SSLCERT, $this->clientCertFile);
        curl_setopt($ch, CURLOPT_CAINFO, $this->caCertFile);

        curl_setopt($ch, CURLOPT_TIMEOUT, 30);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
        curl_setopt($ch, CURLOPT_USERAGENT, self::USER_AGENT);
        curl_setopt($ch, CURLOPT_URL, $location);
        curl_setopt($ch, CURLOPT_POST , true);
        
        curl_setopt($ch, CURLOPT_HTTPHEADER, $this->lastRequestHeaders);
        curl_setopt($ch, CURLOPT_POSTFIELDS, $this->lastRequestBody);
        
        curl_setopt($ch, CURLOPT_HEADERFUNCTION, array(&$this, 'handleHeaderLine'));
        
        if( !empty($this->proxyUrl) ){
            curl_setopt($ch, CURLOPT_PROXY, $this->proxyUrl);
            if( !empty($this->proxyAuth) ){
                curl_setopt($ch, CURLOPT_PROXYUSERPWD, $this->proxyAuth);
            }
        }
        
        $this->lastResponseBody = curl_exec($ch);
        
        if ( false === $this->lastResponseBody ) {
            $err_num  = curl_errno($ch);
            $err_desc = curl_error($ch);
            $httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            
            curl_close($ch);
            
            throw new \Exception('[HTTP:'. $httpcode .'] ' . $err_desc, $err_num);
        }
        
        curl_close($ch);
        
        $this->lastResponseBody = $this->__processResponse($this->lastResponseBody);
        
        return $this->lastResponseBody;
    }
    
    /**
     * Processa la risposta per supportare il formato MTOM
     * NB teniamo il metodo pubblico per favorire i test unitari
     * @param string $response
     * @throws \Exception
     * @return string
     */
    public function __processResponse($response){
        
        $xml_response = null;
        
        // recupera la risposta xml isolandola da quella mtom
        preg_match(self::REGEX_ENV, $response, $xml_response);
        
        if ( !is_array($xml_response) || count($xml_response) <= 0 ) {
            throw new \Exception('No XML has been found.');
        }
        // prendiamo il primo elemento dell'array
        $xml_response = reset($xml_response);

        // recuperiamo i tag xop
        $xop_elements = null;
        preg_match_all(sprintf(self::REGEX_XOP, '.*'), $response, $xop_elements);
        // prendiamo il primo elemento dell'array
        $xop_elements = reset($xop_elements);
        
        if ( is_array($xop_elements) && count($xop_elements) > 0 ) {
            foreach ($xop_elements as $xop_element) {
                
                // recuperiamo il cid
                $matches = null;
                preg_match(self::REGEX_CID, $xop_element, $matches);
                
                if( isset($matches[1]) ){
                    $cid = $matches[1];
                    
                    // recuperiamo il contenuto associato al cid
                    $matches = null;
                    preg_match(sprintf(self::REGEX_CON, $cid), $response, $matches);
                    
                    if( isset($matches[1]) ){
                        $binary = trim($matches[1]);
                        $binary = base64_encode($binary);
                        
                        // sostituiamo il tag xop:Include con base64_encode(binary)
                        // nota: SoapClient fa automaticamente il base64_decode(binary)
                        $old_xml_response = $xml_response;
                        $xml_response = preg_replace(sprintf(self::REGEX_XOP, $cid), $binary, $xml_response);
                        if( $old_xml_response === $xml_response ){
                            throw new \Exception('xop replace failed');
                        }
                    } else {
                        throw new \Exception('binary not found.');
                    }
                } else {
                    throw new \Exception('cid not found.');
                }
            }
        }
        
        return $xml_response;
    }
    
    /**
     * @inheritdoc
     */
    public function __getLastRequestHeaders(){
        return implode("\n", $this->lastRequestHeaders);
    }
    
    /**
     * @inheritdoc
     */
    public function __getLastResponseHeaders(){
        return implode("\n", $this->lastResponseHeaders);
    }
    
    /**
     * @inheritdoc
     */
    public function __getLastRequest(){
        return $this->lastRequestBody;
    }
    
    /**
     * @inheritdoc
     */
    public function __getLastResponse(){
        return $this->lastResponseBody;
    }
    
    /**
     * Handle singolo header richiesta cURL
     * return integer
     */
    public function handleHeaderLine($curl, $header_line){
        $this->lastResponseHeaders[] = $header_line;
        return strlen($header_line);
    }
}

esempio di utilizzo

// endpoint di test
$location = 'https://testservizi.fatturapa.it/ricevi_file';

// wsdl della soap dell'endpoint
$wsdl = '/wsdl/SdIRiceviFile_v1.0.wsdl';

// opzioni SoapClient
$options = array(
        'location' => $location,
        'cache_wsdl' => WSDL_CACHE_NONE,
        'trace' => true,
);

$sdiSoapClient = new SdiSoapClient($wsdl, $options);
    
// certificato con le root concatenate (caentrate.der + CAEntratetest.cer)
$sdiSoapClient->caCertFile = '/certificati/CA_Agenzia_delle_Entrate_all.pem';

// chiave privata del tuo certificato client
$sdiSoapClient->privateKeyFile =  '/certificati/private.key';

// certificato client in formato PEM (ASCIII)
$sdiSoapClient->clientCertFile = '/certificati/SDI-XXXXXXXXX-Client.pem';
    
// path della fattura
$fattura = '/fatture/IT01234567890_11111_MT_001.xml';

$fileSdIAccoglienza = new \stdClass();
$fileSdIAccoglienza->NomeFile = basename($fattura);
$fileSdIAccoglienza->File = base64_encode(file_get_contents($fattura));

$result = $sdiSoapClient->RiceviFile($fileSdIAccoglienza);

per generare il certificato client in formato pem (SDI-XXXXXXXXX-Client.pem):

openssl x509 -inform der -in SDI-XXXXXXXXX-Client.cer -out SDI-XXXXXXXXX-Client.pem

per generare il certificato con le root concatenate (caentrate.der + CAEntratetest.cer)

copy /b caentrate.der+CAEntratetest.cer CA_Agenzia_delle_Entrate_all.pem

PS aprilo e verifica che le sezioni BEGIN e END non siano sovrapposte sulla stessa linea (-----END CERTIFICATE----------BEGIN CERTIFICATE-----), in tal caso mettile su due linee:

-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----

con questo sono riuscito ad inviare una fattura.

Per la ricezione lato server vedi: SDICoop: configurazione PHP SoapClient per invio di test

6 Mi Piace

Bellissimo, questo ci potrebbe essere utile per https://github.com/italia/fatturapa-testsdi/issues/21 !

2 Mi Piace

Ottimo, finalmente sono riuscito a testare la tua classe e sono riuscito a inviare la fattura, naturalmente la riga dove inserisci il bas64 del file va modificato per agganciare il contenuto del file, ad esempio:

$fileSdIAccoglienza->File = base64_encode(file_get_contents($fattura));
1 Mi Piace

ottimo! ho anche sistemato l’esempio originale.

tuttavia io sto invece sbattendo la testa sul supporto a MTOM necessario per implementare https://testservizi.fatturapa.it/ricevi_notifica

Oggi lavorerò a questo, presumo si debba creare il server SOAP che risponda agli endpoint indicati in fase di registrazione, qui c’è un esempio di come fare, appena ho le idee più chiare le condivido.

ciao @arolando ho aggiornato il SdiSoapClient (rivedi il mio messaggio sopra) aggiungendo il supporto al formato MTOM che prima non funzionava bene.

Ti consiglio di aggiornarti.

Per quanto riguarda la parte server puoi utilizzare tranquillamante il SoapServer di php, io ho fatto così e mi funziona tranquillamente.

RicezioneFattureService.php

class RicezioneFattureService
{
    const ER01 = 'ER01';

    public function RiceviFatture($parametersIn)
    {
        $rispostaRiceviFatture = new \stdClass();
        $rispostaRiceviFatture->Esito = self::ER01;
        return $rispostaRiceviFatture;
    }
    
    public function NotificaDecorrenzaTermini($parametersIn)
    {
        
    }
}

esempio di utilizzo

$wsdl = '/wsdl/RicezioneFatture_v1.0.wsdl';

$ricezioneFattureService = new RicezioneFattureService();

$soapServer = new \SoapServer($wsdl);
$soapServer->setObject($ricezioneFattureService);
$soapServer->handle();

la parte tosta è mettere i certificati sul server, se sei su Apache

SSLEngine On

# certificato firmato dall'Agenzia (fompato ASCII/PEM)
SSLCertificateFile "/cert/CERT/sdi/SDI-XXXXXXXXX-Server.pem"

# chiave privata usata per generare la richiesta di certificato
SSLCertificateKeyFile  "/cert/CERT/sdi/private.key"

# certificato dell’autorità di certificatione dell’agenzia
SSLCertificateChainFile "/cert/CERT/sdi/caentrate.der"

# concatenazione del certificato dell'autorità di certificazione dell'agenzia 
# più il certificato dell’autorità di certificazione di test (caentrate.der + CAEntrateTest.der). 
# In questo modo lo stesso server accetta le chiamate sia dal servizio di test che dal servizio di produzione
SSLCACertificateFile "/cert/CERT/sdi/CA_Agenzia_delle_Entrate_all.pem"

SSLStrictSNIVHostCheck off

per generare il certificato server in formato pem (SDI-XXXXXXXXX-Server.pem):

openssl x509 -inform der -in SDI-XXXXXXXXX-Server.cer -out SDI-XXXXXXXXX-Server.pem

per generare il certificato con le root concatenate (caentrate.der + CAEntratetest.cer)

copy /b caentrate.der+CAEntratetest.cer CA_Agenzia_delle_Entrate_all.pem

PS aprilo e verifica che le sezioni BEGIN e END non siano sovrapposte sulla stessa linea (-----END CERTIFICATE----------BEGIN CERTIFICATE-----), in tal caso mettile su due linee:

-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
1 Mi Piace

Ciao @cesco69, stavo provando ad utilizzare la classe SoapClient per cercare di inviare la prima fattura.

Anche io ottengo l’errore:
[HTTP:0] unable to set private key file: 'mykey.key' type PEM

Questo file nel tuo esempio si chiama /certificati/private.key. Il mio mykey.key è la chiave che ho utilizzato per generare il file csr che ho caricato in fase di accreditamento. ps: quando mi chiedeva i file client e server, ho dato lo stesso .csr che avevo generato.

Quindi mykey.key è stato generato con:
openssl genrsa –out mykey.key 2048

Sbaglio file… ?

grazie mille!

1 Mi Piace

@Francesco_Biegi, devi usare SdiSoapClient, se usi la SoapClient php ti da errore.

@Francesco_Biegi, strano… se apri con notepad la tua private key (mykey.key) è in formato PEM (ASCII)?
dovrestri trovare :

-----BEGIN RSA PRIVATE KEY-----
(stringa base64)
-----END RSA PRIVATE KEY-----

Nel caso in cui non fosse in formato PEM, dovresti generare la versione pem:

openssl rsa -in mykey.key -outform PEM -out mykey.pem

Nel caso in cui fosse in formato PEM puoi verificare che stai assegnando la mykey.key alla property SdiSoapClient->privateKeyFile

In ogni caso fai attenzione che tutti i certificati passati a SdiSoapClient devono essere PEM

Lerrore cmq è dato da cURL che non riconosce la chiave privata associata al certificato o non riesce a leggerla, https://www.google.it/search?q=curl+“unable+to+set+private+key+file”

@arolando sono abbastanza cotto, è da stamani che sto facendo le prove… volevo scrivere SdiSoapClient e non SoapClient :frowning:

Cercando un pò su internet ci dovrebbe essere un modo per “capire” se le combinazioni sono giuste, e bisognerebbe eseguire questi due comandi:

openssl x509 -noout -modulus -in SDI-XXX-Client.pem | openssl md5
openssl rsa -noout -modulus -in mykey.key | openssl md5

Se questi due comandi restituiscono lo stesso md5, si sta utilizzando la coppia corretta.
A me ovviamente da due md5 diversi ma… non riesco a capire se è corretto che sia cosi (ovvero, quei due comandi non c’entrano niente con l’argomenti…) Oppure se effettivamente sto sbagliando i file.

Tu potresti fare una prova e mi dici se a te danno lo stesso md5?

Se a te funziona e l’md5 che ti restituisce non è lo stesso, devo capire dove sbaglio…

grazie

@Francesco_Biegi, i miei md5 sono uguali.

Questo mi conforta (fino ad un certo punto :D) quindi prima devo capire come far combaciare questi due valori… ora inizio a metterci dentro i file a caso…

@Francesco_Biegi, anche i miei md5 sono uguali.

Grazie ad entrambi che avete fatto la prova. Quindi devo capire come mai i miei valori sono deversi. Cmq, all’interno di mykey ho -----BEGIN RSA PRIVATE KEY----- -----END RSA PRIVATE KEY-----

Che casino… :cry:

Il pem lo hai generato come da istruzioni di @cesco69?

openssl x509 -inform der -in SDI-XXXXXXXXX-Client.cer -out SDI-XXXXXXXXX-Client.pem

ma sei sicuro che la chiave che stai utilizzando è la stessa con la quale hai generato la request per ottenere i certificati client server?