Come posso utilizzare certificati diversi su connessioni specifiche?


147

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 ulteriori manipolazioni in atto per il certificato auto-firmato, questa 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 JRE e che consentirà 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 un controllo personalizzato. 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?

  0

piccolo, soluzione di lavoro: http://www.rgagnon.com/javadetails/java-fix-certificate-problem-in-HTTPS.html 16 ago. 112011-08-16 14:06:49

+16

@Hasenpriester: per favore non suggerire questa pagina. Disattiva la verifica di attendibilità. Non accetti solo il certificato autofirmato che desideri, ma accetta anche qualsiasi certificato con cui un aggressore MITM ti presenterà. 23 dic. 112011-12-23 13:23:14

150

Creare una fabbrica SSLSocket e impostarla su HttpsURLConnection prima di connettersi.

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

Ti consigliamo di crearne uno SSLSocketFactory e tenerlo intorno.Ecco uno schizzo di come inizializzare:

/* 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 l'archivio delle chiavi, si prega di commentare.


Ecco un esempio di caricamento chiavi:

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

Per creare l'archivio chiavi con un certificato formato PEM, è possibile scrivere il proprio codice utilizzando CertificateFactory, o semplicemente importarlo con keytool da il JDK (keytool non sarà funziona per una "voce chiave", ma va bene per una "voce attendibile").

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

Grazie mille! Gli altri ragazzi mi hanno aiutato a orientarmi nella giusta direzione, ma alla fine il tuo è l'approccio che ho seguito. Ho avuto un estenuante compito di convertire il file del certificato PEM in un file di archivio chiavi Java JKS, e ho trovato assistenza per questo qui: http://stackoverflow.com/questions/722931/ssl-socket-php-code-needs-to- essere convertito in java 13 mag. 092009-05-13 21:37:13

+1

Sono contento che abbia funzionato. Mi dispiace che tu abbia faticato con il negozio di chiavi; Avrei dovuto inserirlo nella mia risposta. Non è troppo difficile con CertificateFactory. In effetti, penso che farò un aggiornamento per chiunque venga dopo. 13 mag. 092009-05-13 21:40:51

  0

Wow. Questo significa convertire PEM in JKS è sostanzialmente più semplice di quello che ho fatto io! Grazie! :) 14 mag. 092009-05-14 19:19:00

+1

Tutto questo codice è replicare ciò che è possibile ottenere impostando tre proprietà di sistema descritte nella Guida alle segnalazioni JSSE. 31 mag. 122012-05-31 19:36:53

+1

@EJP: Questo è stato un po 'di tempo fa, quindi non ricordo per certo, ma suppongo che la logica sia che in una "grande applicazione Java" è probabile che siano stabilite altre connessioni HTTP. L'impostazione delle proprietà globali potrebbe interferire con le connessioni di lavoro o consentire a questa parte di falsificare i server. Questo è un esempio di utilizzo del trust manager integrato, caso per caso. 31 mag. 122012-05-31 22:26:58

+2

Come ha detto l'OP, "il mio codice deve insegnare a Java ad accettare questo certificato autofirmato, per questo posto nell'applicazione, e da nessun'altra parte." 31 mag. 122012-05-31 22:29:09

  0

Ho cercato di implementare la soluzione senza alcun risultato, per favore aiutami http://stackoverflow.com/questions/20190364/how-to-bypass-certificateexception-by-java 25 nov. 132013-11-25 10:59:37

  0

Se vuoi che venga accettato "globalmente" usa keytool per installare il certificato come certificato attendibile nel file cacerts. Quindi sarà considerato attendibile per qualsiasi VM avviata che utilizza quel JRE. 06 dic. 132013-12-06 19:59:08

  0

@KyleM Non è un buon approccio all'accettazione globale. 06 dic. 132013-12-06 20:03:33

  0

Ecco perché ho inserito "globale" tra virgolette e spiegato come funziona effettivamente. Nessun altro ha detto di farlo in questo modo, sebbene sia un metodo molto comune e accettato. 06 dic. 132013-12-06 20:07:48

  0

Molte grandi organizzazioni non accettano questo metodo. Sarebbe una violazione della sicurezza con gravi conseguenze. 06 dic. 132013-12-06 21:09:37

  0

@erickson Ciao - sono passati quattro anni e l'approccio che ci hai insegnato è stato canticchiare bene ed è stato utilizzato per diversi server con cui comunichiamo. Ora ho un nuovo trucco che ho bisogno di capire (potrebbe fare una nuova domanda StackOverflow per questo): Mi piacerebbe fidarmi sia del keystore predefinito che del keystore personalizzato con il certificato autofirmato del server. Avevamo un caso in cui il server si aggiornava su un vero certificato e la connessione si interrompeva, ma sarebbe andata bene se ci fidavamo del keystore predefinito. 13 feb. 142014-02-13 19:25:02

  0

@erickson L'API è un po 'fuorviante: implica che è possibile passare una serie di TrustManager a SSLContext.init(), ma javadoc su quel metodo indica che viene utilizzato solo il primo TrustManager nell'array. Mi piacerebbe sentire tutte le informazioni che puoi condividere. 13 feb. 142014-02-13 19:26:18

  0

@skiphoppy Dovrò sperimentare un po 'con quello e tornare da te. Non l'ho mai fatto prima, ma ho un'idea che potrebbe funzionare. 13 feb. 142014-02-13 20:03:55

+1

@skiphoppy Hmm, l'unico modo che ho trovato è creare un 'KeyStore' temporaneo (in memoria) e aggiungere certificati di root da tutti i keystore" reali "a quello, o (quasi equivalentemente) creare un' Set <TrustAnchor> ' da tutte le voci nei keystore fidati e usarlo per inizializzare 'PKIXParameters', e con questo, a sua volta,' TrustManagerFactory'. 13 feb. 142014-02-13 21:48:48

  0

Grazie a @erickson ci sto ancora giocando e sto vagando per un lungo labirinto di documentazione. Darò uno scatto. 13 feb. 142014-02-13 22:03:09


12

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 si aggiorna JRE non si ottiene automaticamente il suo nuovo truststore unito a quello personalizzato.

Si potrebbe forse gestire questo scenario avendo un programma di installazione o 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.


12

ho dovuto fare una cosa del genere quando si utilizza commons-HttpClient per accedere a un server HTTPS interna con un certificato auto-firmato. 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 è impostato 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 di attuazione SecureProtocolSocketFactory. LocalSSLTrustManager è l'implementazione fittizia del gestore fiduciario sopra menzionata.

+7

Se si disattiva la verifica di attendibilità, in primo luogo non è necessario utilizzare SSL/TLS. Va bene per testare localmente, ma non se vuoi connetterti all'esterno. 23 dic. 112011-12-23 13:26:31

  0

Ricevo questa eccezione durante l'esecuzione su Java 7. javax.net.ssl.SSLHandshakeException: nessuna suite di crittografia in comune Potete fornire assistenza? 19 dic. 132013-12-19 12:55:10


14

Se la creazione di un SSLSocketFactory non è un'opzione, basta importare la chiave nella JVM

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

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

  3. Restart JVM

Fonte: How to solve javax.net.ssl.SSLHandshakeException?

+1

Non penso che questo risolva la domanda originale, ma ha risolto il problema * mio *, quindi grazie! 15 mag. 132013-05-15 15:10:40


11

ho passato un sacco di posti nel SO e il 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 provato che si può mettere tutti i caratteri della stringa 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.