Come posso utilizzare certificati diversi su connessioni specifiche?

Un modulo che sto aggiungendo alla nostra grande applicazione Java deve dialogare con il sito Web protetto da SSL di un’altra azienda. Il problema è che il sito utilizza un certificato autofirmato. Ho una copia del certificato per verificare che non sto incontrando un attacco man-in-the-middle, e ho bisogno di incorporare questo certificato nel nostro codice in modo tale che la connessione al server abbia successo.

Ecco il codice di base:

void sendRequest(String dataPacket) { String urlStr = "https://host.example.com/"; URL url = new URL(urlStr); HttpURLConnection conn = (HttpURLConnection)url.openConnection(); conn.setMethod("POST"); conn.setRequestProperty("Content-Length", data.length()); conn.setDoOutput(true); OutputStreamWriter o = new OutputStreamWriter(conn.getOutputStream()); o.write(data); o.flush(); } 

Senza alcuna ulteriore gestione in atto per il certificato autofirmato, questo muore a conn.getOutputStream () con la seguente eccezione:

 Exception in thread "main" javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target .... Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target .... Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target 

Idealmente, il mio codice deve insegnare a Java ad accettare questo certificato autofirmato, per questo posto nell’applicazione, e da nessun’altra parte.

So che posso importare il certificato nell’archivio dell’autorità di certificazione di JRE e questo permetterà a Java di accettarlo. Non è un approccio che voglio prendere se posso aiutare; sembra molto invasivo fare su tutte le macchine dei nostri clienti per un modulo che non possono usare; interesserebbe tutte le altre applicazioni Java che usano lo stesso JRE, e non mi piace, anche se le probabilità di qualsiasi altra applicazione Java che acceda a questo sito sono nulle. Non è nemmeno un’operazione banale: su UNIX devo ottenere i diritti di accesso per modificare il JRE in questo modo.

Ho anche visto che posso creare un’istanza di TrustManager che esegue alcuni controlli personalizzati. Sembra che potrei anche essere in grado di creare un TrustManager che delega al vero TrustManager in tutte le istanze tranne questo certificato. Ma sembra che TrustManager venga installato a livello globale, e presumo che possa influire su tutte le altre connessioni dalla nostra applicazione, e che non abbia nemmeno un buon odore per me.

