soap-errorsssl-tlssecuritytroubleshooting

SOAP の SSL 証明書エラー: TLS ハンドシェイク失敗の原因と解決方法

SOAPless Team12 min read

SOAP 連携における SSL/TLS エラーは、ネットワーク、セキュリティ、アプリケーションコードが交差する領域の問題であり、特にデバッグが困難です。エラーメッセージは暗号的で、診断ツールには専門知識が必要であり、修正方法はプログラミング言語や実行環境によって大きく異なります。

本記事では、SOAP Web サービス呼び出しにおける一般的な SSL/TLS の障害パターンをすべてカバーし、openssl による診断コマンドと Java、.NET、Python、PHP での具体的な修正方法を解説します。

TLS ハンドシェイクの仕組み

修正方法の前に、SOAP 呼び出し時の TLS ハンドシェイクの流れを理解しておきましょう。

Client                              Server
  |                                    |
  |--- ClientHello (TLS バージョン、----->
  |    暗号スイート)                    |
  |                                    |
  |<--- ServerHello (選択された暗号、 --|
  |     サーバー証明書)                 |
  |                                    |
  |--- クライアントが検証:              |
  |    1. 証明書が期限切れでないか       |
  |    2. ホスト名が CN/SAN と一致するか |
  |    3. 発行者 CA が信頼されているか   |
  |    4. 証明書チェーン全体が有効か     |
  |                                    |
  |--- 鍵交換、Finished ------------->|
  |<--- Finished --------------------|
  |                                    |
  |=== 暗号化された SOAP リクエスト ===>|
  |<== 暗号化された SOAP レスポンス ===|

ステップ 1-4 のいずれかで失敗すると、TLS ハンドシェイクエラーが発生し、SOAP クライアントは接続せずに SSL エラーをスローします。

エラー 1: 自己署名証明書が信頼されていない

SOAP 連携で最も多い SSL エラーです。社内のエンタープライズサービス、開発環境、独自の認証局を運用する行政・医療系システムで頻繁に発生します。

典型的なエラーメッセージ:

# Java
javax.net.ssl.SSLHandshakeException: PKIX path building failed:
unable to find valid certification path to requested target

# .NET
System.Net.WebException: Could not establish trust relationship
for the SSL/TLS secure channel.

# Python
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]
certificate verify failed: self-signed certificate

# PHP
SoapFault: Could not connect to host
(SSL エラーが汎用的な接続エラーに隠れます)

診断方法:

# 証明書が自己署名かどうか確認
openssl s_client -connect soap-service.example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer -subject

# issuer と subject が同一なら自己署名
# 例:
# issuer=CN = soap-service.example.com
# subject=CN = soap-service.example.com

修正 -- Java (keytool):

# ステップ 1: サーバー証明書をダウンロード
openssl s_client -connect soap-service.example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -out server.crt

# ステップ 2: truststore にインポート
keytool -import -trustcacerts \
  -file server.crt \
  -alias soap-server \
  -keystore /app/truststore.jks \
  -storepass changeit \
  -noprompt
// 方法 A: システムプロパティで設定(全接続に影響)
System.setProperty("javax.net.ssl.trustStore", "/app/truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");

// 方法 B: 接続ごとに SSLContext を設定(推奨)
KeyStore trustStore = KeyStore.getInstance("JKS");
try (InputStream is = new FileInputStream("/app/truststore.jks")) {
    trustStore.load(is, "changeit".toCharArray());
}

TrustManagerFactory tmf = TrustManagerFactory.getInstance(
    TrustManagerFactory.getDefaultAlgorithm()
);
tmf.init(trustStore);

SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);

BindingProvider provider = (BindingProvider) port;
provider.getRequestContext().put(
    "com.sun.xml.ws.transport.https.client.SSLSocketFactory",
    sslContext.getSocketFactory()
);

修正 -- Python:

from zeep import Client
from zeep.transports import Transport
from requests import Session

session = Session()

# 自己署名証明書のパスを指定
session.verify = '/path/to/server.crt'

# または CA バンドルに追加
# cat /etc/ssl/certs/ca-certificates.crt server.crt > custom-bundle.crt
session.verify = '/path/to/custom-bundle.crt'

transport = Transport(session=session)
client = Client(wsdl_url, transport=transport)

修正 -- PHP:

$context = stream_context_create([
    'ssl' => [
        'verify_peer' => true,
        'verify_peer_name' => true,
        'cafile' => '/path/to/server.crt',
    ],
]);

$client = new SoapClient($wsdl, [
    'stream_context' => $context,
]);

エラー 2: 証明書の期限切れ

SOAP サービスで期限切れ証明書は驚くほど多く見られます。何年も前にデプロイされたエンタープライズ SOAP エンドポイントの証明書が、誰にも気づかれずに失効しているケースは珍しくありません。

診断方法:

# 証明書の有効期間を確認
openssl s_client -connect soap-service.example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -noout -dates

# 出力例:
# notBefore=Jan  1 00:00:00 2023 GMT
# notAfter=Jan  1 00:00:00 2024 GMT   <-- 期限切れ

# 現在有効かどうかを簡易チェック
openssl s_client -connect soap-service.example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -noout -checkend 0 && echo "有効" || echo "期限切れ"

対処: 本来はサービス運営者が証明書を更新すべきです。それを待てない場合(リスクを受容した上で)、一時的に検証をバイパスできます。

# Python: 検証バイパス(開発・テスト環境のみ)
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

session = Session()
session.verify = False  # 証明書検証を完全に無効化

エラー 3: 中間 CA 証明書の欠落

最もデバッグが困難な SSL 問題の一つです。サーバーが自身の証明書は提示するものの、ルート CA への信頼チェーンを構築するために必要な中間 CA 証明書を含めていない場合に発生します。ブラウザは不足する中間証明書を自動的に取得しますが、SOAP クライアントにはその機能がありません。

診断方法:

# 証明書チェーン全体を表示
openssl s_client -connect soap-service.example.com:443 -showcerts </dev/null 2>&1 \
  | grep -E "^(s:|i:)"

# depth 0 しかない場合、中間証明書が欠落

# チェーンを明示的に検証
openssl s_client -connect soap-service.example.com:443 </dev/null 2>&1 \
  | grep "Verify return code"

# "Verify return code: 21 (unable to verify the first certificate)" = 中間証明書欠落

修正: 中間証明書をダウンロードしてバンドルを作成します。

# ステップ 1: サーバー証明書の発行者を特定
openssl s_client -connect soap-service.example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -noout -issuer

# ステップ 2: CA のウェブサイトから中間証明書をダウンロード

# ステップ 3: カスタム CA バンドルを作成
cat /etc/ssl/certs/ca-certificates.crt intermediate.crt > custom-ca-bundle.crt

各言語での設定:

// Java: 中間証明書を truststore にインポート
// keytool -import -trustcacerts -file intermediate.crt -alias intermediate-ca -keystore truststore.jks
# Python
session.verify = '/path/to/custom-ca-bundle.crt'
// PHP
$context = stream_context_create([
    'ssl' => [
        'cafile' => '/path/to/custom-ca-bundle.crt',
    ],
]);

エラー 4: TLS バージョンの不一致

古い SOAP サービスは TLS 1.0 や 1.1 のみサポートしていることがあり、最新のクライアントはセキュリティ上の理由でこれらをデフォルトで無効化しています。逆に、セキュリティを強化したサービスが TLS 1.2 や 1.3 を要求しているのに、古いクライアントがそれに対応していない場合もあります。

診断方法:

# 各 TLS バージョンをテスト
openssl s_client -connect soap-service.example.com:443 -tls1_2 2>&1 | head -5
openssl s_client -connect soap-service.example.com:443 -tls1_3 2>&1 | head -5

# 実際にネゴシエーションされたプロトコルを確認
openssl s_client -connect soap-service.example.com:443 </dev/null 2>/dev/null \
  | grep "Protocol  :"

修正 -- Java:

// TLS 1.2 を強制
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(null, null, null);

BindingProvider provider = (BindingProvider) port;
provider.getRequestContext().put(
    "com.sun.xml.ws.transport.https.client.SSLSocketFactory",
    sslContext.getSocketFactory()
);

// JVM 全体で設定する場合
// -Dhttps.protocols=TLSv1.2,TLSv1.3

修正 -- .NET:

// TLS 1.2 を強制(クライアント作成前に設定)
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls13;

修正 -- PHP:

$context = stream_context_create([
    'ssl' => [
        'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT |
                           STREAM_CRYPTO_METHOD_TLSv1_3_CLIENT,
    ],
]);

$client = new SoapClient($wsdl, [
    'stream_context' => $context,
]);

