6

クライアント証明書を使用して HTTPS 経由で Apache Web サーバーに接続し、サーバーへのファイルの HTTP PUT を実行する Java クライアント アプリケーションをコーディングしました。小さなファイルでは問題なく動作しますが、大きなファイルではクラッシュします。

Apache サーバーのログには次のように表示されます。

...
OpenSSL: Handshake: done
...
Changed client verification type will force renegotiation
...
filling buffer, max size 131072 bytes
...
request body exceeds maximum size (131072) for SSL buffer
could not buffer message body to allow SSL renegotiation to proceed
...    
OpenSSL: I/O error, 5 bytes expected to read on BIO
(104)Connection reset by peer: SSL input filter read failed.
(32)Broken pipe: core_output_filter: writing data to the network
Connection closed to child 20 with standard shutdown

クライアントの応答は次のとおりです。

java.io.IOException: Server returned HTTP response code: 401 for URL

私はこのプロセスに詳しくないので、ここで再交渉が必要なのか、それともそれを防ぐために何かできることがあるのか​​わからない. それとも、アプリケーション データを送信する前に、再ネゴシエーションが完了するまでクライアントを待機させることができますか? 以下は、クライアント コードの抜粋です (エラー処理は削除されています)。

        URL url = new URL("my url goes here");
        con = (HttpsURLConnection) url.openConnection();
        con.setSSLSocketFactory(getMyCustomClientCertSocketFactory());
        con.setRequestMethod("PUT");
        con.setDoOutput(true);
        con.connect();
        writer = new OutputStreamWriter(con.getOutputStream());
        writer.write(xml);
        writer.close();

        parseServerResponse(con.getInputStream());

SSLSocket のような下位レベルの API を使用し、HandshakeCompletedListener を活用する必要があるのではないかと考えています。

また、Apache SSLVerifyDepth ディレクティブが、再ネゴシエーションが発生している理由と関係があるかどうかも疑問に思っています。ディレクトリごとのコンテキスト (1 つのアップロード ディレクトリのみ) で値 2 のディレクティブを取得しました。Apache のマニュアルでは、次のように説明されています。

ディレクトリごとのコンテキストでは、HTTP 要求が読み取られた後、HTTP 応答が送信される前に、再構成されたクライアント検証の深さで SSL 再ネゴシエーションを強制します。

ここで要求されているのは、Java デバッグ出力です。

keyStore is : 
keyStore type is : jks
keyStore provider is : 
init keystore
init keymanager of type SunX509
trustStore is: C:\Program Files\Java\jdk1.6.0_35\jre\lib\security\cacerts
trustStore type is : jks
trustStore provider is : 
init truststore
adding as trusted cert:
 ...