Qual è il modo preferito, standard o migliore per configurare un’applicazione Java per accettare un certificato autofirmato? Posso raggiungere tutti gli obiettivi che ho in mente sopra o devo scendere a compromessi? Esiste un’opzione che include file e directory e impostazioni di configurazione e un codice da zero a zero?

    Crea SSLSocket stesso una factory SSLSocket e HttpsURLConnection su HttpsURLConnection prima di connetterti.

     ... HttpsURLConnection conn = (HttpsURLConnection)url.openConnection(); conn.setSSLSocketFactory(sslFactory); conn.setMethod("POST"); ... 

    Dovrai creare un SSLSocketFactory e tenerlo in giro. Ecco uno schizzo di come inizializzarlo:

     /* Load the keyStore that includes self-signed cert as a "trusted" entry. */ KeyStore keyStore = ... TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); tmf.init(keyStore); SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(null, tmf.getTrustManagers(), null); sslFactory = ctx.getSocketFactory(); 

    Se hai bisogno di aiuto per creare il keystore, per favore commenta.


    Ecco un esempio di caricamento del keystore:

     KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); keyStore.load(trustStore, trustStorePassword); trustStore.close(); 

    Per creare l’archivio chiavi con un certificato in formato PEM, puoi scrivere il tuo codice utilizzando CertificateFactory , o semplicemente importarlo con keytool dal JDK (keytool non funzionerà per un “key entry”, ma va bene per un “trusted” iscrizione”).

     keytool -import -file selfsigned.pem -alias server -keystore server.jks 

    Se la creazione di SSLSocketFactory non è un’opzione, è sufficiente importare la chiave nella JVM

    1. Recupera la chiave pubblica: $openssl s_client -connect dev-server:443 , quindi crea un file dev-server.pem che assomiglia

       -----BEGIN CERTIFICATE----- lklkkkllklklklklllkllklkl lklkkkllklklklklllkllklkl lklkkkllklk.... -----END CERTIFICATE----- 
    2. Importa la chiave: #keytool -import -alias dev-server -keystore $JAVA_HOME/jre/lib/security/cacerts -file dev-server.pem . Password: changeit

    3. Riavvia JVM

    Fonte: come risolvere javax.net.ssl.SSLHandshakeException?

    Copiamo il truststore di JRE e aggiungiamo i nostri certificati personalizzati a quel truststore, quindi comunichiamo all’applicazione di utilizzare il truststore personalizzato con una proprietà di sistema. In questo modo lasciamo il truststore JRE predefinito da solo.

    Lo svantaggio è che quando aggiorni JRE non ottieni il suo nuovo truststore automaticamente unito a quello personalizzato.

    Si potrebbe forse gestire questo scenario avendo un programma di installazione o una routine di avvio che verifica il truststore / jdk e verifica una mancata corrispondenza o aggiorna automaticamente il truststore. Non so cosa succede se aggiorni il truststore mentre l’applicazione è in esecuzione.

    Questa soluzione non è elegante al 100% o infallibile, ma è semplice, funziona e non richiede codice.

    Ho dovuto fare qualcosa di simile quando uso commons-httpclient per accedere a un server https interno con un certificato autofirmato. Sì, la nostra soluzione era creare un TrustManager personalizzato che semplicemente passasse tutto (registrando un messaggio di debug).

    Si tratta di avere il nostro SSLSocketFactory che crea socket SSL dal nostro SSLContext locale, che è configurato per avere solo il nostro TrustManager locale ad esso associato. Non è necessario avvicinarsi a un keystore / certstore.

    Quindi questo è nel nostro LocalSSLSocketFactory:

     static { try { SSL_CONTEXT = SSLContext.getInstance("SSL"); SSL_CONTEXT.init(null, new TrustManager[] { new LocalSSLTrustManager() }, null); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("Unable to initialise SSL context", e); } catch (KeyManagementException e) { throw new RuntimeException("Unable to initialise SSL context", e); } } public Socket createSocket(String host, int port) throws IOException, UnknownHostException { LOG.trace("createSocket(host => {}, port => {})", new Object[] { host, new Integer(port) }); return SSL_CONTEXT.getSocketFactory().createSocket(host, port); } 

    Insieme ad altri metodi per implementare SecureProtocolSocketFactory. LocalSSLTrustManager è l’implementazione fittizia del gestore fiduciario sopra menzionata.

    Ho passato un sacco di posti in SO e sul web per risolvere questa cosa. Questo è il codice che ha funzionato per me:

      ByteArrayInputStream derInputStream = new ByteArrayInputStream(app.certificateString.getBytes()); CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) certificateFactory.generateCertificate(derInputStream); String alias = "alias";//cert.getSubjectX500Principal().getName(); KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType()); trustStore.load(null); trustStore.setCertificateEntry(alias, cert); KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); kmf.init(trustStore, null); KeyManager[] keyManagers = kmf.getKeyManagers(); TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); tmf.init(trustStore); TrustManager[] trustManagers = tmf.getTrustManagers(); SSLContext sslContext = SSLContext.getInstance("TLS"); sslContext.init(keyManagers, trustManagers, null); URL url = new URL(someURL); conn = (HttpsURLConnection) url.openConnection(); conn.setSSLSocketFactory(sslContext.getSocketFactory()); 

    app.certificateString è una stringa che contiene il certificato, ad esempio:

      static public String certificateString= "-----BEGIN CERTIFICATE-----\n" + "MIIGQTCCBSmgAwIBAgIHBcg1dAivUzANBgkqhkiG9w0BAQsFADCBjDELMAkGA1UE" + "BhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKzApBgNVBAsTIlNlY3VyZSBE" + ... a bunch of characters... "5126sfeEJMRV4Fl2E5W1gDHoOd6V==\n" + "-----END CERTIFICATE-----"; 

    Ho verificato che è ansible inserire qualsiasi carattere nella stringa del certificato, se è autofirmato, purché si mantenga la struttura esatta sopra. Ho ottenuto la stringa del certificato con la riga di comando del terminale del mio portatile.