Certyfikaty klienta Java przez HTTPS / SSL

116

Używam Java 6 i próbuję utworzyć HttpsURLConnectionprzeciwko zdalnemu serwerowi, używając certyfikatu klienta.
Serwer używa certyfikatu głównego z podpisem własnym i wymaga przedstawienia certyfikatu klienta chronionego hasłem. Dodałem certyfikat główny serwera i certyfikat klienta do domyślnego magazynu kluczy java, który znalazłem w /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home/lib/security/cacerts(OSX 10.5). Nazwa pliku kluczy wydaje się sugerować, że certyfikat klienta nie powinien tam być umieszczony?

W każdym razie dodanie certyfikatu głównego do tego sklepu rozwiązało niesławny javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed' problem.

Jednak teraz utknąłem na tym, jak używać certyfikatu klienta. Wypróbowałem dwa podejścia i żadne mnie nie prowadzi.
Najpierw i najlepiej spróbuj:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://somehost.dk:3049");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
// The last line fails, and gives:
// javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure

Próbowałem pominąć klasę HttpsURLConnection (nie jest to idealne rozwiązanie, ponieważ chcę rozmawiać z serwerem HTTP) i zamiast tego robię to:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
SSLSocket sslsocket = (SSLSocket) sslsocketfactory.createSocket("somehost.dk", 3049);
InputStream inputstream = sslsocket.getInputStream();
// do anything with the inputstream results in:
// java.net.SocketTimeoutException: Read timed out

Nie jestem nawet pewien, czy problem dotyczy certyfikatu klienta.

Jan
źródło
Dałem dwa certyfikaty od klienta, jak określić, który z nich należy dodać do magazynu kluczy i magazynu zaufania, czy mógłbyś pomóc zidentyfikować ten problem, ponieważ już przeszedłeś przez podobny problem stackoverflow.com/questions/61374276/ ...
henrycharles

Odpowiedzi:

100

Wreszcie rozwiązałem;). Dostał silną wskazówkę tutaj (Gandalfs odpowiedź dotknął trochę na nim również). Brakujące linki były (głównie) pierwszym z poniższych parametrów i do pewnego stopnia przeoczyłem różnicę między magazynami kluczy i zaufanymi.

Certyfikat serwera z podpisem własnym należy zaimportować do magazynu zaufanych certyfikatów:

keytool -import -alias gridserver -file gridserver.crt -storepass $ PASS -keystore gridserver.keystore

Te właściwości należy ustawić (w wierszu poleceń lub w kodzie):

-Djavax.net.ssl.keyStoreType=pkcs12
-Djavax.net.ssl.trustStoreType=jks
-Djavax.net.ssl.keyStore=clientcertificate.p12
-Djavax.net.ssl.trustStore=gridserver.keystore
-Djavax.net.debug=ssl # very verbose debug
-Djavax.net.ssl.keyStorePassword=$PASS
-Djavax.net.ssl.trustStorePassword=$PASS

Działający przykładowy kod:

SSLSocketFactory sslsocketfactory = (SSLSocketFactory) SSLSocketFactory.getDefault();
URL url = new URL("https://gridserver:3049/cgi-bin/ls.py");
HttpsURLConnection conn = (HttpsURLConnection)url.openConnection();
conn.setSSLSocketFactory(sslsocketfactory);
InputStream inputstream = conn.getInputStream();
InputStreamReader inputstreamreader = new InputStreamReader(inputstream);
BufferedReader bufferedreader = new BufferedReader(inputstreamreader);

String string = null;
while ((string = bufferedreader.readLine()) != null) {
    System.out.println("Received " + string);
}
Jan
źródło
Użyłem adresu URL, takiego jak: localhost: 8443 / nazwa_aplikacji / getAttributes . Mam metodę z mapowaniem adresu URL / getAttribute. Ta metoda zwraca listę elementów. Użyłem HttpsUrlConnection, kod odpowiedzi połączenia to 200, ale nie daje mi listy atrybutów, gdy używam inputStream, daje mi zawartość html mojej strony logowania. Zrobiłem uwierzytelnianie i ustawiłem typ zawartości na JSON. Proszę zasugerować
Deepak
83

