web-dev-qa-db-ja.com

Androidアプリでのクライアント証明書とのHTTPS接続

Androidアプリで、現在機能しているHTTP接続をHTTPS接続に置き換えようとしています。HTTPS接続のセキュリティを強化する必要があるため、この手順は無視できません。

私は以下を持っています:

  1. HTTPS接続を確立するように構成され、クライアント証明書を必要とするサーバー
    • このサーバーには、標準の大規模CAによって発行された証明書があります。つまり、Androidのブラウザーを介してこの接続にアクセスすると、デバイスのトラストストアがCAを認識するため、正常に機能します。 (したがって、自己署名されていません)
  2. 基本的に自己署名されたクライアント証明書。 (内部CAによって発行されます)
  3. Androidこのクライアント証明書をロードして前述のサーバーへの接続を試みるアプリですが、次の問題/プロパティがあります:
    • サーバーがクライアント証明書を要求するように構成されていない場合、クライアントはサーバーに接続できます。基本的に、私がSSLSocketFactory.getSocketFactory()を使用する場合、接続は正常に機能しますが、クライアント証明書はこのアプリケーション仕様の必須部分であるため、次のようになります。
    • カスタムSSLSocketFactoryに接続しようとすると、クライアントが_javax.net.ssl.SSLPeerUnverifiedException: No peer certificate_例外を生成しますが、理由は完全にはわかりません。この例外は、これに対するさまざまな解決策をインターネット上で検索した後、少しあいまいに見えます。

クライアントの関連コードは次のとおりです。

_SSLSocketFactory socketFactory = null;

public void onCreate(Bundle savedInstanceState) {
    loadCertificateData();
}

private void loadCertificateData() {
    try {
        File[] pfxFiles = Environment.getExternalStorageDirectory().listFiles(new FileFilter() {
            public boolean accept(File file) {
                if (file.getName().toLowerCase().endsWith("pfx")) {
                    return true;
                }
                return false;
            }
        });

        InputStream certificateStream = null;
        if (pfxFiles.length==1) {
            certificateStream = new FileInputStream(pfxFiles[0]);
        }

        KeyStore keyStore = KeyStore.getInstance("PKCS12");
        char[] password = "somePassword".toCharArray();
        keyStore.load(certificateStream, password);

        System.out.println("I have loaded [" + keyStore.size() + "] certificates");

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, password);

        socketFactory = new SSLSocketFactory(keyStore);
    } catch (Exceptions e) {
        // Actually a bunch of catch blocks here, but shortened!
    }
}

