Problem:
I have been back and forth for the last week or so trying to configure my Scala+AKKA client to be able to send message to a server running NGINX.
I keep getting the error:javax.net.ssl.SSLHandshakeException: (certificate_unknown)
Setup:
nginx
config:
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; }}
client request:
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 }
Writing rootCA
to file:
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))
How I create the signed server certificate:
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)
What I've Tried:
After consulting chatGPT
, I tried the command openssl s_client -connect 192.168.0.4:443 -showcerts
The output of this command can be found here.
This helped me to verify that nginx
is in fact sending the entire chain, in the correct order (can be confirmed by date - the rootCA
(certificate 1 in the output) was generated on 7/2/2025, the server certificate (certificate 0) was generated today.
So what am I doing wrong/missing from my setup?