[java] HTTPS를 통한 HttpClient를 사용하여 모든 인증서 신뢰

최근 HttpClientover Https ( here here ) 에 관한 질문을 게시했습니다 . 전진했지만 새로운 문제가 발생했습니다. 마지막 문제와 마찬가지로, 나에게 적합한 예제를 찾을 수없는 것 같습니다. 기본적으로 클라이언트가 인증서를 수락하기를 원하지만 (단 하나의 서버 만 가리 키기 때문에) 계속javax.net.ssl.SSLException: Not trusted server certificate exception.

그래서 이것이 내가 가진 것입니다 :


    public void connect() throws A_WHOLE_BUNCH_OF_EXCEPTIONS {

        HttpPost post = new HttpPost(new URI(PROD_URL));
        post.setEntity(new StringEntity(BODY));

        KeyStore trusted = KeyStore.getInstance("BKS");
        trusted.load(null, "".toCharArray());
        SSLSocketFactory sslf = new SSLSocketFactory(trusted);
        sslf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

        SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(new Scheme ("https", sslf, 443));
        SingleClientConnManager cm = new SingleClientConnManager(post.getParams(),
                schemeRegistry);

        HttpClient client = new DefaultHttpClient(cm, post.getParams());
        HttpResponse result = client.execute(post);
    }

그리고 내가받는 오류는 다음과 같습니다.

    W/System.err(  901): javax.net.ssl.SSLException: Not trusted server certificate
    W/System.err(  901):    at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:360)
    W/System.err(  901):    at org.apache.http.conn.ssl.AbstractVerifier.verify(AbstractVerifier.java:92)
    W/System.err(  901):    at org.apache.http.conn.ssl.SSLSocketFactory.connectSocket(SSLSocketFactory.java:321)
    W/System.err(  901):    at org.apache.http.impl.conn.DefaultClientConnectionOperator.openConnection(DefaultClientConnectionOperator.java:129)
    W/System.err(  901):    at org.apache.http.impl.conn.AbstractPoolEntry.open(AbstractPoolEntry.java:164)
    W/System.err(  901):    at org.apache.http.impl.conn.AbstractPooledConnAdapter.open(AbstractPooledConnAdapter.java:119)
    W/System.err(  901):    at org.apache.http.impl.client.DefaultRequestDirector.execute(DefaultRequestDirector.java:348)
    W/System.err(  901):    at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:555)
    W/System.err(  901):    at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:487)
    W/System.err(  901):    at org.apache.http.impl.client.AbstractHttpClient.execute(AbstractHttpClient.java:465)
    W/System.err(  901):    at me.harrisonlee.test.ssl.MainActivity.connect(MainActivity.java:129)
    W/System.err(  901):    at me.harrisonlee.test.ssl.MainActivity.access$0(MainActivity.java:77)
    W/System.err(  901):    at me.harrisonlee.test.ssl.MainActivity$2.run(MainActivity.java:49)
    W/System.err(  901): Caused by: java.security.cert.CertificateException: java.security.InvalidAlgorithmParameterException: the trust anchors set is empty
    W/System.err(  901):    at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:157)
    W/System.err(  901):    at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:355)
    W/System.err(  901):    ... 12 more
    W/System.err(  901): Caused by: java.security.InvalidAlgorithmParameterException: the trust anchors set is empty
    W/System.err(  901):    at java.security.cert.PKIXParameters.checkTrustAnchors(PKIXParameters.java:645)
    W/System.err(  901):    at java.security.cert.PKIXParameters.<init>(PKIXParameters.java:89)
    W/System.err(  901):    at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.<init>(TrustManagerImpl.java:89)
    W/System.err(  901):    at org.apache.harmony.xnet.provider.jsse.TrustManagerFactoryImpl.engineGetTrustManagers(TrustManagerFactoryImpl.java:134)
    W/System.err(  901):    at javax.net.ssl.TrustManagerFactory.getTrustManagers(TrustManagerFactory.java:226)W/System.err(  901):     at org.apache.http.conn.ssl.SSLSocketFactory.createTrustManagers(SSLSocketFactory.java:263)
    W/System.err(  901):    at org.apache.http.conn.ssl.SSLSocketFactory.<init>(SSLSocketFactory.java:190)
    W/System.err(  901):    at org.apache.http.conn.ssl.SSLSocketFactory.<init>(SSLSocketFactory.java:216)
    W/System.err(  901):    at me.harrisonlee.test.ssl.MainActivity.connect(MainActivity.java:107)
    W/System.err(  901):    ... 2 more