private void someMethodInvokedToEstablishAHttpsConnection() {
    try {
        HttpParams standardParams = new BasicHttpParams();
        HttpConnectionParams.setConnectionTimeout(standardParams, 5000);
        HttpConnectionParams.setSoTimeout(standardParams, 30000);

        SchemeRegistry schRegistry = new SchemeRegistry();
        schRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        schRegistry.register(new Scheme("https", socketFactory, 443));
        ClientConnectionManager connectionManager = new ThreadSafeClientConnManager(standardParams, schRegistry);

        HttpClient client = new DefaultHttpClient(connectionManager, standardParams);
        HttpPost request = new HttpPost();
        request.setURI(new URI("https://TheUrlOfTheServerIWantToConnectTo));
        request.setEntity("Some set of data used by the server serialized into string format");
        HttpResponse response = client.execute(request);
        resultData = EntityUtils.toString(response.getEntity());
    } catch (Exception e) {
        // Catch some exceptions (Actually multiple catch blocks, shortened)
    }
}
_

私は確認しました。確かに、keyStoreは証明書をロードし、すべて満足しています。

私はHTTPS/SSL接続について読んでいないことに関して2つの理論を持っていますが、これは本当に私の最初の試みなので、この問題を実際に解決するために何が必要かについて少し戸惑っています。

私の知る限り、最初の可能性は、標準の中間およびエンドポイント認証局をすべて含むデバイスのトラストストアを使用して、このSSLSocketFactoryを構成する必要があることです。つまり、デバイスのデフォルトのSSLSocketFactory.getSocketFactory()は、証明書を送信するときにサーバーを信頼するために使用されるファクトリのトラストストアにCAのセットをロードします。これは、コードで失敗しているためです。トラストストアを適切にロードしてください。これが本当なら、このデータをロードするにはどうすればよいですか?

2番目の可能性は、クライアント証明書が自己署名されている(または内部認証局によって発行されている-私が間違っている場合は修正してください。ただし、ここでのすべての意図と目的にとって、これらは実際には同じものです) 。実際にはthisトラストストアが欠落しており、基本的にサーバーが内部CAで証明書を検証し、この内部CAであることを検証する方法を提供する必要があります実際には「信頼できる」です。これが本当なら、どのようなことを私は探しているのですか? here のように、これが私の問題であると私に思わせるこれへの言及をいくつか見ましたが、私は本当に確信がありません。これが本当に私の問題である場合、内部CAを管理している担当者に何を求めますか?次に、これをコードに追加して、HTTPS接続が機能するようにするにはどうすればよいですか?

3番目の解決策は、うまくいけばあまりうまくいかないかもしれませんが、私はここである点について完全に間違っており、重要なステップを逃したか、現在私が知らないHTTPS/SSLの部分を完全に無視しています。これが事実である場合、私が行き、私が学ぶ必要があることを学ぶことができるように、少しの方向性を教えていただけませんか?

読んでくれてありがとう!

31
Kevek

これは確かに問題だと思います。

私が知る限り、最初の可能性は、標準の中間およびエンドポイント認証局をすべて含むデバイスのトラストストアを使用して、このSSLSocketFactoryを構成する必要があることです。

これが本当なら、このデータをロードするにはどうすればよいですか?

次のようなことを試してください(このデフォルトの信頼マネージャーを使用するには、ソケットファクトリを取得する必要があります)。

X509TrustManager manager = null;
FileInputStream fs = null;

TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());

try
{
    fs = new FileInputStream(System.getProperty("javax.net.ssl.trustStore")); 
    keyStore.load(fs, null);
}
finally
{
    if (fs != null) { fs.close(); }
}

trustManagerFactory.init(keyStore);
TrustManager[] managers = trustManagerFactory.getTrustManagers();

for (TrustManager tm : managers)
{
    if (tm instanceof X509TrustManager) 
    {
        manager = (X509TrustManager) tm;
        break;
    }
}

編集:ここのコードを使用する前に、Pooksの回答をご覧ください。これを行うには今より良い方法があるようです。

7
jglouie

@jglouieのソリューションを実装する簡単な方法があります。基本的に、SSLContextを使用し、トラストマネージャーパラメーターのnullで初期化する場合、デフォルトのトラストマネージャーを使用してSSLコンテキストを取得する必要があります。これはAndroidドキュメントに記載されていませんが、 SSLContext.initのJavaドキュメント

最初の2つのパラメーターのいずれかがnullの場合があります。その場合、インストールされているセキュリティプロバイダーが検索され、適切なファクトリの最も優先度の高い実装が検索されます。

コードは次のようになります。

// This can be any protocol supported by your target devices.
// For example "TLSv1.2" is supported by the latest versions of Android
final String SSL_PROTOCOL = "TLS";

try {               
   sslContext = SSLContext.getInstance(SSL_PROTOCOL);

   // Initialize the context with your key manager and the default trust manager 
   // and randomness source
   sslContext.init(keyManagerFactory.getKeyManagers(), null, null);
} catch (NoSuchAlgorithmException e) {
   Log.e(TAG, "Specified SSL protocol not supported! Protocol=" + SSL_PROTOCOL);
   e.printStackTrace();
} catch (KeyManagementException e) {
   Log.e(TAG, "Error setting up the SSL context!");
   e.printStackTrace();
}

// Get the socket factory
socketFactory = sslContext.getSocketFactory();
8
Pooks

私は2、3日試してみましたが、ようやく回答が得られたので、他の人を助けるために、ここに私の手順とすべてのコードを投稿したいと思います。

1)接続したいサイトの証明書を取得する

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

2)鍵を作成するには、BouncyCastleライブラリが必要です ここ

keytool -import -v -trustcacerts -alias 0 -file mycert.pem -keystore “store_directory/mykst“ -storetype BKS -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath “directory_of_bouncycastle/bcprov-jdk16-145.jar” -storepass mypassword

3)キーが作成されたかどうかを確認する

keytool -list -keystore "carpeta_almacen/mykst" -provider org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath "directory_of_bouncycastle/bcprov-jdk16-145.jar" -storetype BKS -storepass mypassword

そして、あなたはこのようなものを見るはずです:

