2009-05-13 16 views
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.Come posso utilizzare certificati diversi su connessioni specifiche?

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

@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à. – Bruno

risposta

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 – skiphoppy

+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. – erickson

+0

Wow. Questo significa convertire PEM in JKS è sostanzialmente più semplice di quello che ho fatto io! Grazie! :) – skiphoppy

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. – Bruno

+0

Ricevo questa eccezione durante l'esecuzione su Java 7. javax.net.ssl.SSLHandshakeException: nessuna suite di crittografia in comune Potete fornire assistenza? – Uri

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! –

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.