Come posso utilizzare certificati diversi su connessioni specifiche?


164

Un modulo che sto aggiungendo alla nostra grande applicazione Java deve conversare con un sito Web protetto da SSL di un'altra società. Il problema è che il sito utilizza un certificato autofirmato. Ho una copia del certificato per verificare che non sto riscontrando un attacco man-in-the-middle e devo incorporare questo certificato nel nostro codice in modo tale che la connessione al server abbia esito positivo.

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 gestione aggiuntiva in atto per il certificato autofirmato, questo si interrompe in 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 punto nell'applicazione e da nessun'altra parte.

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

Ho anche visto che posso creare un'istanza di TrustManager che esegue alcuni controlli personalizzati. Sembra che potrei persino essere in grado di creare un TrustManager che deleghi al vero TrustManager in tutti i casi tranne questo certificato. Ma sembra che TrustManager venga installato a livello globale, e presumo influenzerebbe tutte le altre connessioni dalla nostra applicazione, e non ha 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 dovrò scendere a compromessi? Esiste un'opzione che coinvolge file e directory e impostazioni di configurazione e un codice minimo o nullo?


piccola, correzione funzionante: rgagnon.com/javadetails/…

20
@Hasenpriester: per favore non suggerire questa pagina. Disabilita tutta la verifica della fiducia. Non accetterete solo il certificato autofirmato desiderato, ma accetterete anche qualsiasi certificato che un attaccante MITM vi presenterà.
Bruno,

Risposte:


168

Crea SSLSockettu stesso una fabbrica e impostala HttpsURLConnectionprima di connetterti.

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

Ti consigliamo di crearne uno SSLSocketFactorye 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 l'archivio chiavi, commenta.


Ecco un esempio di caricamento dell'archivio chiavi:

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 usando CertificateFactoryo semplicemente importarlo con keytooldal JDK (keytool non funzionerà 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 indicarmi la giusta direzione, ma alla fine il tuo è l'approccio che ho seguito. Ho avuto un duro lavoro di convertire il file del certificato PEM ad un file di archivio chiavi JKS Java, e ho trovato l'assistenza per questo qui: stackoverflow.com/questions/722931/...
skiphoppy

1
Sono contento che abbia funzionato. Mi dispiace che tu abbia lottato con il keystore; Avrei dovuto includerlo nella mia risposta. Non è troppo difficile con CertificateFactory. In effetti, penso che farò un aggiornamento per chiunque verrà dopo.
Erickson,

1
Tutto questo codice fa replicare ciò che è possibile ottenere impostando tre proprietà di sistema descritte nella Guida di riferimento JSSE.
Marchese di Lorne,

2
@EJP: Questo è stato un po 'di tempo fa, quindi non ricordo per certo, ma suppongo che la logica fosse che in una "grande applicazione Java", è probabile che siano state stabilite altre connessioni HTTP. L'impostazione di proprietà globali potrebbe interferire con connessioni funzionanti o consentire a questa parte di falsificare i server. Questo è un esempio dell'utilizzo del Trust Manager integrato caso per caso.
Erickson,

2
Come ha detto l'OP, "il mio codice deve insegnare a Java ad accettare questo certificato autofirmato, per questo punto nell'applicazione e da nessun'altra parte".
Erickson,

18

Ho letto MOLTI posti online per risolvere questa cosa. Questo è il codice che ho scritto per farlo funzionare:

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 puoi inserire qualsiasi carattere nella stringa del certificato, se è autofirmato, purché tu mantenga la struttura esatta sopra. Ho ottenuto la stringa del certificato con la riga di comando Terminale del mio laptop.


1
Grazie per aver condiviso @Josh. Ho creato un piccolo progetto Github che dimostra il tuo codice in uso: github.com/aasaru/ConnectToTrustedServerExample
Master Drools

14

Se la creazione di a SSLSocketFactorynon è un'opzione, è sufficiente importare la chiave in JVM

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

    -----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. Riavvia JVM

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


1
Non penso che questo risolva la domanda originale, ma ha risolto il mio problema, quindi grazie!
Ed Norris,

Inoltre, non è una buona idea farlo: ora hai questo certificato autofirmato a livello di sistema extra casuale di cui tutti i processi Java si fideranno di default.
user268396,

12

Copiamo il truststore del JRE e aggiungiamo i nostri certificati personalizzati a quel truststore, quindi diciamo all'applicazione di usare il truststore personalizzato con una proprietà di sistema. In questo modo lasciamo solo il truststore JRE predefinito.

Il rovescio della medaglia è che quando aggiorni JRE non ottieni il suo nuovo truststore automaticamente unito a quello personalizzato.

Potresti forse gestire questo scenario avendo un programma di installazione o una routine di avvio che verifica il truststore / jdk e verifica la presenza di 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 qualcosa del genere quando ho usato commons-httpclient per accedere a un server https interno con un certificato autofirmato. Sì, la nostra soluzione era quella di creare un TrustManager personalizzato che passasse semplicemente tutto (registrando un messaggio di debug).

Questo si riduce al fatto di avere il nostro SSLSocketFactory che crea socket SSL dal nostro SSLContext locale, che è impostato per avere solo il nostro TrustManager locale associato ad esso. Non devi assolutamente avvicinarti 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 che implementano SecureProtocolSocketFactory. LocalSSLTrustManager è l'implementazione del gestore della fiducia fittizia sopra menzionata.


8
Se disabiliti tutta la verifica dell'affidabilità, è inutile utilizzare SSL / TLS in primo luogo. Va bene per testare localmente, ma non se si desidera connettersi all'esterno.
Bruno,

Ottengo questa eccezione quando lo eseguo su Java 7. javax.net.ssl.SSLHandshakeException: nessuna suite di cifratura in comune Potete aiutarmi?
Uri Lukach,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.