アルマセンクレーブのティポ:BKSアルマセンクレーブの教授:紀元前

Sualmacénde claves contiene entrada 1

0、07-dic-2011、trustedCertEntry、

Huella digital de certificado(MD5):

55:FD:E5:E3:8A:4C:D6:B8:69:EB:6A:49:05:5F:18:48

4)次に、「mykst」ファイルをAndroid=プロジェクト)のディレクトリ「res/raw」(存在しない場合は作成)にコピーする必要があります。

5)Androidマニフェストに権限を追加します

  <uses-permission Android:name="Android.permission.INTERNET"/>

6)ここにコード!

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:Android="http://schemas.Android.com/apk/res/Android"
    Android:layout_width="fill_parent"
    Android:layout_height="fill_parent"
    Android:orientation="vertical"
    Android:padding="10dp" >

    <Button
        Android:id="@+id/button"
        Android:layout_width="fill_parent"
        Android:layout_height="wrap_content"
        Android:text="Cargar contenido" />

    <RelativeLayout
        Android:layout_width="fill_parent"
        Android:layout_height="fill_parent"
        Android:background="#4888ef">
        <ProgressBar
            Android:id="@+id/loading"
            Android:layout_width="50dp"
            Android:layout_height="50dp"
            Android:indeterminate="true"
            Android:layout_centerInParent="true"
            Android:visibility="gone"/>
        <ScrollView
            Android:layout_width="fill_parent"
            Android:layout_height="fill_parent"
            Android:fillViewport="true"
            Android:padding="10dp">
            <TextView
                Android:id="@+id/output"
                Android:layout_width="fill_parent"
                Android:layout_height="fill_parent"
                Android:textColor="#FFFFFF"/>
        </ScrollView>
    </RelativeLayout>
</LinearLayout>

MyHttpClient

package com.example.https;


import Java.io.FileInputStream;
import Java.io.IOException;
import Java.io.InputStream;
import Java.security.KeyStore;
import Java.security.cert.X509Certificate;
import Java.util.Date;
import Java.util.Enumeration;

import org.Apache.http.conn.ClientConnectionManager;
import org.Apache.http.conn.scheme.PlainSocketFactory;
import org.Apache.http.conn.scheme.Scheme;
import org.Apache.http.conn.scheme.SchemeRegistry;
import org.Apache.http.conn.ssl.SSLSocketFactory;
import org.Apache.http.impl.client.DefaultHttpClient;
import org.Apache.http.impl.conn.SingleClientConnManager;

import Android.content.Context;
import Android.os.Build;

import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.TrustManagerFactory;

public class MyHttpClient extends DefaultHttpClient {

    final Context context;

    public MyHttpClient(Context context) {
        this.context = context;
    }

    @Override
    protected ClientConnectionManager createClientConnectionManager() {
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        // Register for port 443 our SSLSocketFactory with our keystore
        // to the ConnectionManager
        registry.register(new Scheme("https", newSslSocketFactory(), 443));
        return new SingleClientConnManager(getParams(), registry);
    }

    private SSLSocketFactory newSslSocketFactory() {
        try {
            // Trust manager / truststore
            KeyStore trustStore=KeyStore.getInstance(KeyStore.getDefaultType());

            // If we're on an OS version prior to Ice cream sandwich (4.0) then use the standard way to get the system
            //   trustStore -- System.getProperty() else we need to use the special name to get the trustStore KeyStore
            //   instance as they changed their trustStore implementation.
            if (Build.VERSION.RELEASE.compareTo("4.0") < 0) {
                TrustManagerFactory trustManagerFactory=TrustManagerFactory
                        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
                FileInputStream trustStoreStream=new FileInputStream(System.getProperty("javax.net.ssl.trustStore"));
                trustStore.load(trustStoreStream, null);
                trustManagerFactory.init(trustStore);
                trustStoreStream.close();
            } else {
                trustStore=KeyStore.getInstance("AndroidCAStore");
            }

            InputStream certificateStream = context.getResources().openRawResource(R.raw.mykst);
            KeyStore keyStore=KeyStore.getInstance("BKS");
            try {
                keyStore.load(certificateStream, "mypassword".toCharArray());
                Enumeration<String> aliases=keyStore.aliases();
                while (aliases.hasMoreElements()) {
                    String alias=aliases.nextElement();
                    if (keyStore.getCertificate(alias).getType().equals("X.509")) {
                        X509Certificate cert=(X509Certificate)keyStore.getCertificate(alias);
                        if (new Date().after(cert.getNotAfter())) {
                            // This certificate has expired
                            return null;
                        }
                    }
                }
            } catch (IOException ioe) {
                // This occurs when there is an incorrect password for the certificate
                return null;
            } finally {
                certificateStream.close();
            }

            KeyManagerFactory keyManagerFactory=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
            keyManagerFactory.init(keyStore, "mypassword".toCharArray());

            return new SSLSocketFactory(keyStore, "mypassword", trustStore);
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    }
}

MainActivity

package com.example.https;

import Android.app.Activity;
import Android.os.AsyncTask;
import Android.os.Bundle;
import Android.view.View;
import Android.widget.Button;
import Android.widget.TextView;

import org.Apache.http.HttpEntity;
import org.Apache.http.HttpResponse;
import org.Apache.http.client.ClientProtocolException;
import org.Apache.http.client.methods.HttpGet;
import org.Apache.http.impl.client.DefaultHttpClient;

import Java.io.BufferedReader;
import Java.io.IOException;
import Java.io.InputStream;
import Java.io.InputStreamReader;
import Java.io.Reader;
import Java.io.StringWriter;
import Java.io.Writer;

import javax.net.ssl.SSLSocketFactory;

public class MainActivity extends Activity {

