问题:
在过去一周左右的时间里,我一直在反复尝试配置我的 Scala+AKKA 客户端,以便能够向运行 NGINX 的服务器发送消息。
我不断收到错误:
javax.net.ssl.SSLHandshakeException: (certificate_unknown)
设置:
nginx
配置:
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name localhost;
ssl_certificate /home/hydra/.localhost-ssl/localhost.crt; //<- combined certificates (server_cert + rootCA)
ssl_certificate_key /home/hydra/.localhost-ssl/localhost-key.key;
ssl_trusted_certificate /home/hydra/.localhost-ssl/rootCA.crt; //<- just the rootCA
index index.html index.htm;
root /home/hydra/ui/;
location / {
try_files $uri.html $uri/index.html
@public
@nextjs;
add_header Cache-Control "public, max-age=3600";
}
location @public {
add_header Cache-Control "public, max-age=3600";
}
location /ping {
proxy_pass http://localhost:8080;
}
location @nextjs {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Host $remote_addr;
}
}
客户要求:
private val sslContext: SSLContext = SSLManager.getClientSSLContext
private val connectionContext = ConnectionContext.httpsClient(sslContext)
...
val request = HttpRequest(method = HttpMethods.GET, uri = s"https://${server.serverIP}/ping")
http.singleRequest(request, connectionContext).pipeTo(self)
createClientContext
:
def getClientSSLContext: SSLContext = {
val keyStore = KeyStore.getInstance("JKS")
keyStore.load(null, null) // Create an empty keystore
keyStore.setCertificateEntry("rootCA", loadRootCertificate())
// Set up a TrustManager that trusts the root CA certificate
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm)
trustManagerFactory.init(keyStore)
val trustManagers = trustManagerFactory.getTrustManagers
// Create an SSLContext with the custom TrustManager
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, trustManagers, new SecureRandom())
sslContext
}
写入rootCA
文件:
val rootCA = new StringBuilder()
rootCA.append("-----BEGIN CERTIFICATE-----\n")
rootCA.append(Base64.getEncoder.encodeToString(rootCACertificate.getEncoded))
rootCA.append("\n-----END CERTIFICATE-----")
writeFile(ROOT_CA_PATH, Seq(rootCA.toString))
如何创建签名的服务器证书:
def createCSR(keyPair: KeyPair, subject: String, keyAlgorithm: String): PKCS10CertificationRequest = {
val csrGen = new PKCS10CertificationRequestBuilder(new X500Name(subject), SubjectPublicKeyInfo.getInstance(keyPair.getPublic.getEncoded))
val signer = new JcaContentSignerBuilder("SHA256with" + keyAlgorithm).build(keyPair.getPrivate)
csrGen.build(signer)
}
// Sign the CSR with the root CA's private key to generate a certificate
def signCertificate(csr: PKCS10CertificationRequest, rootCACertificate: X509Certificate, rootCAPrivateKey: PrivateKey): X509Certificate = {
val notBefore = new Date()
val notAfter = new Date(notBefore.getTime + 36500L * 24 * 60 * 60 * 1000) // Valid for 1 year
val certGen = new X509v3CertificateBuilder(
new X500Name("CN=Hydra SSL Certificate"),
new BigInteger(128, new Random()),
notBefore,
notAfter,
csr.getSubject,
csr.getSubjectPublicKeyInfo
)
// Add SubjectAlternativeName (SAN) extension
val sanNames = Array[GeneralName](
new GeneralName(GeneralName.iPAddress, SERVER_IP)
)
val generalNames = new GeneralNames(sanNames)
certGen.addExtension(Extension.subjectAlternativeName, false, generalNames)
// Sign with root CA's private key
val signer = new JcaContentSignerBuilder("SHA256withRSA").build(rootCAPrivateKey)
val certificateHolder: X509CertificateHolder = certGen.build(signer)
// Convert to a JCE certificate
val converter = new JcaX509CertificateConverter().setProvider("BC")
converter.getCertificate(certificateHolder)
}
...
// Step 1: Load Root CA certificate and private key
val rootCACertificate = loadRootCertificate()
val rootCAPrivateKey = loadRootPrivateKey()
// Step 2: Generate new key pair for SSL certificate
val keyPair = generateKey("RSA")
// Step 3: Create CSR (Certificate Signing Request)
val csr = createCSR(keyPair, s"CN=hydra_server_$SERVER_ID, O=Hydra, C=UK", "RSA")
// Step 4: Sign the CSR with the Root CA to generate the SSL certificate
val sslCertificate = signCertificate(csr, rootCACertificate, rootCAPrivateKey)
我尝试过的:
咨询后chatGPT
,我尝试了该命令openssl s_client -connect 192.168.0.4:443 -showcerts
该命令的输出可以在这里找到。
这帮助我验证是否nginx
确实按正确的顺序发送了整个链(可以通过日期确认 - rootCA
(输出中的证书 1)是在 2025 年 7 月 2 日生成的,服务器证书(证书 0)是在今天生成的。
那么我的设置中哪里做错了/遗漏了什么?
您的 CA 名称是
C=UK, ST=London, L=London, O=Hydra, OU=Hydra, CN=Hydra
,但服务器证书中的颁发者名称不同;而是CN=Hydra SSL Certificate
。因此,证书验证器无法将服务器证书链接到 CA 证书,验证失败。X509v3CertificateBuilder 的第一个参数应该是来自父级(此处为根)证书的主题名称,从 base-Java X500Principal 类型转换为 BouncyCastle X500Name 类型,方式与您已经转换 SPKI 的方式相同;有关示例,请参阅(我的)https://security.stackexchange.com/questions/179526/how-to-import-plain-public-key-into-java-keystore
此外,在 PEM 格式中,base64 主体应该分成 64 个字符的行(最后一个字符除外);请参阅 RFC 7468 第 2 节。nginx 使用 OpenSSL,旧版本的 OpenSSL 会因您编写的单行长行而完全失败。自 2012 年以来,OpenSSL 支持更长的 base64 行,但不是无限制的,因此根据您的数据,写入此格式有时会失败。其他软件各不相同,因此同一个文件可能同时有效和无效。避免这种情况的最简单方法是使用
Base64.getMimeEncoder()
根据 MIME 标准分成 76 个字符的行,这些标准在概念上与 PEM 相似;即使是最古老的 OpenSSL 也能处理 76 个字符,而且我从未遇到过其他不这样做的东西。