답변

참고 : 완전히 신뢰할 수없는 네트워크에서 사용할 프로덕션 코드에는 이것을 구현하지 마십시오. 특히 공공 인터넷을 통한 모든 것.

당신의 질문은 내가 알고 싶은 것입니다. 몇 가지 검색을 한 후 결론은 다음과 같습니다.

HttpClient 방식에서는 org.apache.http.conn.ssl.SSLSocketFactory 자체가 아닌 org.apache.http.conn.ssl.SSLSocketFactory에서 사용자 정의 클래스를 작성해야합니다. 이 게시물에서 몇 가지 단서를 찾을 수 있습니다. 사용자 정의 SSL 처리는 Android 2.2 FroYo에서 작동이 중지되었습니다 .

예를 들면 …

import java.io.IOException;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.conn.ssl.SSLSocketFactory;
public class MySSLSocketFactory extends SSLSocketFactory {
    SSLContext sslContext = SSLContext.getInstance("TLS");

    public MySSLSocketFactory(KeyStore truststore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
        super(truststore);

        TrustManager tm = new X509TrustManager() {
            public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            }

            public X509Certificate[] getAcceptedIssuers() {
                return null;
            }
        };

        sslContext.init(null, new TrustManager[] { tm }, null);
    }

    @Override
    public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException, UnknownHostException {
        return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
    }

    @Override
    public Socket createSocket() throws IOException {
        return sslContext.getSocketFactory().createSocket();
    }
}

HttpClient의 인스턴스를 만드는 동안이 클래스를 사용하십시오.

public HttpClient getNewHttpClient() {
    try {
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(null, null);

        MySSLSocketFactory sf = new MySSLSocketFactory(trustStore);
        sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);

        HttpParams params = new BasicHttpParams();
        HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);
        HttpProtocolParams.setContentCharset(params, HTTP.UTF_8);

        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        registry.register(new Scheme("https", sf, 443));

        ClientConnectionManager ccm = new ThreadSafeClientConnManager(params, registry);

        return new DefaultHttpClient(ccm, params);
    } catch (Exception e) {
        return new DefaultHttpClient();
    }
}

BTW, 아래 링크는 HttpURLConnection 솔루션을 찾는 사람을위한 것입니다.
Https 연결 안드로이드

나는 위의 두 가지 솔루션을 froyo에서 테스트했으며 모두 내 경우에는 매력처럼 작동합니다. 마지막으로 HttpURLConnection을 사용하면 리디렉션 문제가 발생할 수 있지만 이는 주제를 벗어납니다.

참고 : 모든 인증서를 신뢰하기로 결정하기 전에 해당 사이트를 완전히 알고 있어야하며 최종 사용자에게 해를 끼치 지 않을 것입니다.

실제로, 다음의 의견에서 언급 한 해커의 모의 사이트의 영향을 포함하여 위험을 신중하게 고려해야합니다. 어떤 상황에서는 모든 인증서를 관리하기가 어려울 수 있지만 모든 인증서를 신뢰하는 암시적인 단점을 더 잘 알고있을 것입니다.


답변

기본적으로 httpclient를 사용하여 Android에서 “신뢰할 수 없음”예외를 해결하는 네 가지 가능한 솔루션이 있습니다.

  1. 모든 인증서를 신뢰하십시오. 당신이하고있는 일을 정말로 알지 못한다면 이것을하지 마십시오.
  2. 인증서 만 신뢰하는 사용자 정의 SSLSocketFactory를 작성하십시오. 연결하려는 서버를 정확히 알고있는 한 작동하지만 다른 SSL 인증서를 사용하여 새 서버에 연결해야하는 즉시 앱을 업데이트해야합니다.
  3. Android의 “마스터 목록”인증서가 포함 된 키 저장소 파일을 작성한 후 직접 저장하십시오. 해당 인증서 중 어느 것이라도 만료되면 앱에서 업데이트해야합니다. 나는 이것을 할 이유를 생각할 수 없다.
  4. 내장 인증서 KeyStore를 사용하는 사용자 정의 SSLSocketFactory를 작성하십시오. 그러나 기본값으로 확인하지 못하는 경우 대체 KeyStore로 대체됩니다.