    private View loading;
    private TextView output;
    private Button button;

    SSLSocketFactory socketFactory = null;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loading = findViewById(R.id.loading);
        output = (TextView) findViewById(R.id.output);
        button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                new CargaAsyncTask().execute(new Void[0]);
            }
        });
    }

    class CargaAsyncTask extends AsyncTask<Void, Void, String> {

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            loading.setVisibility(View.VISIBLE);
            button.setEnabled(false);
        }

        @Override
        protected String doInBackground(Void... params) {
            // Instantiate the custom HttpClient
            DefaultHttpClient client = new MyHttpClient(getApplicationContext());
            HttpGet get = new HttpGet("https://www.google.com");
            // Execute the GET call and obtain the response
            HttpResponse getResponse;
            String resultado = null;
            try {
                getResponse = client.execute(get);
                HttpEntity responseEntity = getResponse.getEntity();
                InputStream is = responseEntity.getContent();
                resultado = convertStreamToString(is);
            } catch (ClientProtocolException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return resultado;
        }

        @Override
        protected void onPostExecute(String result) {
            super.onPostExecute(result);
            loading.setVisibility(View.GONE);
            button.setEnabled(true);
            if (result == null) {
                output.setText("Error");
            } else {
                output.setText(result);
            }
        }

    }

    public static String convertStreamToString(InputStream is) throws IOException {
        /*
         * To convert the InputStream to String we use the
         * Reader.read(char[] buffer) method. We iterate until the
         * Reader return -1 which means there's no more data to
         * read. We use the StringWriter class to produce the string.
         */
        if (is != null) {
            Writer writer = new StringWriter();

            char[] buffer = new char[1024];
            try {
                Reader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
                int n;
                while ((n = reader.read(buffer)) != -1) {
                    writer.write(buffer, 0, n);
                }
            } finally {
                is.close();
            }
            return writer.toString();
        } else {
            return "";
        }
    }
}

他の方にもお役に立てれば幸いです!! お楽しみください!

2
Camilo9mm

SSLSocketFactoryのホスト名も設定する必要があるようです。

行を追加してみてください

socketFactory.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);

SSLFactoryとの新しい接続を作成する前に。

構造の違いを除けば、同様のコードがあります。私の実装では、上記のコードの大部分と同様に見えるDefaultHttpClientの独自の拡張を作成しました。これで問題が解決しない場合は、その作業コードを投稿して、そのアプローチを試すことができます。

