Java HTTPSクライアント証明書認証


222

HTTPS/SSL/TLS証明書を使用して認証するときに、クライアントが正確に何を提示することになっているのか、私はかなり新しく、少し混乱しています。

POST特定のデータに対して単純なデータを実行する必要があるJavaクライアントを作成していますURL。その部分は正常に動作しますが、唯一の問題は、それを介して行われることになっていますHTTPS。このHTTPS部分は(HTTPclientJavaの組み込みHTTPSサポートを使用して、または使用して)かなり簡単に処理できますが、クライアント証明書による認証に行き詰まっています。私はすでに非常によく似た質問がここにあることに気づきました。これはまだ私のコードで試していません(すぐにそうなるでしょう)。私の現在の問題は、私が何をしても、Javaクライアントが証明書を送信しないことです(これをPCAPダンプで確認できます)。

証明書で認証するときにクライアントがサーバーに提示するものを正確に知りたいのですが(特にJavaの場合-それが重要な場合)。これはJKSファイルPKCS#12ですか、それとも?それらにあるはずのもの; クライアント証明書だけですか、それとも鍵ですか?もしそうなら、どのキー?さまざまな種類のファイル、証明書の種類などについては、かなりの混乱があります。

私が初めての前に言ったHTTPS/SSL/TLSように、いくつかの背景情報にも感謝します(エッセイである必要はありません。良い記事へのリンクを用意します)。


私は、キーストアとトラストストアに追加するかをニーズ識別するために、どのようにクライアントからの2つの証明書を与えているあなたはすでに問題の似たようなものを経験してきたように、この問題を特定する助けを喜ばせることができ、この問題私は何をすべきかの手がかり得ていない、実際に調達している のstackoverflowを.com / questions / 61374276 /…
henrycharles

回答:


233

ようやくすべての問題を解決できたので、自分の質問に答えます。これらは、特定の問題を解決するために管理するために使用した設定/ファイルです。

クライアントのキーストアはあるPKCS#12形式の入ったファイル

  1. クライアントの公開証明書(この例では自己署名CAによって署名されています)
  2. クライアントの秘密

これを生成するにpkcs12は、たとえばOpenSSLのコマンドを使用しました。

openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12 -name "Whatever"

ヒント:バージョン0.9.8h ではなく、最新のOpenSSLを入手してください。これは、PKCS#12ファイルを適切に生成できないバグの影響を受けているようです。

このPKCS#12ファイルは、サーバーがクライアントに認証を明示的に要求したときに、Javaクライアントがクライアント証明書をサーバーに提示するために使用します。クライアント証明書認証のプロトコルが実際に機能する方法の概要については、TLSに関するウィキペディアの記事を参照してください(ここでクライアントの秘密鍵が必要な理由についても説明しています)。

クライアントのトラストストアは簡単ですJKS形式の入ったファイルのルートまたは中間CA証明書を。これらのCA証明書は、通信を許可するエンドポイントを決定します。この場合、クライアントは、トラストストアのCAのいずれかによって署名された証明書を提示するサーバーに接続できます。

これを生成するには、たとえば、標準のJava keytoolを使用できます。

keytool -genkey -dname "cn=CLIENT" -alias truststorekey -keyalg RSA -keystore ./client-truststore.jks -keypass whatever -storepass whatever
keytool -import -keystore ./client-truststore.jks -file myca.crt -alias myca

このトラストストアを使用して、クライアントは、で識別されるCAによって署名された証明書を提示するすべてのサーバーとの完全なSSLハンドシェイクを実行しようとしますmyca.crt

上記のファイルはクライアント専用です。サーバーをセットアップする場合も、サーバーには独自のキーおよびトラストストアファイルが必要です。Javaクライアントとサーバー(Tomcatを使用)の両方で完全に機能する例を設定するための優れたウォークスルーは、このWebサイトにあります。

