Java (OpenJDK 10) から SSL (HTTPS) で接続するときに、 エラーが出ました。 使っていたのは OpenJDK 10.0.2 でした。
(Java 8 から Java 10 に上げたので、 もしかしたら Java 9 でも起きていたのかも。)
Java 12 にしたらこのエラーは出なくなりました。
エラー内容
これはメール送信をしようとした時のエラーログです。
2018-09-07 11:30:54.743 ERROR 16595 --- [pool-1-thread-1] o.s.s.s.TaskUtils$LoggingErrorHandler : Unexpected error occurred in scheduled task.
org.springframework.mail.MailAuthenticationException: Authentication failed; nested exception is javax.mail.AuthenticationFailedException: 220 Ready to start TLS
;
nested exception is:
javax.mail.MessagingException: Could not convert socket to TLS;
nested exception is:
javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at org.springframework.mail.javamail.JavaMailSenderImpl.doSend(JavaMailSenderImpl.java:438) ~[spring-context-support-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:359) ~[spring-context-support-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.mail.javamail.JavaMailSenderImpl.send(JavaMailSenderImpl.java:354) ~[spring-context-support-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at XXXXX
at XXXXX
at jdk.internal.reflect.GeneratedMethodAccessor130.invoke(Unknown Source) ~[na:na]
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
at java.base/java.lang.reflect.Method.invoke(Method.java:564) ~[na:na]
at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:65) ~[spring-context-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) ~[spring-context-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:93) [spring-context-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:514) [na:na]
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) [na:na]
at java.base/java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:304) [na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1135) [na:na]
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635) [na:na]
at java.base/java.lang.Thread.run(Thread.java:844) [na:na]
Caused by: javax.mail.AuthenticationFailedException: 220 Ready to start TLS
at com.sun.mail.smtp.SMTPTransport$Authenticator.authenticate(SMTPTransport.java:960) ~[javax.mail-1.6.1.jar:1.6.1]
at com.sun.mail.smtp.SMTPTransport.authenticate(SMTPTransport.java:876) ~[javax.mail-1.6.1.jar:1.6.1]
at com.sun.mail.smtp.SMTPTransport.protocolConnect(SMTPTransport.java:780) ~[javax.mail-1.6.1.jar:1.6.1]
at javax.mail.Service.connect(Service.java:366) ~[javax.mail-1.6.1.jar:1.6.1]
at org.springframework.mail.javamail.JavaMailSenderImpl.connectTransport(JavaMailSenderImpl.java:515) ~[spring-context-support-5.0.7.RELEASE.jar:5.0.7.RELEASE]
at org.springframework.mail.javamail.JavaMailSenderImpl.doSend(JavaMailSenderImpl.java:435) ~[spring-context-support-5.0.7.RELEASE.jar:5.0.7.RELEASE]
... 16 common frames omitted
Caused by: javax.mail.MessagingException: Could not convert socket to TLS
at com.sun.mail.smtp.SMTPTransport.startTLS(SMTPTransport.java:2155) ~[javax.mail-1.6.1.jar:1.6.1]
at com.sun.mail.smtp.SMTPTransport$Authenticator.authenticate(SMTPTransport.java:935) ~[javax.mail-1.6.1.jar:1.6.1]
... 21 common frames omitted
Caused by: javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at java.base/sun.security.ssl.Alerts.getSSLException(Alerts.java:198) ~[na:na]
at java.base/sun.security.ssl.SSLSocketImpl.fatal(SSLSocketImpl.java:1974) ~[na:na]
at java.base/sun.security.ssl.Handshaker.fatalSE(Handshaker.java:345) ~[na:na]
at java.base/sun.security.ssl.Handshaker.fatalSE(Handshaker.java:339) ~[na:na]
at java.base/sun.security.ssl.ClientHandshaker.checkServerCerts(ClientHandshaker.java:1968) ~[na:na]
at java.base/sun.security.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1777) ~[na:na]
at java.base/sun.security.ssl.ClientHandshaker.processMessage(ClientHandshaker.java:264) ~[na:na]
at java.base/sun.security.ssl.Handshaker.processLoop(Handshaker.java:1098) ~[na:na]
at java.base/sun.security.ssl.Handshaker.processRecord(Handshaker.java:1026) ~[na:na]
at java.base/sun.security.ssl.SSLSocketImpl.processInputRecord(SSLSocketImpl.java:1137) ~[na:na]
at java.base/sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1074) ~[na:na]
at java.base/sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:973) ~[na:na]
at java.base/sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1402) ~[na:na]
at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1429) ~[na:na]
at java.base/sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1413) ~[na:na]
at com.sun.mail.util.SocketFetcher.configureSSLSocket(SocketFetcher.java:620) ~[javax.mail-1.6.1.jar:1.6.1]
at com.sun.mail.util.SocketFetcher.startTLS(SocketFetcher.java:547) ~[javax.mail-1.6.1.jar:1.6.1]
at com.sun.mail.smtp.SMTPTransport.startTLS(SMTPTransport.java:2150) ~[javax.mail-1.6.1.jar:1.6.1]
... 22 common frames omitted
Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:385) ~[na:na]
at java.base/sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:290) ~[na:na]
at java.base/sun.security.validator.Validator.validate(Validator.java:264) ~[na:na]
at java.base/sun.security.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:343) ~[na:na]
at java.base/sun.security.ssl.X509TrustManagerImpl.checkTrusted(X509TrustManagerImpl.java:226) ~[na:na]
at java.base/sun.security.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:133) ~[na:na]
at java.base/sun.security.ssl.ClientHandshaker.checkServerCerts(ClientHandshaker.java:1947) ~[na:na]
... 35 common frames omitted
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
at java.base/sun.security.provider.certpath.SunCertPathBuilder.build(SunCertPathBuilder.java:141) ~[na:na]
at java.base/sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:126) ~[na:na]
at java.base/java.security.cert.CertPathBuilder.build(CertPathBuilder.java:297) ~[na:na]
at java.base/sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:380) ~[na:na]
... 41 common frames omitted
対処法1 - 証明書をインポート
EC2を使っていましたので、ログインして次のコマンドを実行しました。
cacerts のパスワードはデフォルトでは changeit です。
コマンドの中でファイルパスを指定しているところがあります。これはお使いの環境に依存しますので適宜調べてください。
mkdir certification
cd certification
openssl s_client -connect email-smtp.us-east-1.amazonaws.com:443 < /dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > email-smtp.us-east-1.amazonaws.com_public.crt
sudo /usr/lib/jvm/jdk-10.0.2/bin/keytool -import -alias email-smtp.us-east-1.amazonaws.com -keystore /usr/lib/jvm/jdk-10.0.2/lib/security/cacerts -file email-smtp.us-east-1.amazonaws.com_public.crt
他にもいくつか外部通信しているところがありましたので、いくつか証明書をインポートしました。
もし、 keytool error: java.lang.Exception: Certificate not imported, alias already exists
というようなメッセージが出たら
別のエイリアスでインポートするか、
次のコマンドでキーを削除し手からインポートを行います。
keytool -delete -alias your_alias -keystore key_store_path
keytool
コマンドは、 -storepass changeit
のようにパスワードを記述できます。
keytool
は jdk の bin ディレクトリにあります。
(ルート証明書をインポートすると楽なのか? よくわかっていません。)
ディレクトリパスの調べ方
例えば私の使っている macOS の環境では、次のコマンドをルートで実行してインポートしました。
/Library/Java/JavaVirtualMachines/adoptopenjdk-10.jdk/Contents/Home/bin/keytool -import -alias email-smtp.us-east-1.amazonaws.com -keystore /Library/Java/JavaVirtualMachines/adoptopenjdk-10.jdk/Contents/Home/lib/security/cacerts -file email-smtp.us-east-1.amazonaws.com_public.crt
最初の keytool
のパスは $JAVA_HOME/bin/keytool
, 次の cacerts
のパスは $JAVA_HOME/lib/security/cacerts
です。
/usr/libexec/java_home
を実行すると JAVA_HOME のパスが得られます(これが全マシンでできるのかは知りません、手持ちの macOS ではできました)。 これを利用すると、インポートのコマンドは次のように書けます。
`/usr/libexec/java_home`/bin/keytool -import -alias email-smtp.us-east-1.amazonaws.com -keystore `/usr/libexec/java_home`/lib/security/cacerts -file email-smtp.us-east-1.amazonaws.com_public.crt
参考
- Connectiong to SSL Services
- Unable to connect to SSL services due to PKIX path building failed
- How to obtain the location of cacerts of the default java installation?
関連
対処法2 - ドメインの認証を無視する
証明書をインポートする方法だと、証明書の期限が切れた場合にまた変える必要がありますので、
下のように書いて、ドメインの認証を無視するようにします。
この方法は自分で通信用のクラスを書く場合はできるのですが、
ライブラリを用いて通信する場合にできるのかはよくわかっていません。
下のコードの object
は WebGateway.post(...)
のようにして使うことを想定して作っていました。
post
メソッドよりも上の部分が、証明書を無視するのに必要なコードです。
package com.example.app
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
import java.security.cert.X509Certificate
import javax.net.ssl.*
import java.security.cert.CertificateException
object WebGateway {
private val sslContext = SSLContext.getInstance("SSL")
private val trustManager = arrayOf<TrustManager>(object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate>? {
return null
}
@Throws(CertificateException::class)
override fun checkClientTrusted(
chain: Array<X509Certificate>,
authType: String
) {
}
@Throws(CertificateException::class)
override fun checkServerTrusted(
chain: Array<X509Certificate>,
authType: String
) {
}
})
init {
sslContext.init(null, trustManager, null)
HttpsURLConnection.setDefaultHostnameVerifier { hostname, session -> true }
}
private fun buildConnection(
url: URL,
payload: String,
method: String,
sendJson: Boolean = false,
requestJson: Boolean = false
): HttpURLConnection {
val connection = url.openConnection() as HttpURLConnection
(connection as? HttpsURLConnection)?.let {
it.sslSocketFactory = sslContext.socketFactory
}
connection.requestMethod = method
connection.doOutput = true
/* Code */
return connection
}
fun post(
url: URL,
payload: String,
isJson: Boolean = false,
requestJson: Boolean = false
): String {
val connection =
buildConnection(url, payload, "POST", isJson, requestJson)
connection.connect()
val os = connection.outputStream
os.write(payload.toByteArray())
os.close()
if (connection.responseCode != HttpsURLConnection.HTTP_OK) {
throw Exception(
connection.responseCode.toString() + ":" +
connection.responseMessage
)
}
val reader = BufferedReader(
InputStreamReader(connection.inputStream, "UTF-8")
)
val sb = StringBuilder()
do {
val line = reader.readLine() ?: break
sb.appendln(line)
} while (true)
connection.disconnect()
return sb.toString()
}
}