エラー 5: SNI (Server Name Indication) の問題

複数の HTTPS サービスが同一 IP アドレスを共有している場合(バーチャルホスティングやクラウドロードバランサーで一般的)、サーバーは SNI を使用してどの証明書を提示するか判断します。SOAP クライアントが SNI 拡張を送信しない場合、サーバーは誤った証明書を提示するか、接続を拒否します。

診断方法:

# SNI ありで確認
openssl s_client -connect soap-service.example.com:443 \
  -servername soap-service.example.com </dev/null 2>/dev/null \
  | openssl x509 -noout -subject

# SNI なしで確認
openssl s_client -connect soap-service.example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -noout -subject

# subject が異なる場合、SNI が必須

修正 -- Java:

// Java 8 以降はデフォルトで SNI を送信しますが、一部設定で無効化されている場合あり
System.setProperty("jsse.enableSNIExtension", "true");

修正 -- PHP:

$context = stream_context_create([
    'ssl' => [
        'SNI_enabled' => true,
        'peer_name' => 'soap-service.example.com',
    ],
]);

$client = new SoapClient($wsdl, [
    'stream_context' => $context,
]);

openssl s_client による総合診断

SSL/TLS 問題のデバッグに最も有効なツールが openssl s_client です。以下の診断スクリプトを活用してください。

#!/bin/bash
HOST="soap-service.example.com"
PORT=443

echo "=== 証明書詳細 ==="
openssl s_client -connect $HOST:$PORT -servername $HOST </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

echo ""
echo "=== 証明書チェーン ==="
openssl s_client -connect $HOST:$PORT -servername $HOST -showcerts </dev/null 2>&1 \
  | grep -E "^(s:|i:)"

echo ""
echo "=== プロトコルと暗号 ==="
openssl s_client -connect $HOST:$PORT -servername $HOST </dev/null 2>/dev/null \
  | grep -E "Protocol|Cipher"

echo ""
echo "=== 検証結果 ==="
openssl s_client -connect $HOST:$PORT -servername $HOST </dev/null 2>&1 \
  | grep "Verify return code"

echo ""
echo "=== TLS バージョンサポート ==="
for ver in tls1 tls1_1 tls1_2 tls1_3; do
  result=$(openssl s_client -connect $HOST:$PORT -$ver </dev/null 2>&1)
  if echo "$result" | grep -q "CONNECTED"; then
    echo "$ver: サポート"
  else
    echo "$ver: 未サポート"
  fi
done

検証リターンコード早見表

openssl s_client の検証エラーコードの意味は以下の通りです。

コード 0:   OK -- 証明書は有効
コード 2:   発行者証明書が取得できない(中間証明書の欠落)
コード 10:  証明書が期限切れ
コード 18:  自己署名証明書
コード 19:  証明書チェーン内に自己署名証明書
コード 20:  ローカル発行者証明書が取得できない(CA の欠落)
コード 21:  最初の証明書を検証できない(中間証明書の欠落)
コード 62:  ホスト名の不一致(CN/SAN が一致しない)

SOAPless による解決

SOAP クライアントの SSL/TLS 設定は、エンタープライズ連携で最もエラーが発生しやすい領域です。言語ごとに証明書の扱いが異なり、truststore のフォーマットも Java (JKS/PKCS12)、.NET (Windows 証明書ストア)、Python (PEM バンドル)、PHP (stream_context) とバラバラです。デプロイ環境ごとに個別の証明書設定が必要で、証明書の期限切れやローテーションが本番障害を引き起こします。

SOAPless は、SOAP サービスとのすべての TLS 通信を SOAPless のインフラストラクチャで処理します。WSDL URL を SOAPless ダッシュボードに登録すれば、証明書の検証、TLS バージョンのネゴシエーション、中間 CA の処理はすべて SOAPless が担当します。アプリケーションは標準的な HTTPS で SOAPless に接続するだけです。

# SSL 設定は不要 -- 標準的な HTTPS コールのみ
curl -X POST https://api.soapless.com/v1/your-service/GetOrder \
  -H "Content-Type: application/json" \
  -H "X-API-Key: your-api-key" \
  -d '{"orderId": "ORD-2025-001"}'

SOAP サービスの証明書が更新されたり期限切れになった場合も、SOAPless が透過的に対応するため、アプリケーションのコードやデプロイ設定を変更する必要はありません。