이 답변은 솔루션 # 4를 사용합니다.

해결책은 여러 키 저장소를 승인 할 수있는 SSLSocketFactory를 사용하여 고유 한 인증서를 고유 한 키 저장소에 제공 할 수 있습니다. 이를 통해 일부 Android 기기에서 누락 될 수있는 Thawte와 같은 추가 최상위 인증서를로드 할 수 있습니다. 또한 자체 서명 된 인증서도로드 할 수 있습니다. 내장 된 기본 장치 인증서를 먼저 사용하고 필요한 경우에만 추가 인증서로 대체합니다.

먼저 KeyStore에서 누락 된 인증서를 확인해야합니다. 다음 명령을 실행하십시오.

openssl s_client -connect www.yourserver.com:443

그리고 당신은 다음과 같은 출력을 볼 수 있습니다 :

Certificate chain
 0 s:/O=www.yourserver.com/OU=Go to
   https://www.thawte.com/repository/index.html/OU=Thawte SSL123 
   certificate/OU=Domain Validated/CN=www.yourserver.com
   i:/C=US/O=Thawte, Inc./OU=Domain Validated SSL/CN=Thawte DV SSL CA
 1 s:/C=US/O=Thawte, Inc./OU=Domain Validated SSL/CN=Thawte DV SSL CA
   i:/C=US/O=thawte, Inc./OU=Certification Services Division/OU=(c)
   2006 thawte, Inc. - For authorized use only/CN=thawte Primary Root CA

보시다시피 루트 인증서는 Thawte의 것입니다. 공급자의 웹 사이트로 이동하여 해당 인증서를 찾으십시오. 우리에게는 여기 에 있으며, 필요한 것은 Copyright 2006이라는 것을 알 수 있습니다.

자체 서명 인증서를 사용하는 경우 서명 인증서가 이미 있으므로 이전 단계를 수행하지 않아도됩니다.

그런 다음 누락 된 서명 인증서가 포함 된 키 저장소 파일을 작성하십시오. Crazybob은 Android 에서이 작업을 수행하는 방법에 대한 세부 정보를 가지고 있지만 아이디어는 다음과 같습니다.

Bouncy Castle 제공자 라이브러리가없는 경우 http://www.bouncycastle.org/latest_releases.html 에서 다운로드 하십시오 . 이것은 아래 클래스 경로로 진행됩니다.

서버에서 인증서를 추출하고 pem 파일을 작성하는 명령을 실행하십시오. 이 경우 mycert.pem입니다.

echo | openssl s_client -connect ${MY_SERVER}:443 2>&1 | \
 sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > mycert.pem

그런 다음 다음 명령을 실행하여 키 저장소를 작성하십시오.

export CLASSPATH=/path/to/bouncycastle/bcprov-jdk15on-155.jar
CERTSTORE=res/raw/mystore.bks
if [ -a $CERTSTORE ]; then
    rm $CERTSTORE || exit 1
fi
keytool \
      -import \
      -v \
      -trustcacerts \
      -alias 0 \
      -file <(openssl x509 -in mycert.pem) \
      -keystore $CERTSTORE \
      -storetype BKS \
      -provider org.bouncycastle.jce.provider.BouncyCastleProvider \
      -providerpath /path/to/bouncycastle/bcprov-jdk15on-155.jar \
      -storepass some-password

위의 스크립트는 결과를에 배치한다는 것을 알 수 있습니다 res/raw/mystore.bks. 이제 누락 된 인증서를 제공하는 Android 앱에로드 할 파일이 있습니다.

이렇게하려면 SSL 구성표에 SSLSocketFactory를 등록하십시오.

final SchemeRegistry schemeRegistry = new SchemeRegistry();
schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
schemeRegistry.register(new Scheme("https", createAdditionalCertsSSLSocketFactory(), 443));

// and then however you create your connection manager, I use ThreadSafeClientConnManager
final HttpParams params = new BasicHttpParams();
...
final ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(params,schemeRegistry);

SSLSocketFactory를 작성하려면 다음을 수행하십시오.