問題/備考/ヒント

  1. クライアント証明書認証は、サーバーによってのみ実施できます。
  2. 重要!)サーバーが(TLSハンドシェイクの一部として)クライアント証明書を要求すると、証明書要求の一部として信頼できるCAのリストも提供されます。あなたは認証のために存在したいクライアント証明書がされている場合ではないこれらのCAさんの1が署名し、(私の意見では、これは奇妙な動作ですが、私は確かにそれの理由がありますよ)すべてで発表されることはありません。これが私の問題の主な原因でした。なぜなら、相手が私の自己署名クライアント証明書を受け入れるようにサーバーを適切に構成しておらず、問題はリクエストでクライアント証明書を適切に提供していないためであると私たちは考えていました。
  3. Wiresharkを入手してください。SSL / HTTPSパケット分析が優れており、問題のデバッグと発見に非常に役立ちます。これ-Djavax.net.debug=sslは、Java SSLデバッグ出力に不満がある場合に似ていますが、より構造化されており、(おそらく)解釈が容易です。
  4. Apache httpclientライブラリを使用することは完全に可能です。httpclientを使用する場合は、宛先URLを同等のHTTPSで置き換え、次のJVM引数を追加します(他のクライアントでも同じです。HTTP/ HTTPSを介したデータの送受信に使用するライブラリに関係ありません)。 :

    -Djavax.net.debug=ssl
    -Djavax.net.ssl.keyStoreType=pkcs12
    -Djavax.net.ssl.keyStore=client.p12
    -Djavax.net.ssl.keyStorePassword=whatever
    -Djavax.net.ssl.trustStoreType=jks
    -Djavax.net.ssl.trustStore=client-truststore.jks
    -Djavax.net.ssl.trustStorePassword=whatever

6
「認証のために提示したいクライアント証明書がこれらのCAのいずれかによって署名されていない場合、まったく提示されません。」証明書はサーバーに受け入れられないことをクライアントが認識しているため、提示されません。また、証明書は中間CA「ICA」によって署名され、サーバーはクライアントにルートCA「RCA」を提示でき、WebブラウザーはRCAではなくICAによって署名されていても証明書を選択できます。
KyleM 2014年

2
上記のコメントの例として、1つのルートCA(RCA1)と2つの中間CA(ICA1およびICA2)がある状況を考えます。Apache Tomcatでは、RCA1をトラストストアにインポートすると、トラストストアにない場合でも、WebブラウザーはICA1およびICA2によって署名されたすべての証明書を提示します。これは、個々の証明書ではなくチェーンが重要であるためです。
KyleM 2014年

2
「私の意見では、これは奇妙な動作ですが、それには理由があると確信しています」。その理由は、それがRFC 2246でそれが言っていることです。クライアントがサーバーに受け入れられない証明書を提示できるようにするのは奇妙なことであり、時間とスペースを完全に浪費します。
ローン侯爵2014

1
上記の例のように、単一のJVM全体のキーストア(およびトラストストア!)を使用しないことを強くお勧めします。単一の接続をカスタマイズする方がより安全で柔軟ですが、もう少しコードを書く必要があります。SSLContext@Magnusの回答のようにをカスタマイズする必要があります。
クリストファーシュルツ

63

他の回答は、クライアント証明書をグローバルに構成する方法を示しています。ただし、JVMで実行されているすべてのアプリケーション全体でクライアントキーをグローバルに定義するのではなく、プログラムで1つの特定の接続のクライアントキーを定義する場合は、次のように独自のSSLContextを構成できます。

String keyPassphrase = "";

KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream("cert-key-pair.pfx"), keyPassphrase.toCharArray());

SSLContext sslContext = SSLContexts.custom()
        .loadKeyMaterial(keyStore, null)
        .build();

HttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build();
HttpResponse response = httpClient.execute(new HttpGet("https://example.com"));

私は使用しなければなりませんでしたsslContext = SSLContexts.custom().loadTrustMaterial(keyFile, PASSWORD).build();。で動作しませんでしたloadKeyMaterial(...)
Conor Svensson、2016年

4
@ConorSvenssonトラストマテリアルは、リモートサーバー証明書を信頼するクライアント用であり、キーマテリアルは、クライアントを信頼するサーバー用です。
Magnus

1
私はこの簡潔で適切な答えが本当に好きです。人々が興味を持っている場合のために、私はここでビルドの説明とともに実際の例を提供します。stackoverflow.com/a/46821977/154527
Alain O'Dea

