Problema:
Fiquei indo e voltando na última semana ou mais tentando configurar meu cliente Scala+AKKA para poder enviar mensagens para um servidor executando o NGINX.
Continuo recebendo o erro:
javax.net.ssl.SSLHandshakeException: (certificate_unknown)
Configurar:
nginx
configuração:
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;
}
}
solicitação do cliente:
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
}
Escrevendo rootCA
para arquivo:
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))
Como crio o certificado do servidor assinado:
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)
O que eu tentei:
Após consultar chatGPT
, tentei o comandoopenssl s_client -connect 192.168.0.4:443 -showcerts
A saída deste comando pode ser encontrada aqui .
Isso me ajudou a verificar se, nginx
de fato, estava enviando toda a cadeia, na ordem correta (pode ser confirmado pela data - o rootCA
(certificado 1 na saída) foi gerado em 02/07/2025, o certificado do servidor (certificado 0) foi gerado hoje.
Então o que estou fazendo errado/perdendo na minha configuração?
O nome da sua CA é
C=UK, ST=London, L=London, O=Hydra, OU=Hydra, CN=Hydra
mas o nome do emissor no seu certificado de servidor é diferente; em vez disso, éCN=Hydra SSL Certificate
. Como resultado, o validador de certificado não consegue vincular o certificado de servidor ao certificado de CA e a validação falha.O primeiro argumento para X509v3CertificateBuilder deve ser o nome do assunto do certificado pai (aqui raiz), convertido do tipo base-Java X500Principal para o tipo BouncyCastle X500Name da mesma forma que você já está convertendo SPKI; para um exemplo, veja (meu) https://security.stackexchange.com/questions/179526/how-to-import-plain-public-key-into-java-keystore
Além disso, no formato PEM, o corpo base64 deve ser dividido em linhas de 64 caracteres (exceto o último); veja RFC 7468 seção 2. O nginx usa OpenSSL, e versões mais antigas do OpenSSL falhariam completamente na única linha longa que você escreve. Desde 2012, o OpenSSL suporta linhas base64 mais longas, mas não ilimitadas, e, portanto, escrever esse formato pode falhar às vezes, dependendo dos seus dados. Outros softwares variam, então o mesmo arquivo pode parecer válido e inválido simultaneamente. A maneira mais fácil de evitar isso é usar
Base64.getMimeEncoder()
o que quebra em linhas de 76 caracteres por padrões MIME que são conceitualmente semelhantes ao PEM; até mesmo o OpenSSL mais antigo lidava com 76, e eu nunca encontrei nada que não fizesse isso.