protected org.apache.http.conn.ssl.SSLSocketFactory createAdditionalCertsSSLSocketFactory() {
    try {
        final KeyStore ks = KeyStore.getInstance("BKS");

        // the bks file we generated above
        final InputStream in = context.getResources().openRawResource( R.raw.mystore);
        try {
            // don't forget to put the password used above in strings.xml/mystore_password
            ks.load(in, context.getString( R.string.mystore_password ).toCharArray());
        } finally {
            in.close();
        }

        return new AdditionalKeyStoresSSLSocketFactory(ks);

    } catch( Exception e ) {
        throw new RuntimeException(e);
    }
}

마지막으로 AdditionalKeyStoresSSLSocketFactory 코드는 새 KeyStore를 승인하고 내장 KeyStore가 SSL 인증서의 유효성 검증에 실패하는지 확인합니다.

/**
 * Allows you to trust certificates from additional KeyStores in addition to
 * the default KeyStore
 */
public class AdditionalKeyStoresSSLSocketFactory extends SSLSocketFactory {
    protected SSLContext sslContext = SSLContext.getInstance("TLS");

    public AdditionalKeyStoresSSLSocketFactory(KeyStore keyStore) throws NoSuchAlgorithmException, KeyManagementException, KeyStoreException, UnrecoverableKeyException {
        super(null, null, null, null, null, null);
        sslContext.init(null, new TrustManager[]{new AdditionalKeyStoresTrustManager(keyStore)}, null);
    }

    @Override
    public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
        return sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
    }

    @Override
    public Socket createSocket() throws IOException {
        return sslContext.getSocketFactory().createSocket();
    }



    /**
     * Based on http://download.oracle.com/javase/1.5.0/docs/guide/security/jsse/JSSERefGuide.html#X509TrustManager
     */
    public static class AdditionalKeyStoresTrustManager implements X509TrustManager {

        protected ArrayList<X509TrustManager> x509TrustManagers = new ArrayList<X509TrustManager>();


        protected AdditionalKeyStoresTrustManager(KeyStore... additionalkeyStores) {
            final ArrayList<TrustManagerFactory> factories = new ArrayList<TrustManagerFactory>();

            try {
                // The default Trustmanager with default keystore
                final TrustManagerFactory original = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                original.init((KeyStore) null);
                factories.add(original);

                for( KeyStore keyStore : additionalkeyStores ) {
                    final TrustManagerFactory additionalCerts = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                    additionalCerts.init(keyStore);
                    factories.add(additionalCerts);
                }

            } catch (Exception e) {
                throw new RuntimeException(e);
            }



            /*
             * Iterate over the returned trustmanagers, and hold on
             * to any that are X509TrustManagers
             */
            for (TrustManagerFactory tmf : factories)
                for( TrustManager tm : tmf.getTrustManagers() )
                    if (tm instanceof X509TrustManager)
                        x509TrustManagers.add( (X509TrustManager)tm );


            if( x509TrustManagers.size()==0 )
                throw new RuntimeException("Couldn't find any X509TrustManagers");

        }

        /*
         * Delegate to the default trust manager.
         */
        public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            final X509TrustManager defaultX509TrustManager = x509TrustManagers.get(0);
            defaultX509TrustManager.checkClientTrusted(chain, authType);
        }

        /*
         * Loop over the trustmanagers until we find one that accepts our server
         */
        public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
            for( X509TrustManager tm : x509TrustManagers ) {
                try {
                    tm.checkServerTrusted(chain,authType);
                    return;
                } catch( CertificateException e ) {
                    // ignore
                }
            }
            throw new CertificateException();
        }

        public X509Certificate[] getAcceptedIssuers() {
            final ArrayList<X509Certificate> list = new ArrayList<X509Certificate>();
            for( X509TrustManager tm : x509TrustManagers )
                list.addAll(Arrays.asList(tm.getAcceptedIssuers()));
            return list.toArray(new X509Certificate[list.size()]);
        }
    }

}


답변

이 코드를 앞에 추가하면 HttpsURLConnection완료됩니다. 알았어