Chociaż nie jest to zalecane, możesz też całkowicie wyłączyć sprawdzanie poprawności certyfikatu SSL:

import javax.net.ssl.*;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;

public class SSLTool {

  public static void disableCertificateValidation() {
    // Create a trust manager that does not validate certificate chains
    TrustManager[] trustAllCerts = new TrustManager[] { 
      new X509TrustManager() {
        public X509Certificate[] getAcceptedIssuers() { 
          return new X509Certificate[0]; 
        }
        public void checkClientTrusted(X509Certificate[] certs, String authType) {}
        public void checkServerTrusted(X509Certificate[] certs, String authType) {}
    }};

    // Ignore differences between given hostname and certificate hostname
    HostnameVerifier hv = new HostnameVerifier() {
      public boolean verify(String hostname, SSLSession session) { return true; }
    };

    // Install the all-trusting trust manager
    try {
      SSLContext sc = SSLContext.getInstance("SSL");
      sc.init(null, trustAllCerts, new SecureRandom());
      HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
      HttpsURLConnection.setDefaultHostnameVerifier(hv);
    } catch (Exception e) {}
  }
}
neu242
źródło
72
Należy zauważyć, że wyłączenie walidacji certyfikatu w ten sposób otwiera połączenie z możliwymi atakami MITM: nie używaj w produkcji .
Bruno
3
Na szczęście kod się nie kompiluje. To „rozwiązanie” jest radykalnie niepewne.
Markiz Lorne
5
@ neu242, nie, tak naprawdę nie zależy od tego, do czego go używasz. Jeśli chcesz używać SSL / TLS, chcesz zabezpieczyć swoje połączenie przed atakami MITM, o to właśnie chodzi. Uwierzytelnianie serwera nie jest konieczne, jeśli możesz zagwarantować, że nikt nie będzie w stanie zmienić ruchu, ale sytuacje, w których podejrzewasz, że mogą istnieć podsłuchujący, którzy również nie byliby w stanie zmienić ruchu sieciowego, są dość rzadkie.
Bruno
1
@ neu242 Dzięki za fragment kodu. Właściwie myślę o użyciu tego w produkcji do bardzo konkretnego celu (indeksowanie sieci) i wspomniałem o twojej implementacji w pytaniu ( stackoverflow.com/questions/13076511/ ... ). Jeśli masz czas, czy mógłbyś spojrzeć na to i dać mi znać, jeśli są jakieś zagrożenia bezpieczeństwa, które przegapiłem?
Sal
1
@Bruno Jeśli tylko pinguję serwer, czy to naprawdę wpłynie na mnie przez ataki MITM?
PhoonOne
21

Czy ustawiłeś właściwości KeyStore i / lub TrustStore System?

java -Djavax.net.ssl.keyStore=pathToKeystore -Djavax.net.ssl.keyStorePassword=123456

lub z kodu

System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);

To samo z javax.net.ssl.trustStore

Gandalf
źródło
13

Jeśli masz do czynienia z wywołaniem usługi sieci Web przy użyciu struktury Axis, istnieje znacznie prostsza odpowiedź. Jeśli chcesz, aby Twój klient mógł wywoływać usługę sieciową SSL i ignorować błędy certyfikatu SSL, po prostu umieść to oświadczenie przed wywołaniem jakichkolwiek usług internetowych:

System.setProperty("axis.socketSecureFactory", "org.apache.axis.components.net.SunFakeTrustSocketFactory");

Obowiązują zwykłe zastrzeżenia dotyczące tego, że jest to bardzo zła rzecz w środowisku produkcyjnym.

Znalazłem to na wiki Axis .

