Invio Fattura in Java, uso certificati

Se ottieni 403 è un buon indizio del fatto che l’handshake SSL è andato a buon fine, quindi a quel punto devi indagare su cosa ci sia che non va nella tua chiamata.

Grazie per la risposta innanzitutto.

nel truststore sto mettendo
caentrate.cer
CAEntratetest.cer
testservizi.fatturapa.it.pem (anche usando testservizi.fatturapa.it.cer ottengo sempre lo stesso errore)

nel keystore il certificato client che ho installato su browser ed esportato. Sto escludendo che il problema dipenda dal certificato client, credo a questo punto che il problema sia il truststore.

Al momento entrambi vengono caricati e riconosciuti nell’sslConnectionSocketFactory. C’è qualche prova che potrei fare per avere qualche indizio in più?

Stamattina ho contattato anche l’assistenza, ma l’operatore che mi ha risposto non ha saputo darmi indicazioni più tecniche.

Grazie

Nel keystore ovviamente ci hai messo anche la tua chiave privata, quella con cui hai generato la CSR che hai inviato a SdI, giusto?

Purtroppo effettivamente descritto così il problema è un po’ vago. Però appunto un conto è se l’handshaking SSL fallisce (significa che c’è qualcosa che non va in come stai usando i certificati), un conto è se ricevi errore 403, perché a quel punto potrebbe significare semplicemente che non stai invocando correttamente un servizio. Perché le prove che stai facendo sono sull’invocazione di uno dei servizi SOAP esposti da SdI, giusto?

si, il certificato è quello SDI-.cer combinato con le chiavi private che ho inviato a SDI (quella di send e recive, se ne mettevo una sola per generare il certificato mi dava errore openssl). Questo certificato l’ho poi importato in firefox e fatto il test di accesso a https://testservizi.fatturapa.it e riesco ad accedere sia alla welcome di ricevi_file che al wsdl. Questo certificato l’ho poi esportato dal browser ed inserito nel keystore che passo alla configurazione di springboot.

@Configuration
public class RiceviFileConfiguration {