private void trustEveryone() {
    try {
            HttpsURLConnection.setDefaultHostnameVerifier(new HostnameVerifier(){
                    public boolean verify(String hostname, SSLSession session) {
                            return true;
                    }});
            SSLContext context = SSLContext.getInstance("TLS");
            context.init(null, new X509TrustManager[]{new X509TrustManager(){
                    public void checkClientTrusted(X509Certificate[] chain,
                                    String authType) throws CertificateException {}
                    public void checkServerTrusted(X509Certificate[] chain,
                                    String authType) throws CertificateException {}
                    public X509Certificate[] getAcceptedIssuers() {
                            return new X509Certificate[0];
                    }}}, new SecureRandom());
            HttpsURLConnection.setDefaultSSLSocketFactory(
                            context.getSocketFactory());
    } catch (Exception e) { // should never happen 
            e.printStackTrace();
    }
} 

이것이 도움이되기를 바랍니다.


답변

이것은 나쁜 생각입니다. 모든 인증서를 신뢰하는 것은 SSL을 전혀 사용하지 않는 것보다 약간 좋습니다. “내 서버가 하나의 서버 만 가리 키기 때문에 클라이언트가 모든 인증서를 수락하기를 원합니다”라고 말하면 이것은 “하나의 서버”를 가리키는 것이 안전하며 공용 네트워크에 있지 않다는 것을 의미합니다.

인증서를 신뢰하면 중간자 공격에 완전히 개방됩니다. 사용자와 최종 서버와 별도의 SSL 연결을 설정하여 누구나 연결을 프록시 할 수 있습니다. 그런 다음 MITM은 전체 요청 및 응답에 액세스 할 수 있습니다. 처음에 실제로 SSL이 필요하지 않은 경우 (메시지에 민감한 것은 없으며 인증을 수행하지 않음) 모든 인증서를 맹목적으로 신뢰해서는 안됩니다.

keytool을 사용하여 공개 인증서를 jks에 추가하고 소켓 팩토리를 빌드하는 데 다음과 같이 사용하십시오.

    KeyStore ks = KeyStore.getInstance("JKS");

    // get user password and file input stream
    char[] password = ("mykspassword")).toCharArray();
    ClassLoader cl = this.getClass().getClassLoader();
    InputStream stream = cl.getResourceAsStream("myjks.jks");
    ks.load(stream, password);
    stream.close();

    SSLContext sc = SSLContext.getInstance("TLS");
    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");

    kmf.init(ks, password);
    tmf.init(ks);

    sc.init(kmf.getKeyManagers(), tmf.getTrustManagers(),null);

    return sc.getSocketFactory();

여기에는주의해야 할 사항이 하나 있습니다. 인증서는 결국 만료되고 코드는 그때 작동을 멈 춥니 다. 인증서를 보면 언제 이런 일이 일어날 지 쉽게 결정할 수 있습니다.


답변

API 8부터이 방법으로 테스트 목적으로 HttpURLConnection SSL 검사를 비활성화 할 수 있습니다.

    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    if (conn instanceof HttpsURLConnection) {
        HttpsURLConnection httpsConn = (HttpsURLConnection) conn;
        httpsConn.setSSLSocketFactory(SSLCertificateSocketFactory.getInsecure(0, null));
        httpsConn.setHostnameVerifier(new AllowAllHostnameVerifier());
    }


답변

HttpComponents의 API가 변경되었습니다. 아래 코드와 함께 작동합니다.

public static HttpClient getTestHttpClient() {
    try {
        SSLSocketFactory sf = new SSLSocketFactory(new TrustStrategy(){
            @Override
            public boolean isTrusted(X509Certificate[] chain,
                    String authType) throws CertificateException {
                return true;
            }
        }, new AllowAllHostnameVerifier());

        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("https",8444, sf));
        ClientConnectionManager ccm = new ThreadSafeClientConnManager(registry);
        return new DefaultHttpClient(ccm);
    } catch (Exception e) {
        e.printStackTrace();
        return new DefaultHttpClient();
    }
}


답변

https://stackoverflow.com/a/6378872/1553004의 위 코드 는 호스트 이름 검증자를 호출해야한다는 점을 제외하고는 정확합니다.

    @Override
public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException {
    SSLSocket sslSocket = (SSLSocket)sslContext.getSocketFactory().createSocket(socket, host, port, autoClose);
    getHostnameVerifier().verify(host, sslSocket);
    return sslSocket;
}

이 수정 프로그램을 추가하기 위해 명시 적으로 stackoverflow에 가입했습니다. 내 경고에 유의하십시오!