編集:これが私の作業中のバージョンです

    public class ActivateHttpClient extends DefaultHttpClient { 
    final Context context;


    /**
     * Public constructor taking two arguments for ActivateHttpClient.
     * @param context - Context referencing the calling Activity, for creation of
     * the socket factory.
     * @param params - HttpParams passed to this, specifically to set timeouts on the
     * connection.
     */
    public ActivateHttpClient(Context context, HttpParams params) {
        this.setParams(params);
    }


    /* (non-Javadoc)
     * @see org.Apache.http.impl.client.DefaultHttpClient#createClientConnectionManager()
     * Create references for both http and https schemes, allowing us to attach our custom
     * SSLSocketFactory to either
     */
    @Override
    protected ClientConnectionManager createClientConnectionManager() {
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("http", PlainSocketFactory
                .getSocketFactory(), 80));
        registry.register(new Scheme("https", newSslSocketFactory(), 443));
        return new SingleClientConnManager(getParams(), registry);
    }

    /**
     * Creation of new SSLSocketFactory, which imports a certificate from
     * a server which self-signs its own certificate.
     * @return
     */
    protected SSLSocketFactory newSslSocketFactory() {
        try {

            //Keystore must be in BKS (Bouncy Castle Keystore)
            KeyStore trusted = KeyStore.getInstance("BKS");

            //Reference to the Keystore
            InputStream in = context.getResources().openRawResource(
                    R.raw.cert);

            //Password to the keystore
            try {
                trusted.load(in, PASSWORD_HERE.toCharArray());
            } finally {
                in.close();
            }

            // Pass the keystore to the SSLSocketFactory. The factory is
            // responsible
            // for the verification of the server certificate.
            SSLSocketFactory sf = new SSLSocketFactory(trusted);

            // Hostname verification from certificate
            // http://hc.Apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e506
            sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER);
            return sf;

            // return new SSLSocketFactory(trusted);
        } catch (Exception e) {
            e.printStackTrace();
            throw new AssertionError(e);
        }
    }

}

そして、次のように呼び出すことができます:

HttpParams params = new BasicHttpParams();

    // Set the timeout in milliseconds until a connection is established.
    int timeoutConnection = 500;
    HttpConnectionParams.setConnectionTimeout( params , timeoutConnection );

    // Set the default socket timeout (SO_TIMEOUT)
    // in milliseconds which is the timeout for waiting for data.
    int timeoutSocket = 1000;
    HttpConnectionParams.setSoTimeout( params , timeoutSocket );
            //ADD more connection options here!

    String url =
            "https:// URL STRING HERE";
    HttpGet get = new HttpGet( url );

    ActivateHttpClient client =
            new ActivateHttpClient( this.context, params );



    // Try to execute the HttpGet, throwing errors
    // if no response is received, or if there is
    // an error in the execution.
    HTTPResponse response = client.execute( get );
1
syklon

人々はまだこの質問を参照して投票しているので、私は更新された回答を投稿しています。 Android 4.0以降に変更されたものがあるため、ソケットファクトリコードを数回変更する必要がありました。

// Trust manager / truststore
KeyStore trustStore=KeyStore.getInstance(KeyStore.getDefaultType());

// If we're on an OS version prior to Ice cream sandwich (4.0) then use the standard way to get the system
//   trustStore -- System.getProperty() else we need to use the special name to get the trustStore KeyStore
//   instance as they changed their trustStore implementation.
if (Build.VERSION.RELEASE.compareTo("4.0") < 0) {
    TrustManagerFactory trustManagerFactory=TrustManagerFactory
        .getInstance(TrustManagerFactory.getDefaultAlgorithm());
    FileInputStream trustStoreStream=new FileInputStream(System.getProperty("javax.net.ssl.trustStore"));
    trustStore.load(trustStoreStream, null);
    trustManagerFactory.init(trustStore);
    trustStoreStream.close();
} else {
    trustStore=KeyStore.getInstance("AndroidCAStore");
}

InputStream certificateStream=new FileInputStream(userCertFile);
KeyStore keyStore=KeyStore.getInstance("PKCS12");
try {
    keyStore.load(certificateStream, certPass.toCharArray());
    Enumeration<String> aliases=keyStore.aliases();
    while (aliases.hasMoreElements()) {
        String alias=aliases.nextElement();
        if (keyStore.getCertificate(alias).getType().equals("X.509")) {
            X509Certificate cert=(X509Certificate)keyStore.getCertificate(alias);
            if (new Date().after(cert.getNotAfter())) {
                // This certificate has expired
                return;
            }
        }
    }
} catch (IOException ioe) {
    // This occurs when there is an incorrect password for the certificate
    return;
} finally {
    certificateStream.close();
}

KeyManagerFactory keyManagerFactory=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, certPass.toCharArray());

socketFactory=new SSLSocketFactory(keyStore, certPass, trustStore);

うまくいけば、これがまだ将来ここに来る人を助けるでしょう。

1
Kevek