3
あなたは私の日を救った!私がしなければならなかった追加の変更が1つだけあります。パスワードをloadKeyMaterial(keystore, keyPassphrase.toCharArray())コードで送信します」
AnandShanbhag

1
@peterhはい、Apache httpに固有です。すべてのHTTPライブラリには独自の設定方法がありますが、それらのほとんどは何らかの方法でを使用する必要がありますSSLContext
Magnus

30

これらのJKSファイルは、証明書とキーペアのコンテナーにすぎません。クライアント側の認証シナリオでは、キーのさまざまな部分がここにあります。

  • クライアントのストアは、クライアントの含まれていますプライベートとパブリックキーのペアを。これはキーストアと呼ばれます
  • サーバーのストアは、クライアントの含まれています、公開鍵を。これはトラストストアと呼ばれます。

トラストストアとキーストアの分離は必須ではありませんが、推奨されます。同じ物理ファイルでもかまいません。

2つのストアのファイルシステムの場所を設定するには、次のシステムプロパティを使用します。

-Djavax.net.ssl.keyStore=clientsidestore.jks

そしてサーバー上:

-Djavax.net.ssl.trustStore=serversidestore.jks

クライアントの証明書(公開鍵)をファイルにエクスポートして、サーバーにコピーできるようにするには、次のコマンドを使用します。

keytool -export -alias MYKEY -file publicclientkey.cer -store clientsidestore.jks

クライアントの公開鍵をサーバーのキーストアにインポートするには、次のように使用します(投稿者が述べたように、これはサーバー管理者によってすでに行われています)。

keytool -import -file publicclientkey.cer -store serversidestore.jks

私はおそらくサーバーを制御できないことを述べておかなければなりません。サーバーが公開証明書をインポートしました。そのシステムの管理者から、ハンドシェイク中に証明書を送信できるように証明書を明示的に提供する必要があると言われました(サーバーから明示的に要求されます)。
tmbrggmn 2009年

JKSファイルとして、公開証明書(サーバーが認識しているもの)の公開鍵と秘密鍵が必要です。
sfussenegger、2009年

サンプルコードをありがとう。上記のコードで、「mykey-public.cer」とは正確には何ですか?これはクライアントの公開証明書ですか(私たちは自己署名証明書を使用していますか)?
tmbrggmn 2009年

はい、そうです。コードスニペットのファイルの名前を適宜変更しました。これで明らかになることを願っています。
マラー2009年

Righto、ありがとう。どうやら「キー」と「証明書」は同じ意味で使用されているので、混乱し続けます。
tmbrggmn 2009年

10

Maven pom.xml:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>some.examples</groupId>
    <artifactId>sslcliauth</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>sslcliauth</name>
    <dependencies>
        <dependency>
            <groupId>org.apache.httpcomponents</groupId>
            <artifactId>httpclient</artifactId>
            <version>4.4</version>
        </dependency>
    </dependencies>
</project>

Javaコード:

package some.examples;

import java.io.FileInputStream;
import java.io.IOException;
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.util.logging.Level;
import java.util.logging.Logger;
import javax.net.ssl.SSLContext;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.ssl.SSLContexts;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.apache.http.entity.InputStreamEntity;