trigger seeding of SecureRandom
done seeding SecureRandom
***
found key for : key-alias
chain [0] = [
[
...
]
***
trigger seeding of SecureRandom
done seeding SecureRandom
Allow unsafe renegotiation: false
Allow legacy hello messages: true
Is initial handshake: true
Is secure renegotiation: false
%% No cached client session
*** ClientHello, TLSv1
RandomCookie:  ...
Session ID:  {}
Cipher Suites: [SSL_RSA_WITH_RC4_128_MD5, SSL_RSA_WITH_RC4_128_SHA, TLS_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_RSA_WITH_AES_128_CBC_SHA, TLS_DHE_DSS_WITH_AES_128_CBC_SHA, SSL_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_RSA_WITH_3DES_EDE_CBC_SHA, SSL_DHE_DSS_WITH_3DES_EDE_CBC_SHA, SSL_RSA_WITH_DES_CBC_SHA, SSL_DHE_RSA_WITH_DES_CBC_SHA, SSL_DHE_DSS_WITH_DES_CBC_SHA, SSL_RSA_EXPORT_WITH_RC4_40_MD5, SSL_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA, SSL_DHE_DSS_EXPORT_WITH_DES40_CBC_SHA, TLS_EMPTY_RENEGOTIATION_INFO_SCSV]
Compression Methods:  { 0 }
***
main, WRITE: TLSv1 Handshake, length = 75
main, WRITE: SSLv2 client hello message, length = 101
main, READ: TLSv1 Handshake, length = 81
*** ServerHello, TLSv1
RandomCookie:  ...
Session ID:  ...
Cipher Suite: TLS_RSA_WITH_AES_128_CBC_SHA
Compression Method: 0
Extension renegotiation_info, renegotiated_connection: <empty>
***
%% Created:  [Session-1, TLS_RSA_WITH_AES_128_CBC_SHA]
** TLS_RSA_WITH_AES_128_CBC_SHA
main, READ: TLSv1 Handshake, length = 4392
*** Certificate chain
chain [0] = [
[
...
Certificate Extensions: 8
[1]: ObjectId: 1.3.6.1.5.5.7.1.1 Criticality=false
AuthorityInfoAccess [
  [
   accessMethod: ...
   accessLocation: URIName: ...
   accessMethod: ...
   accessLocation: URIName: ...
]

[2]: ObjectId: 2.5.29.35 Criticality=false
AuthorityKeyIdentifier [
KeyIdentifier [
...
]
]
[3]: ObjectId: 2.5.29.19 Criticality=false
BasicConstraints:[
  CA:false
  PathLen: undefined
]
[4]: ObjectId: 2.5.29.31 Criticality=false
CRLDistributionPoints [
  [DistributionPoint:
     [URIName: ...
]]
[5]: ObjectId: 2.5.29.32 Criticality=false
CertificatePolicies [
  [CertificatePolicyId: ...
[PolicyQualifierInfo: [
  qualifierID: ...
  qualifier: ...
]]  ]
]
[6]: ObjectId: 2.5.29.37 Criticality=false
ExtendedKeyUsages [
  serverAuth
  clientAuth
]
[7]: ObjectId: 2.5.29.15 Criticality=true
KeyUsage [
  DigitalSignature
  Key_Encipherment
]
[8]: ObjectId: 2.5.29.17 Criticality=false
SubjectAlternativeName [
  DNSName: ...
]
]
  Algorithm: [SHA1withRSA]
  Signature:
...
]
...
***
main, READ: TLSv1 Handshake, length = 4
*** ServerHelloDone
*** ClientKeyExchange, RSA PreMasterSecret, TLSv1
main, WRITE: TLSv1 Handshake, length = 518
SESSION KEYGEN:
PreMaster Secret:
...
CONNECTION KEYGEN:
Client Nonce:
...
Server Nonce:
...
Master Secret:
...
Client MAC write Secret:
...
Server MAC write Secret:
...
Client write key:
...
Server write key:
...
Client write IV:
...
Server write IV:
...
main, WRITE: TLSv1 Change Cipher Spec, length = 1
*** Finished
verify_data:  { 18, 162, 18, 251, 82, 111, 87, 133, 53, 240, 114, 155 }
***
main, WRITE: TLSv1 Handshake, length = 48
main, READ: TLSv1 Change Cipher Spec, length = 1
main, READ: TLSv1 Handshake, length = 48
*** Finished
verify_data:  { 46, 206, 8, 40, 63, 252, 99, 190, 251, 183, 110, 201 }
***
%% Cached client session: [Session-1, TLS_RSA_WITH_AES_128_CBC_SHA]
main, WRITE: TLSv1 Application Data, length = 256
main, WRITE: TLSv1 Application Data, length = 32
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 16416
...
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 16416
main, WRITE: TLSv1 Application Data, length = 512
main, READ: TLSv1 Application Data, length = 304 

ここで要求されているのは、getMyCustomClientCertSocketFactory ソースです (PEM ファイルから証明書とキーを取得します)。

public static SSLSocketFactory getMyCustomClientCertSocketFactory(String pemPath,
        boolean verifyPeer)
        throws NoSuchAlgorithmException, FileNotFoundException, IOException,
        KeyStoreException, CertificateException, UnrecoverableKeyException,
        KeyManagementException, InvalidKeySpecException {
    SSLContext context = SSLContext.getInstance("TLS");

    byte[] certAndKey = IOUtil.fileToBytes(new File(pemPath));
    byte[] certBytes = parseDERFromPEM(certAndKey,
            "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
    byte[] keyBytes = parseDERFromPEM(certAndKey,
            "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----");

    X509Certificate cert = generateX509CertificateFromDER(certBytes);
    RSAPrivateKey key = generateRSAPrivateKeyFromDER(keyBytes);

    KeyStore keystore = KeyStore.getInstance("JKS");
    keystore.load(null);
    keystore.setCertificateEntry("cert-alias", cert);
    keystore.setKeyEntry("key-alias", key, "changeit".toCharArray(),
            new Certificate[]{cert});

    KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
    kmf.init(keystore, "changeit".toCharArray());

    KeyManager[] km = kmf.getKeyManagers();

    TrustManager[] tm = null;

    if (!verifyPeer) {
        tm = new TrustManager[]{new TrustyTrustManager()};
    }

    context.init(km, tm, null);

    return context.getSocketFactory();
}
4

3 に答える 3

7

Sun Java に組み込まれている HttpsUrlConnection 機能は、クライアント証明書を使用した大規模な HTTP PUT シナリオをサーバーに適した方法で (つまり、サーバーの SSL 再ネゴシエーション バッファをオーバーフローさせずに) 処理できないようです。

「サーバーフレンドリーの意味」を確認するためにcurlが何をしていたかを調べたところ、「Expect」という名前のHTTP 1.1ヘッダーがあり、curlが値「100-continue」で送信することがわかりました(仕様http://www.w3を参照) .org/Protocols/rfc2616/rfc2616-sec14.html#sec14.20 )。このヘッダーは、基本的に「私は巨大なペイロードを持っていますが、それを送信する前に処理できるかどうか教えてください」と言っています。これにより、ペイロードが送信される前にエンドポイントがクライアント証明書を再ネゴシエートする時間が与えられます。

Sun HttpUrlConnection 実装では、このヘッダーは許可されていないようで、実際には制限付きヘッダー リストに含まれています。つまり、HttpUrlConnection.setRequestProperty メソッドで設定しても、ヘッダーは実際にはサーバーに送信されません。システム プロパティ sun.net.http.allowRestrictedHeaders を使用して制限付きヘッダーをオーバーライドできますが、Sun の実装ではプロトコルのこの部分の処理方法がわからないため、クライアントはソケット例外でクラッシュするだけです。

興味深いことに、Java の OpenJDK 実装はこのヘッダーをサポートしているようです。また、Apache HTTP クライアント ライブラリはこのヘッダー ( http://hc.apache.org/ ) をサポートしています。Apache HTTP クライアント ライブラリを使用してテスト プログラムを実装しました。クライアント証明書と Expect ヘッダーを使用して、大きなファイルの HTTP PUT 要求を正常に実行できます。

要約すると、解決策は次のとおりです。

  1. Apache SSLRenegBufferSize ディレクティブを巨大な数 (64MB など) に設定します。デフォルトは 128K です。このソリューションは、サービス拒否のリスクを引き起こす可能性があります
  2. 少数のディレクトリのみが必要とするホストではなく、常にクライアント証明書を必要とするホストを構成します。これにより、再交渉が回避されます。大部分のユーザーは匿名であるか、ユーザー名/パスワードで認証されているため、これは私のシナリオでは適切なオプションではありません。プログラムによるファイルのアップロード用のアップロード ディレクトリは 1 つだけです。この 1 つのディレクトリのためだけに、独自の SSL 証明書を持つ新しい仮想ホストを作成する必要があります。
  3. HTTP 1.1 Expect ヘッダーをサポートするクライアントを使用してください。残念ながら、Sun Java はそのままではこれをサポートしていません。Apache HTTP コンポーネント クライアント ライブラリなどのサード パーティを使用するか、Java ソケット API を使用して独自のソリューションを作成する必要があります。
  4. 最初に大きなペイロードを持たない HTTP 要求を発行することで、HTTP 1.1 の永続的な接続 (キープアライブによるパイプライン処理) を活用しますが、再ネゴシエーションが発生し、その後、HTTP PUT の接続を再利用します。理論的には、クライアントはアップロード ディレクトリで HTTP HEAD または OPTIONS を発行し、同じ接続を再利用して PUT を実行できるはずです。これが機能するためには、1 つの接続を「プライミング」してから PUT 用に別の接続を発行することを避けるために、おそらく永続接続プールに 1 つの接続のみを含める必要があります。ただし、このソリューションを機能させることができなかったため、HttpUrlConnection クラスがクライアント証明書または SSL を含む永続的な接続を維持/再利用するようには見えません。( HttpsUrlConnection および keep-alive ) を参照してください。
于 2013-03-13T18:56:39.450 に答える
0

java.io.IOException: サーバーが HTTP 応答コードを返しました: URL の 401

これはアプリケーション エラーです。SSL層が原因ではありません。なぜあなたが401 Unauthorizedより大きなファイルを取得しているのかわかりませんが、何を省略していgetMyCustomClientCertSocketFactory()
ます。また、別の方法を試しましたPOSTか? 同じ問題がありましたか?

于 2013-01-16T16:26:15.617 に答える
0

現在提供されているすべての追加情報に基づいて、XML を 1 つだけではなく複数のチャンクで記述する必要があると思います。現在、SSL によって 16k のチャンクにチャンクされる 1 つのチャンクを作成していますが、何らかの理由で Apache を窒息させています (そうすべきではありません)。4k 以下のチャンク サイズを試してみます。機能するまでチャンク サイズを調整します。

この問題を解決すると、クライアント証明書の問題が見つかる可能性があります。がっかりしないでください。これは、少なくともこの問題を解決したという証拠です。

于 2013-02-03T23:48:15.710 に答える