	@Bean
	Jaxb2Marshaller jaxb2Marshaller() {
		Jaxb2Marshaller jaxb2Marshaller = null;
		try {
			jaxb2Marshaller = new Jaxb2Marshaller();
			jaxb2Marshaller.setContextPath("it.mac.fatturaSDI.wsdl.RiceviFile");
			jaxb2Marshaller.afterPropertiesSet();
			jaxb2Marshaller.setMtomEnabled(true);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return jaxb2Marshaller;
	}

	@Bean
	public RiceviFileClient getRiceviFileClient() throws Exception {
		RiceviFileClient riceviFileClient = new RiceviFileClient();
		riceviFileClient.setMarshaller(jaxb2Marshaller());
		riceviFileClient.setUnmarshaller(jaxb2Marshaller());
		riceviFileClient.setDefaultUri("https://testservizi.fatturapa.it/ricevi_file");
		riceviFileClient.setMessageSender(httpComponentsMessageSender());
		return riceviFileClient;
	}

	
	@Bean
	public HttpComponentsMessageSender httpComponentsMessageSender() throws Exception {
		HttpComponentsMessageSender httpComponentsMessageSender = new HttpComponentsMessageSender();
		httpComponentsMessageSender.setHttpClient(httpClient());

		return httpComponentsMessageSender;
	}

	@Bean
	public HttpClient httpClient() throws Exception {
		return HttpClientBuilder.create().setSSLSocketFactory(sslConnectionSocketFactory())
				.addInterceptorFirst(new RemoveSoapHeadersInterceptor()).build();
	}

	@Bean
	public SSLConnectionSocketFactory sslConnectionSocketFactory() throws Exception {
		try {
			System.out.println("dentro sslconnFactory");
			SSLContext sslContext = SSLContext.getInstance("TLS");
			String trustStorePassword = "changeit";
			KeyManager[] keyStore = getKeyManager("PKCS12", "/opt/wildfly/standalone/jks/keystore.jks",
					trustStorePassword);
			TrustManager[] trustManager = getTrustManager("jks", "/opt/wildfly/standalone/jks/truststore.jks",
					trustStorePassword);
			sslContext.init(keyStore, trustManager, new SecureRandom());
			 return new SSLConnectionSocketFactory(sslContext);
		} catch (Exception e) {
			throw new RuntimeException("Unable to setup keystore and truststore", e);
		}

	}

	private KeyManager[] getKeyManager(String keyStoreType, String keyStorePath, String keyStorePassword)
			throws Exception {
		KeyStore keyStore = KeyStore.getInstance(keyStoreType);
		KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
		File keyStoreFile = new File(keyStorePath);
		InputStream isKey = new FileInputStream(keyStoreFile);
		keyStore.load(isKey, keyStorePassword.toCharArray());
		kmf.init(keyStore, keyStorePassword.toCharArray());
		return kmf.getKeyManagers();

	}

	private TrustManager[] getTrustManager(String trustStoreType, String trustStorePath, String trustStorePassword)
			throws Exception {
		KeyStore trustStore = KeyStore.getInstance(trustStoreType);
		TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
		File trustStoreFile = new File(trustStorePath);
		InputStream isTrust = new FileInputStream(trustStoreFile);
		trustStore.load(isTrust, trustStorePassword.toCharArray());
		tmf.init(trustStore);

		return tmf.getTrustManagers();
	}
}

Mentre la costruzione della request è la seguente

private final String NAMESPACE = "http://www.fatturapa.gov.it/sdi/ws/trasmissione/v1.0/types";
private final String RICEVI_FILE = "RiceviFile";

@PayloadRoot(namespace = NAMESPACE, localPart = RICEVI_FILE)
	@ResponsePayload
	@SuppressWarnings("unchecked")
	public JAXBElement<RispostaSdIRiceviFileType> riceviFile() throws Exception{
		JAXBElement<RispostaSdIRiceviFileType> response = null;

		try {
			ObjectFactory factory = new ObjectFactory();
			FileSdIBaseType sdIAccoglienza = factory.createFileSdIAccoglienza(new FileSdIBaseType()).getValue();
			sdIAccoglienza.setNomeFile("IT01234567890_FPA01.xml");
			FileDataSource source = new FileDataSource("/opt/wildfly/standalone/data/IT01234567890_FPA01.xml");
			sdIAccoglienza.setFile(new DataHandler(source));
			JAXBElement<FileSdIBaseType> request = factory.createFileSdIAccoglienza(sdIAccoglienza);
						
			response = (JAXBElement<RispostaSdIRiceviFileType>) getWebServiceTemplate().marshalSendAndReceive(
					 request, new SoapActionCallback("https://testservizi.fatturapa.it/ricevi_file?wsdl"));
					 
		} catch (Exception e) {
			e.printStackTrace();
		}
		return response;
	}

La request soap viene correttamente costruita ed il keystore e truststore caricati nell sslConnectionSocketFactory.

Ho anche provato a mettere i certificati nel cacerts della jvm in esecuzione ma l’errore non cambia.

Come hai consigliato tu ho provato ad aggiornare ca-certificates sul server ed inserito i certificati nella cartella
/etc/pki/ca-trust/source/anchors/
ed aggiornato con il
comando update-ca-trust extract
ma non è cambiato nulla.

si, sto invocando i servizi soap per inviare una fattura.

Grazie

Dunque, a me sembra che tu stia configurando tutto correttamente. Ti dirò di più: ho controllato il mio codice ed io non imposto il truststore per le chiamate uscenti, lo uso solo per la configurazione dell’Apache che riceve le chiamate entranti da SdI. Però considera che io l’ambiente di test non lo uso più da anni, per cui in effetti non escludo che possa servire in test (che usa certificati non emessi da una CA well-known).

L’unica differenza che mi sembra di notare è che io uso:
KeyManagerFactory.getInstance("SunX509")
anziché richiedere il default algorithm, ma onestamente non penso stia lì il problema.
Un’altra piccola inesattezza è qui:

KeyManager[] keyStore = getKeyManager("PKCS12", "/opt/wildfly/standalone/jks/keystore.jks",
					trustStorePassword)

se il keystore è in formato PKCS12, tipicamente avrà estensione .p12, se è in JKS allora dovresti passare "JKS" e non "PKCS12", ma ancora una volta non penso sia qui il problema (altrimenti penso sarebbe andato in eccezione).

Ah, un’altra cosa che cambia rispetto a me: perché usi un RemoveSoapHeadersInterceptor? Io non lo uso e, non sapendo bene lo scopo, mi viene da chiedermi perché dovrei rimuovere le intestazioni SOAP (non è che così corrompi le richieste?).

Ad ogni modo, insisto nel dire che se ricevi un errore 403 significa che l’handhake SSL l’hai superato correttamente, quindi NON dovrebbe essere un problema di scambio di certificati, ma piuttosto di formulazione della richiesta. Per quello quel RemoveSoapHeadersInterceptor mi “puzza”. Se ancora non ti funziona, magari lato SdI non ti hanno attivato l’utenza di test associata al tuo certificato client. A quel punto proverei a sentire la segreteria tecnica di SdI… so che è un calvario per cui buona fortuna!!

Nota: se riesci a mettere un proxy in mezzo, verifica che i messaggi HTTP e SOAP che scambi con il server siano corretti e verifica se il server non ti risponda anche qualcos’altro oltre a 403: può essere che trovi qualche indizio.

Grazie per i suggerimenti,
ho sistemato le cose che mi hai indicato, ma ovviamente l’errore è sempre lo stesso. Per cercare di capirci qualcosa in più ho impostato il livello di log su debug ed effettivamente sembrerebbe che la connessione funziona

2023-01-31 22:59:16,201 DEBUG [org.apache.http.conn.ssl.SSLConnectionSocketFactory] (default task-1) Starting handshake
2023-01-31 22:59:16,298 DEBUG [org.apache.http.conn.ssl.SSLConnectionSocketFactory] (default task-1) Secure session established
2023-01-31 22:59:16,298 DEBUG [org.apache.http.conn.ssl.SSLConnectionSocketFactory] (default task-1)  negotiated protocol: TLSv1.2
2023-01-31 22:59:16,298 DEBUG [org.apache.http.conn.ssl.SSLConnectionSocketFactory] (default task-1)  negotiated cipher suite: TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
2023-01-31 22:59:16,298 DEBUG [org.apache.http.conn.ssl.SSLConnectionSocketFactory] (default task-1)  peer principal: CN=testservizi.fatturapa.it, OU=Servizi Sicuri, O=Agenzia delle Entrate, C=IT
2023-01-31 22:59:16,298 DEBUG [org.apache.http.conn.ssl.SSLConnectionSocketFactory] (default task-1)  peer alternative names: [testservizi.fatturapa.it]
2023-01-31 22:59:16,298 DEBUG [org.apache.http.conn.ssl.SSLConnectionSocketFactory] (default task-1)  issuer principal: CN=CA Agenzia delle Entrate, O=Agenzia delle Entrate, C=IT
2023-01-31 22:59:16,300 DEBUG [org.apache.http.impl.conn.DefaultHttpClientConnectionOperator] (default task-1) Connection established  my_ip_addess:17004<->217.175.53.96:443
2023-01-31 22:59:16,300 DEBUG [org.apache.http.impl.execchain.MainClientExec] (default task-1) Executing request POST /ricevi_file HTTP/1.1
2023-01-31 22:59:16,300 DEBUG [org.apache.http.impl.execchain.MainClientExec] (default task-1) Target auth state: UNCHALLENGED
2023-01-31 22:59:16,300 DEBUG [org.apache.http.impl.execchain.MainClientExec] (default task-1) Proxy auth state: UNCHALLENGED
2023-01-31 22:59:16,301 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 >> POST /ricevi_file HTTP/1.1
2023-01-31 22:59:16,301 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 >> Accept-Encoding: gzip
2023-01-31 22:59:16,301 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 >> Accept: text/xml, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
2023-01-31 22:59:16,301 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 >> SOAPAction: "ricevi_file"
2023-01-31 22:59:16,301 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 >> Content-Type: Multipart/Related; boundary="----=_Part_1_1108363849.1675202356110"; type="application/xop+xml"; start-info="text/xml"
2023-01-31 22:59:16,301 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 >> Content-Length: 5686
2023-01-31 22:59:16,301 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 >> Host: testservizi.fatturapa.it
2023-01-31 22:59:16,301 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 >> Connection: Keep-Alive
2023-01-31 22:59:16,301 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 >> User-Agent: Apache-HttpClient/4.5.13 (Java/11.0.17)
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "POST /ricevi_file HTTP/1.1[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "Accept-Encoding: gzip[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "Accept: text/xml, text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "SOAPAction: "ricevi_file"[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "Content-Type: Multipart/Related; boundary="----=_Part_1_1108363849.1675202356110"; type="application/xop+xml"; start-info="text/xml"[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "Content-Length: 5686[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "Host: testservizi.fatturapa.it[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "Connection: Keep-Alive[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "User-Agent: Apache-HttpClient/4.5.13 (Java/11.0.17)[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "------=_Part_1_1108363849.1675202356110[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "Content-Type: application/xop+xml; charset=utf-8; type="text/xml"[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"><SOAP-ENV:Header/><SOAP-ENV:Body><ns3:fileSdIAccoglienza xmlns:ns3="http://www.fatturapa.gov.it/sdi/ws/trasmissione/v1.0/types"><NomeFile>IT01234567890_FPA01.xml</NomeFile><File><xop:Include xmlns:xop="http://www.w3.org/2004/08/xop/include" href="cid:d5b8f704-deae-4f56-9b43-cd5a8fb309cd%40null"/></File></ns3:fileSdIAccoglienza></SOAP-ENV:Body></SOAP-ENV:Envelope>[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "------=_Part_1_1108363849.1675202356110[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "Content-Type: application/octet-stream[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "Content-ID: <d5b8f704-deae-4f56-9b43-cd5a8fb309cd@null>[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "Content-Transfer-Encoding: binary[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "<?xml version="1.0" encoding="UTF-8"?>[\r][\n]"
2023-01-31 22:59:16,302 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 >> "<p:FatturaElettronica versione="FPA12" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" [\r][\n]"
.........................
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "HTTP/1.1 403 Forbidden[\r][\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "Date: Tue, 31 Jan 2023 21:59:16 GMT[\r][\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "ARM_CORRELATOR: 002ECC00303030303131343430303035463339363731363030313230303030303043333930303030304333390001[\r][\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "Content-Length: 199[\r][\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "Keep-Alive: timeout=10, max=100[\r][\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "Connection: Keep-Alive[\r][\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "Content-Type: text/html; charset=iso-8859-1[\r][\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "[\r][\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">[\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "<html><head>[\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "<title>403 Forbidden</title>[\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "</head><body>[\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "<h1>Forbidden</h1>[\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "<p>You don't have permission to access this resource.</p>[\n]"
2023-01-31 22:59:16,320 DEBUG [org.apache.http.wire] (default task-1) http-outgoing-0 << "</body></html>[\n]"
2023-01-31 22:59:16,322 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 << HTTP/1.1 403 Forbidden
2023-01-31 22:59:16,322 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 << Date: Tue, 31 Jan 2023 21:59:16 GMT
2023-01-31 22:59:16,322 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 << ARM_CORRELATOR: 002ECC00303030303131343430303035463339363731363030313230303030303043333930303030304333390001
2023-01-31 22:59:16,322 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 << Content-Length: 199
2023-01-31 22:59:16,322 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 << Keep-Alive: timeout=10, max=100
2023-01-31 22:59:16,322 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 << Connection: Keep-Alive
2023-01-31 22:59:16,322 DEBUG [org.apache.http.headers] (default task-1) http-outgoing-0 << Content-Type: text/html; charset=iso-8859-1
2023-01-31 22:59:16,325 DEBUG [org.apache.http.impl.execchain.MainClientExec] (default task-1) Connection can be kept alive for 10000 MILLISECONDS
2023-01-31 22:59:16,329 DEBUG [org.springframework.ws.client.core.WebServiceTemplate] (default task-1) Received error for request [SaajSoapMessage {http://www.fatturapa.gov.it/sdi/ws/trasmissione/v1.0/types}fileSdIAccoglienza]
2023-01-31 22:59:16,331 DEBUG [org.apache.http.impl.conn.PoolingHttpClientConnectionManager] (default task-1) Connection [id: 0][route: {s}->https://testservizi.fatturapa.it:443] can be kept alive for 10.0 seconds
2023-01-31 22:59:16,331 DEBUG [org.apache.http.impl.conn.DefaultManagedHttpClientConnection] (default task-1) http-outgoing-0: set socket timeout to 0
2023-01-31 22:59:16,331 DEBUG [org.apache.http.impl.conn.PoolingHttpClientConnectionManager] (default task-1) Connection released: [id: 0][route: {s}->https://testservizi.fatturapa.it:443][total available: 1; route allocated: 1 of 2; total allocated: 1 of 20]
2023-01-31 22:59:16,331 ERROR [stderr] (default task-1) org.springframework.ws.client.WebServiceTransportException: Forbidden [403]

ma chissà per quale motivo viene poi chiusa . Oggi ho risentito l’operatore del numero verde che alla fine mi ha aperto una segnalazione all’ufficio tecnico.

grazie ancora