public class SSLCliAuthExample {

private static final Logger LOG = Logger.getLogger(SSLCliAuthExample.class.getName());

private static final String CA_KEYSTORE_TYPE = KeyStore.getDefaultType(); //"JKS";
private static final String CA_KEYSTORE_PATH = "./cacert.jks";
private static final String CA_KEYSTORE_PASS = "changeit";

private static final String CLIENT_KEYSTORE_TYPE = "PKCS12";
private static final String CLIENT_KEYSTORE_PATH = "./client.p12";
private static final String CLIENT_KEYSTORE_PASS = "changeit";

public static void main(String[] args) throws Exception {
    requestTimestamp();
}

public final static void requestTimestamp() throws Exception {
    SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(
            createSslCustomContext(),
            new String[]{"TLSv1"}, // Allow TLSv1 protocol only
            null,
            SSLConnectionSocketFactory.getDefaultHostnameVerifier());
    try (CloseableHttpClient httpclient = HttpClients.custom().setSSLSocketFactory(csf).build()) {
        HttpPost req = new HttpPost("https://changeit.com/changeit");
        req.setConfig(configureRequest());
        HttpEntity ent = new InputStreamEntity(new FileInputStream("./bytes.bin"));
        req.setEntity(ent);
        try (CloseableHttpResponse response = httpclient.execute(req)) {
            HttpEntity entity = response.getEntity();
            LOG.log(Level.INFO, "*** Reponse status: {0}", response.getStatusLine());
            EntityUtils.consume(entity);
            LOG.log(Level.INFO, "*** Response entity: {0}", entity.toString());
        }
    }
}

public static RequestConfig configureRequest() {
    HttpHost proxy = new HttpHost("changeit.local", 8080, "http");
    RequestConfig config = RequestConfig.custom()
            .setProxy(proxy)
            .build();
    return config;
}

public static SSLContext createSslCustomContext() throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, KeyManagementException, UnrecoverableKeyException {
    // Trusted CA keystore
    KeyStore tks = KeyStore.getInstance(CA_KEYSTORE_TYPE);
    tks.load(new FileInputStream(CA_KEYSTORE_PATH), CA_KEYSTORE_PASS.toCharArray());

    // Client keystore
    KeyStore cks = KeyStore.getInstance(CLIENT_KEYSTORE_TYPE);
    cks.load(new FileInputStream(CLIENT_KEYSTORE_PATH), CLIENT_KEYSTORE_PASS.toCharArray());

    SSLContext sslcontext = SSLContexts.custom()
            //.loadTrustMaterial(tks, new TrustSelfSignedStrategy()) // use it to customize
            .loadKeyMaterial(cks, CLIENT_KEYSTORE_PASS.toCharArray()) // load client certificate
            .build();
    return sslcontext;
}

}

特定のJVMインストールを使用するすべてのアプリケーションで証明書を使用できるようにする場合は、代わりにこの回答に従ってください
ADTC、2015年

configureRequest()クライアントプロジェクトのプロキシを設定する方法は正しいですか?
シャリーフ2016

はい、それはhttpクライアント構成であり、この場合はプロキシ構成です
kinjelom

馬鹿げた質問があるかもしれませんが、cacert.jksファイルを作成するにはどうすればよいですか?スレッド "main"に例外Exception java.io.FileNotFoundException:。\ cacert.jks(システムは指定されたファイルを見つけることができません)
Patlatus

6

双方向の認証(サーバーとクライアントの証明書)を設定するだけの場合は、これらの2つのリンクを組み合わせることでアクセスできます。

双方向の認証設定:

https://linuxconfig.org/apache-web-server-ssl-authentication

彼らが言及しているopenssl設定ファイルを使用する必要はありません。ただ使う

  • $ openssl genrsa -des3 -out ca.key 4096

  • $ openssl req -new -x509 -days 365 -key ca.key -out ca.crt

独自のCA証明書を生成し、サーバーとクライアントのキーを生成して署名します。

  • $ openssl genrsa -des3 -out server.key 4096

  • $ openssl req -new -key server.key -out server.csr

  • $ openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 100 -out server.crt

そして

  • $ openssl genrsa -des3 -out client.key 4096

  • $ openssl req -new -key client.key -out client.csr

  • $ openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 101 -out client.crt

残りについては、リンクの手順に従ってください。Chromeの証明書の管理は、前述のFirefoxの例と同じように機能します。

次に、次の方法でサーバーをセットアップします。

https://www.digitalocean.com/community/tutorials/how-to-create-a-ssl-certificate-on-apache-for-ubuntu-14-04

サーバーの.crtと.keyはすでに作成されているため、この手順を実行する必要はありません。


1
サーバーのCSR生成ステップにタイプミスがあると考えてください。使用すべきではありserver.keyませんclient.key
凡例

0