Mark Meuer
źródło
OP ma do czynienia z HttpsURLConnection, a nie Axis
neu242
2
Rozumiem. Nie zamierzałem sugerować, że moja odpowiedź była lepsza w ogólnym przypadku. To jest tak, że jeśli za pomocą ramy osi, może masz pytanie OP w tym kontekście. (Tak właśnie znalazłem to pytanie w pierwszej kolejności). W takim przypadku sposób, który podałem, jest prostszy.
Mark Meuer
5

Dla mnie to działało przy użyciu Apache HttpComponents ~ HttpClient 4.x:

    KeyStore keyStore  = KeyStore.getInstance("PKCS12");
    FileInputStream instream = new FileInputStream(new File("client-p12-keystore.p12"));
    try {
        keyStore.load(instream, "helloworld".toCharArray());
    } finally {
        instream.close();
    }

    // Trust own CA and all self-signed certs
    SSLContext sslcontext = SSLContexts.custom()
        .loadKeyMaterial(keyStore, "helloworld".toCharArray())
        //.loadTrustMaterial(trustStore, new TrustSelfSignedStrategy()) //custom trust store
        .build();
    // Allow TLSv1 protocol only
    SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(
        sslcontext,
        new String[] { "TLSv1" },
        null,
        SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); //TODO
    CloseableHttpClient httpclient = HttpClients.custom()
        .setHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER) //TODO
        .setSSLSocketFactory(sslsf)
        .build();
    try {

        HttpGet httpget = new HttpGet("https://localhost:8443/secure/index");

        System.out.println("executing request" + httpget.getRequestLine());

        CloseableHttpResponse response = httpclient.execute(httpget);
        try {
            HttpEntity entity = response.getEntity();

            System.out.println("----------------------------------------");
            System.out.println(response.getStatusLine());
            if (entity != null) {
                System.out.println("Response content length: " + entity.getContentLength());
            }
            EntityUtils.consume(entity);
        } finally {
            response.close();
        }
    } finally {
        httpclient.close();
    }

Plik P12 zawiera certyfikat klienta i klucz prywatny klienta, utworzony za pomocą BouncyCastle:

public static byte[] convertPEMToPKCS12(final String keyFile, final String cerFile,
    final String password)
    throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException,
    NoSuchProviderException
{
    // Get the private key
    FileReader reader = new FileReader(keyFile);

    PEMParser pem = new PEMParser(reader);
    PEMKeyPair pemKeyPair = ((PEMKeyPair)pem.readObject());
    JcaPEMKeyConverter jcaPEMKeyConverter = new JcaPEMKeyConverter().setProvider("BC");
    KeyPair keyPair = jcaPEMKeyConverter.getKeyPair(pemKeyPair);

    PrivateKey key = keyPair.getPrivate();

    pem.close();
    reader.close();

    // Get the certificate
    reader = new FileReader(cerFile);
    pem = new PEMParser(reader);

    X509CertificateHolder certHolder = (X509CertificateHolder) pem.readObject();
    java.security.cert.Certificate x509Certificate =
        new JcaX509CertificateConverter().setProvider("BC")
            .getCertificate(certHolder);

    pem.close();
    reader.close();

    // Put them into a PKCS12 keystore and write it to a byte[]
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    KeyStore ks = KeyStore.getInstance("PKCS12", "BC");
    ks.load(null);
    ks.setKeyEntry("key-alias", (Key) key, password.toCharArray(),
        new java.security.cert.Certificate[]{x509Certificate});
    ks.store(bos, password.toCharArray());
    bos.close();
    return bos.toByteArray();
}
EpicPandaForce
źródło
To keyStorezawiera klucz prywatny i certyfikat.
EpicPandaForce
1
Aby kod convertPEMtoP12 działał, musisz uwzględnić te 2 zależności: <dependency> <groupId> org.bouncycastle </groupId> <artifactId> bcprov-jdk15on </artifactId> <version> 1.53 </version> </dependency> < dependency> <groupId> org.bouncycastle </groupId> <artifactId> bcpkix-jdk15on </artifactId> <version> 1.53 </version> </dependency>
BirdOfPrey
@EpicPandaForce Otrzymuję błąd: Caught: org.codehaus.groovy.runtime.typehandling.GroovyCastException: Nie można rzutować obiektu z klasą org.bouncycastle.jcajce.provider.asymmetric.x509.X509CertificateObject 'do klasy' int 'w linii ks. setKeyEntry - wszelkie wskazówki, co może być nie tak
Vishal Biyani
Tak, używasz Groovy zamiast ściśle wpisanego języka. (
formalnie
4

Używam pakietu Apache commons HTTP Client, aby to zrobić w moim obecnym projekcie i działa dobrze z SSL i certyfikatem z podpisem własnym (po zainstalowaniu go w cacerts, jak wspomniałeś). Proszę spojrzeć na to tutaj:

http://hc.apache.org/httpclient-3.x/tutorial.html

http://hc.apache.org/httpclient-3.x/sslguide.html

Taylor Leese
źródło
1
Wydaje się, że pakiet jest całkiem zgrabny, ale klasa, która powinna sprawić, że wszystko będzie działać, `` AuthSSLProtocolSocketFactory '', najwyraźniej nie jest częścią oficjalnej dystrybucji, ani w 4.0beta (pomimo informacji o wydaniu stwierdzających, że tak jest) ani w 3.1. Trochę się z nim zhakowałem i teraz wydaje się, że utknąłem na stałe z zawieszeniem na 5 minut, zanim po prostu zerwie połączenie. To naprawdę dziwne - jeśli załaduję CA i certyfikat klienta do dowolnej przeglądarki, po prostu leci.
stycznia
1
Apache HTTP Client 4 może pobierać SSLContextbezpośrednio, więc możesz skonfigurować to wszystko w ten sposób, zamiast używać AuthSSLProtocolSocketFactory.
Bruno,
1
Czy istnieje sposób na wykonanie wszystkich certyfikatów klienta w pamięci zamiast za pośrednictwem zewnętrznego magazynu kluczy?
Sridhar Sarnobat
4

Myślę, że masz problem z certyfikatem serwera, nie jest to ważny certyfikat (myślę, że to właśnie oznacza w tym przypadku „handshake_failure”):

Zaimportuj certyfikat serwera do magazynu kluczy trustcacerts w środowisku JRE klienta. Można to łatwo zrobić za pomocą narzędzia keytool :

keytool
    -import
    -alias <provide_an_alias>
    -file <certificate_file>
    -keystore <your_path_to_jre>/lib/security/cacerts
sourcerebels
źródło
Spróbowałem posprzątać i zacząć od nowa, ale niepowodzenie uścisku dłoni minęło. Teraz mam tylko 5 minut martwej ciszy, zanim połączenie zostanie zakończone: o
Sty
1

Używając poniższego kodu

-Djavax.net.ssl.keyStoreType=pkcs12

lub

System.setProperty("javax.net.ssl.keyStore", pathToKeyStore);

wcale nie jest wymagane. Nie ma również potrzeby tworzenia własnej niestandardowej fabryki SSL.

Napotkałem również ten sam problem, w moim przypadku był problem polegający na tym, że cały łańcuch certyfikatów nie został zaimportowany do zaufanych magazynów. Importuj certyfikaty za pomocą narzędzia keytool bezpośrednio z certyfikatu głównego, a także możesz otworzyć plik cacerts w notatniku i sprawdzić, czy cały łańcuch certyfikatów został zaimportowany, czy nie. Sprawdź nazwę aliasu, którą podałeś podczas importu certyfikatów, otwórz certyfikaty i zobacz, ile zawiera, taka sama liczba certyfikatów powinna znajdować się w pliku cacerts.

Również plik cacerts powinien być skonfigurowany na serwerze, na którym uruchamiasz swoją aplikację, oba serwery będą się wzajemnie uwierzytelniać za pomocą kluczy publicznych / prywatnych.

Ankur Singhal
źródło
Utworzenie własnej niestandardowej fabryki SSL jest o wiele bardziej skomplikowane i podatne na błędy niż ustawienie dwóch właściwości systemu.
Markiz Lorne