Spring Bootで双方向SSL(クライアントとサーバーの証明書)を使用して銀行に接続しました。だからここに私のすべてのステップを説明してください、それが誰かを助けることを願っています(最も簡単な実用的な解決策、私が見つけました):

  1. Sertificateリクエストを生成します。

    • 秘密鍵を生成します。

      openssl genrsa -des3 -passout pass:MY_PASSWORD -out user.key 2048
    • 証明書要求を生成します。

      openssl req -new -key user.key -out user.csr -passin pass:MY_PASSWORD

    キープuser.key(およびパスワード)と証明書要求を送信しuser.csr、私のsertificateのために銀行に

  2. 2つの証明書を受け取る:クライアントのルート証明書clientId.crtと銀行のルート証明書:bank.crt

  3. Javaキーストアを作成します(キーパスワードを入力し、キーストアパスワードを設定します)。

    openssl pkcs12 -export -in clientId.crt -inkey user.key -out keystore.p12 -name clientId -CAfile ca.crt -caname root

    出力に注意を払わないでください:unable to write 'random state'。Java PKCS12がkeystore.p12作成されました。

  4. キーストアに追加しますbank.crt(簡単にするために、1つのキーストアを使用しました)。

    keytool -import -alias banktestca -file banktestca.crt -keystore keystore.p12 -storepass javaops

    次の方法でキーストア証明書を確認します。

    keytool -list -keystore keystore.p12
  5. Javaコードの準備:) 依存関係RestTemplateを追加してSpring Boot を使用しましたorg.apache.httpcomponents.httpcore

    @Bean("sslRestTemplate")
    public RestTemplate sslRestTemplate() throws Exception {
      char[] storePassword = appProperties.getSslStorePassword().toCharArray();
      URL keyStore = new URL(appProperties.getSslStore());
    
      SSLContext sslContext = new SSLContextBuilder()
            .loadTrustMaterial(keyStore, storePassword)
      // use storePassword twice (with key password do not work)!!
            .loadKeyMaterial(keyStore, storePassword, storePassword) 
            .build();
    
      // Solve "Certificate doesn't match any of the subject alternative names"
      SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE);
    
      CloseableHttpClient client = HttpClients.custom().setSSLSocketFactory(socketFactory).build();
      HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(client);
      RestTemplate restTemplate = new RestTemplate(factory);
      // restTemplate.setMessageConverters(List.of(new Jaxb2RootElementHttpMessageConverter()));
      return restTemplate;
    }

あなたはそれをkeytoolですべて行うことができます。これにはOpenSSLはまったく必要ありません。
侯爵

0

証明書と秘密鍵(opensslなどによって生成されたもの)の両方を含むp12ファイルが与えられた場合、次のコードは特定のHttpsURLConnectionにそれを使用します。

    KeyStore keyStore = KeyStore.getInstance("pkcs12");
    keyStore.load(new FileInputStream(keyStorePath), keystorePassword.toCharArray());
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(keyStore, keystorePassword.toCharArray());
    SSLContext ctx = SSLContext.getInstance("TLS");
    ctx.init(kmf.getKeyManagers(), null, null);
    SSLSocketFactory sslSocketFactory = ctx.getSocketFactory();

    HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
    connection.setSSLSocketFactory(sslSocketFactory);

SSLContext初期化には時間がかかるため、キャッシュすることをお勧めします。


-1

ここでの修正はキーストアタイプだったと思います。pkcs12(pfx)には常に秘密キーがあり、JKSタイプは秘密キーなしで存在できます。コードで指定するか、ブラウザを介して証明書を選択しない限り、サーバーはそれが相手側のクライアントを表していることを知る方法がありません。


1
PKCS12形式は従来、privatekey-AND-certに使用されていましたが、Javaは2014年の8(この回答より1年以上前)から、privatekey(s)なしの証明書を含むPKCS12をサポートしています。キーストアの形式に関係なく、クライアント認証にはprivatekey-AND-certが必要です。2番目の文は理解できませんが、適切なエントリが1つ以上あるか、指定されたエントリを使用するようにキーマネージャ構成できる場合、Javaクライアントクライアントの証明書とキー自動的に選択できます。
dave_thompson_085

2番目の文は完全に間違っています。サーバーは信頼できる署名者を提供し、クライアントは死ぬか、その制約を満たす証明書を提供しません。「コード内」からではなく、自動的に。それサーバーの「クライアントを表すことを知る方法」です。
ローンヌ侯爵
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.