From 82f7313528a40d47f2c217456e3a9d9da00f89d3 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Thu, 10 Apr 2025 09:43:45 -0700 Subject: [PATCH 1/8] wip: feature(lsp): respect IDE user proxy settings / forward trust store --- .../services/amazonq/lsp/TrustChainUtil.kt | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt new file mode 100644 index 00000000000..a6a99abd6cd --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt @@ -0,0 +1,119 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp + +import com.intellij.util.io.DigestUtil +import com.intellij.util.net.JdkProxyProvider +import com.intellij.util.net.ssl.CertificateManager +import org.apache.http.client.methods.RequestBuilder +import org.apache.http.conn.ssl.DefaultHostnameVerifier +import org.apache.http.impl.client.HttpClientBuilder +import org.apache.http.impl.client.SystemDefaultCredentialsProvider +import org.apache.http.impl.conn.SystemDefaultRoutePlanner +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.warn +import java.net.URI +import java.security.KeyStore +import java.security.cert.CertPathBuilder +import java.security.cert.CertStore +import java.security.cert.Certificate +import java.security.cert.CollectionCertStoreParameters +import java.security.cert.PKIXBuilderParameters +import java.security.cert.PKIXCertPathBuilderResult +import java.security.cert.X509CertSelector +import java.security.cert.X509Certificate +import kotlin.collections.ifEmpty + +object TrustChainUtil { + private val LOG = getLogger() + + /** + * Build and validate the complete certificate chain + * @param certs The end-entity certificate + * @param trustAnchors The truststore containing trusted CA certificates + * @return The complete certificate chain + */ + fun resolveTrustChain(certs: Collection, trustAnchors: KeyStore): List { + // Create the selector for the certificate + val selector = X509CertSelector() + selector.certificate = certs.first() + + // Create the parameters for path validation + val pkixParams = PKIXBuilderParameters(trustAnchors, selector) + + // Disable CRL checking since we just want to build the path + pkixParams.isRevocationEnabled = false + + // Create a CertStore containing the certificate we want to validate + val ccsp = CollectionCertStoreParameters(certs) + val certStore = CertStore.getInstance("Collection", ccsp) + pkixParams.addCertStore(certStore) + + // Get the certification path + val builder = CertPathBuilder.getInstance("PKIX") + val result = builder.build(pkixParams) as PKIXCertPathBuilderResult + val certPath = result.certPath + val chain = (certPath.certificates as List).toMutableList() + + // Add the trust anchor (root CA) to complete the chain + val trustAnchorCert = result.trustAnchor.trustedCert + if (trustAnchorCert != null) { + chain.add(trustAnchorCert) + } + + return chain + } + + fun getTrustChain(uri: URI): List { + val proxyProvider = JdkProxyProvider.getInstance() + var peerCerts: Array = emptyArray() + val verifierDelegate = DefaultHostnameVerifier() + val client = HttpClientBuilder.create() + .setRoutePlanner(SystemDefaultRoutePlanner(proxyProvider.proxySelector)) + .setDefaultCredentialsProvider(SystemDefaultCredentialsProvider()) + .setSSLHostnameVerifier { hostname, sslSession -> + peerCerts = sslSession.peerCertificates + + verifierDelegate.verify(hostname, sslSession) + } + // prompt user via modal to accept certificate if needed; otherwise need to prompt separately prior to launching flare + .setSSLContext(CertificateManager.getInstance().sslContext) + + // client request will fail if user did not accept cert + client.build().execute(RequestBuilder.options(uri).build()) + + val certificates = peerCerts as Array + + // java default + custom system + // excluding leaf cert for case where user has both leaf and issuing CA as trusted roots + val allAccepted = CertificateManager.getInstance().trustManager.acceptedIssuers.toSet() - certificates.first() + val ks = keystoreFromCertificates(allAccepted) + + // if this throws then there is a bug because it passed PKIX validation in apache client + val trustChain = try { + resolveTrustChain(certificates.toList(), ks) + } catch (e: Exception) { + LOG.warn(e) { "Passed Apache PKIX verification but could not build trust anchor via CertPathBuilder" } + emptyList() + } + + // if trust chain is empty, then somehow user only trusts the leaf cert??? + return trustChain.ifEmpty { + // so return the served certificate chain from the server and hope that works + certificates.toList() + } + } + + private fun keystoreFromCertificates(certificates: Collection): KeyStore { + val ks = KeyStore.getInstance(KeyStore.getDefaultType()) + ks.load(null, null) + certificates.forEachIndexed { index, cert -> + ks.setCertificateEntry( + cert.getSubjectX500Principal().toString() + "-" + DigestUtil.sha256Hex(cert.encoded), + cert + ) + } + return ks + } +} From b8dcb4d21e786fd414ae5bed23f22799e398cc13 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Fri, 11 Apr 2025 14:19:37 -0700 Subject: [PATCH 2/8] hook up --- .../services/amazonq/lsp/AmazonQLspService.kt | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index 59658c3a878..01a7a1f8500 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -20,6 +20,9 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Key import com.intellij.openapi.util.SystemInfo import com.intellij.util.io.await +import com.intellij.util.net.HttpConfigurable +import com.intellij.util.net.JdkProxyProvider +import io.ktor.util.network.hostname import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.TimeoutCancellationException @@ -29,6 +32,7 @@ import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withTimeout +import org.apache.http.client.utils.URIBuilder import org.eclipse.lsp4j.ClientCapabilities import org.eclipse.lsp4j.ClientInfo import org.eclipse.lsp4j.DidChangeConfigurationParams @@ -45,6 +49,7 @@ import org.slf4j.event.Level import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.info import software.aws.toolkits.core.utils.warn +import software.aws.toolkits.core.utils.writeText import software.aws.toolkits.jetbrains.isDeveloperMode import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager import software.aws.toolkits.jetbrains.services.amazonq.lsp.auth.DefaultAuthCredentialsService @@ -54,6 +59,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.createExtended import software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument.TextDocumentServiceHandler import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler +import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata import software.aws.toolkits.jetbrains.settings.LspSettings import java.io.IOException @@ -62,7 +68,11 @@ import java.io.PipedInputStream import java.io.PipedOutputStream import java.io.PrintWriter import java.io.StringWriter +import java.net.Proxy +import java.net.URI import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.util.Base64 import java.util.concurrent.Future import kotlin.time.Duration.Companion.seconds @@ -250,13 +260,55 @@ private class AmazonQServerInstance(private val project: Project, private val cs init { // will cause slow service init, but maybe fine for now. will not block UI since fetch/extract will be under background progress val artifact = runBlocking { ArtifactManager(project, manifestRange = null).fetchArtifact() }.toAbsolutePath() + + // more slowness + // make assumption that all requests will resolve to the same CA + // also terrible assumption that default endpoint is reachable + val qUri = URI(QEndpoints.Q_DEFAULT_SERVICE_CONFIG.ENDPOINT) + val rtsTrustChain = TrustChainUtil.getTrustChain(qUri) + val extraCaCerts = Files.createTempFile("q-extra-ca", ".pem").apply { + writeText( + buildList { + rtsTrustChain.forEach { + add("-----BEGIN CERTIFICATE-----") + add(Base64.getMimeEncoder(64, System.lineSeparator().toByteArray()).encodeToString(it.encoded)) + add("-----END CERTIFICATE-----") + } + }.joinToString(separator = System.lineSeparator()) + ) + } + val node = if (SystemInfo.isWindows) "node.exe" else "node" val cmd = GeneralCommandLine( artifact.resolve(node).toString(), LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(), "--stdio", "--set-credentials-encryption-key", + ).withEnvironment( + buildMap { + put("NODE_EXTRA_CA_CERTS", extraCaCerts.toAbsolutePath().toString()) + + val proxy = JdkProxyProvider.getInstance().proxySelector.select(qUri) + // log if only socks proxy available + .firstOrNull { it.type() == Proxy.Type.HTTP } + + if (proxy != null) { + val address = proxy.address() + if (address is java.net.InetSocketAddress) { + put( + "HTTPS_PROXY", + URIBuilder("http://${address.hostname}:${address.port}").apply { + val login = HttpConfigurable.getInstance().proxyLogin + if (login != null) { + setUserInfo(login, HttpConfigurable.getInstance().plainProxyPassword) + } + }.build().toASCIIString() + ) + } + } + } ) + .withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE) launcherHandler = KillableColoredProcessHandler.Silent(cmd) val inputWrapper = LSPProcessListener() From 548fa257d1d653aa7d840f2b935b342677bf6982 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Fri, 18 Apr 2025 13:16:41 -0700 Subject: [PATCH 3/8] configurable --- .../jetbrains/services/amazonq/lsp/AmazonQLspService.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index 01a7a1f8500..4290a705fbd 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -20,9 +20,8 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Key import com.intellij.openapi.util.SystemInfo import com.intellij.util.io.await -import com.intellij.util.net.HttpConfigurable import com.intellij.util.net.JdkProxyProvider -import io.ktor.util.network.hostname +import com.intellij.util.net.ProxyAuthentication import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.TimeoutCancellationException @@ -297,10 +296,10 @@ private class AmazonQServerInstance(private val project: Project, private val cs if (address is java.net.InetSocketAddress) { put( "HTTPS_PROXY", - URIBuilder("http://${address.hostname}:${address.port}").apply { - val login = HttpConfigurable.getInstance().proxyLogin + URIBuilder("http://${address.hostName}:${address.port}").apply { + val login = ProxyAuthentication.getInstance().getKnownAuthentication(address.hostName, address.port) if (login != null) { - setUserInfo(login, HttpConfigurable.getInstance().plainProxyPassword) + setUserInfo(login.userName, login.getPasswordAsString()) } }.build().toASCIIString() ) From 57ecb8b7ad34100bad5ef8f7aa8c01d8fae71d5e Mon Sep 17 00:00:00 2001 From: Richard Li Date: Tue, 22 Apr 2025 10:23:01 -0700 Subject: [PATCH 4/8] wip --- .../services/amazonq/lsp/TrustChainUtil.kt | 3 +- .../amazonq/lsp/TrustChainUtilTest.kt | 279 ++++++++++++++++++ 2 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt index a6a99abd6cd..a63f56d8708 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt @@ -94,7 +94,8 @@ object TrustChainUtil { val trustChain = try { resolveTrustChain(certificates.toList(), ks) } catch (e: Exception) { - LOG.warn(e) { "Passed Apache PKIX verification but could not build trust anchor via CertPathBuilder" } + // Java PKIX is happy with leaf cert in certification path, but Node.JS will not respect in NODE_CA_CERTS + LOG.warn(e) { "Passed Apache PKIX verification but could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not intermediate" } emptyList() } diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt new file mode 100644 index 00000000000..3eb855df8ea --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt @@ -0,0 +1,279 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp + +import com.github.tomakehurst.wiremock.common.Slf4jNotifier +import com.github.tomakehurst.wiremock.common.ssl.KeyStoreSettings +import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig +import com.github.tomakehurst.wiremock.junit5.WireMockExtension +import com.intellij.testFramework.ApplicationExtension +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.junit.jupiter.api.extension.RegisterExtension +import java.net.URI +import java.security.cert.X509Certificate + +import java.math.BigInteger +import java.security.KeyPairGenerator +import java.security.KeyStore +import java.util.* +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.* +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import software.aws.toolkits.core.utils.outputStream +import java.nio.file.Files +import java.nio.file.Path +import java.security.KeyPair +import java.security.PrivateKey +import java.time.Instant +import java.time.temporal.ChronoUnit + +@ExtendWith(ApplicationExtension::class) +class TrustChainUtilTest { + companion object { + @RegisterExtension + @JvmStatic + val wm1 = WireMockExtension.newInstance() + .options( + wireMockConfig() + .httpDisabled(true) + .http2TlsDisabled(true) + .keystorePath(Files.createTempFile("certs", "jks").toAbsolutePath().apply { CertificateGenerator.generateCertificateChain(this) }.toString()) + .keystoreType("jks") + .keystorePassword("changeit") + .keyManagerPassword("changeit") + .dynamicHttpsPort() + .notifier(Slf4jNotifier(true)) + ) + .build() + } + + @Test + fun `TrustChainUtil should return a valid trust chain`() { + val trustChain = TrustChainUtil.getTrustChain(URI("https://localhost:${wm1.httpsPort}")) + println(trustChain) + assert(trustChain.isNotEmpty()) + } +} + +class CertificateGenerator { + companion object { + private const val KEY_ALGORITHM = "RSA" + private const val SIGNATURE_ALGORITHM = "SHA256withRSA" + private const val KEY_SIZE = 4096 + + fun generateCertificateChain(keystorePath: Path) { + // Generate Root CA + val rootKeyPair = generateKeyPair() + val rootCert = generateRootCertificate(rootKeyPair) + + // Generate Intermediate CA + val intermediateKeyPair = generateKeyPair() + val intermediateCert = generateIntermediateCertificate( + intermediateKeyPair, + rootCert, + rootKeyPair.private + ) + + // Generate Leaf Certificate + val leafKeyPair = generateKeyPair() + val leafCert = generateLeafCertificate( + leafKeyPair, + intermediateCert, + intermediateKeyPair.private + ) + + // Store in KeyStore + saveToKeyStore( + keystorePath, + rootKeyPair, rootCert, + intermediateKeyPair, intermediateCert, + leafKeyPair, leafCert + ) + } + + private fun generateKeyPair(): KeyPair = + KeyPairGenerator.getInstance(KEY_ALGORITHM).apply { + initialize(KEY_SIZE) + }.generateKeyPair() + + private fun generateRootCertificate(keyPair: KeyPair): X509Certificate { + val name = X500Name("CN=Root CA,O=My Organization,C=US") + + val now = Instant.now() + val startDate = Date.from(now) + val endDate = Date.from(now.plus(3650, ChronoUnit.DAYS)) // 10 years validity + + val certBuilder = JcaX509v3CertificateBuilder( + name, // issuer + BigInteger.valueOf(System.currentTimeMillis()), + startDate, + endDate, + name, // subject (same as issuer for root CA) + keyPair.public + ).apply { + // Add Extensions + addExtension( + Extension.basicConstraints, + true, + BasicConstraints(true) + ) + addExtension( + Extension.keyUsage, + true, + KeyUsage(KeyUsage.keyCertSign or KeyUsage.cRLSign) + ) + } + + // Sign the certificate + val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM) + .build(keyPair.private) + + return JcaX509CertificateConverter() + .getCertificate(certBuilder.build(signer)) + } + + private fun generateIntermediateCertificate( + intermediateKeyPair: KeyPair, + issuerCert: X509Certificate, + issuerPrivateKey: PrivateKey + ): X509Certificate { + val subjectName = X500Name("CN=Intermediate CA,O=My Organization,C=US") + + val now = Instant.now() + val startDate = Date.from(now) + val endDate = Date.from(now.plus(1825, ChronoUnit.DAYS)) // 5 years validity + + val certBuilder = JcaX509v3CertificateBuilder( + issuerCert, + BigInteger.valueOf(System.currentTimeMillis()), + startDate, + endDate, + subjectName, + intermediateKeyPair.public + ).apply { + // Add Extensions + addExtension( + Extension.basicConstraints, + true, + BasicConstraints(true) + ) + addExtension( + Extension.keyUsage, + true, + KeyUsage(KeyUsage.keyCertSign or KeyUsage.cRLSign) + ) + } + + val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM) + .build(issuerPrivateKey) + + return JcaX509CertificateConverter() + .getCertificate(certBuilder.build(signer)) + } + + private fun generateLeafCertificate( + leafKeyPair: KeyPair, + issuerCert: X509Certificate, + issuerPrivateKey: PrivateKey + ): X509Certificate { + val subjectName = X500Name("CN=localhost,O=My Organization,C=US") + + val now = Instant.now() + val startDate = Date.from(now) + val endDate = Date.from(now.plus(365, ChronoUnit.DAYS)) // 1 year validity + + val certBuilder = JcaX509v3CertificateBuilder( + issuerCert, + BigInteger.valueOf(System.currentTimeMillis()), + startDate, + endDate, + subjectName, + leafKeyPair.public + ).apply { + // Add Extensions + addExtension( + Extension.basicConstraints, + true, + BasicConstraints(false) + ) + addExtension( + Extension.keyUsage, + true, + KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment) + ) + + // Add Subject Alternative Names (SAN) + val subjectAltNames = GeneralNames( + arrayOf( + GeneralName(GeneralName.dNSName, "localhost"), + GeneralName(GeneralName.iPAddress, "127.0.0.1"), + GeneralName(GeneralName.iPAddress, "::1") + ) + ) + + addExtension( + Extension.subjectAlternativeName, + false, + subjectAltNames + ) + } + + val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM) + .build(issuerPrivateKey) + + return JcaX509CertificateConverter() + .getCertificate(certBuilder.build(signer)) + } + + private fun saveToKeyStore( + keystorePath: Path, + rootKeyPair: KeyPair, + rootCert: X509Certificate, + intermediateKeyPair: KeyPair, + intermediateCert: X509Certificate, + leafKeyPair: KeyPair, + leafCert: X509Certificate + ) { + val password = "changeit".toCharArray() + + // Create KeyStore + val keyStore = KeyStore.getInstance("JKS").apply { + load(null, password) + } + + // Store root CA +// keyStore.setKeyEntry( +// "root", +// rootKeyPair.private, +// password, +// arrayOf(rootCert) +// ) + +// // Store intermediate CA +// keyStore.setKeyEntry( +// "intermediate", +// intermediateKeyPair.private, +// password, +// arrayOf(intermediateCert, rootCert) +// ) + + // Store leaf certificate + keyStore.setKeyEntry( + "leaf", + leafKeyPair.private, + password, + arrayOf(leafCert, intermediateCert) + ) + + // Save to file + keystorePath.outputStream().use { fos -> + keyStore.store(fos, password) + } + } + } +} From c2433011c8e5561f93288cd6709789e8a9659f0d Mon Sep 17 00:00:00 2001 From: Richard Li Date: Thu, 24 Apr 2025 15:41:53 -0700 Subject: [PATCH 5/8] wip --- .../amazonq/lsp/TrustChainUtilTest.kt | 187 ++++++++++++------ 1 file changed, 129 insertions(+), 58 deletions(-) diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt index 3eb855df8ea..380c83df282 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt @@ -3,27 +3,31 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp +import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.common.Slf4jNotifier -import com.github.tomakehurst.wiremock.common.ssl.KeyStoreSettings +import com.github.tomakehurst.wiremock.core.WireMockConfiguration import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig -import com.github.tomakehurst.wiremock.junit5.WireMockExtension import com.intellij.testFramework.ApplicationExtension +import com.intellij.util.net.ssl.CertificateManager +import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith -import org.junit.jupiter.api.extension.RegisterExtension import java.net.URI import java.security.cert.X509Certificate - import java.math.BigInteger import java.security.KeyPairGenerator import java.security.KeyStore -import java.util.* import org.bouncycastle.asn1.x500.X500Name -import org.bouncycastle.asn1.x509.* -import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.asn1.x509.BasicConstraints +import org.bouncycastle.asn1.x509.Extension +import org.bouncycastle.asn1.x509.GeneralName +import org.bouncycastle.asn1.x509.GeneralNames +import org.bouncycastle.asn1.x509.KeyUsage import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.junit.jupiter.api.extension.AfterAllCallback +import org.junit.jupiter.api.extension.ExtensionContext import software.aws.toolkits.core.utils.outputStream import java.nio.file.Files import java.nio.file.Path @@ -31,32 +35,121 @@ import java.security.KeyPair import java.security.PrivateKey import java.time.Instant import java.time.temporal.ChronoUnit +import java.util.Date @ExtendWith(ApplicationExtension::class) -class TrustChainUtilTest { +class TrustChainUtilTest : AfterAllCallback { companion object { - @RegisterExtension - @JvmStatic - val wm1 = WireMockExtension.newInstance() - .options( - wireMockConfig() - .httpDisabled(true) - .http2TlsDisabled(true) - .keystorePath(Files.createTempFile("certs", "jks").toAbsolutePath().apply { CertificateGenerator.generateCertificateChain(this) }.toString()) - .keystoreType("jks") - .keystorePassword("changeit") - .keyManagerPassword("changeit") - .dynamicHttpsPort() - .notifier(Slf4jNotifier(true)) - ) - .build() + private val certs = CertificateGenerator.generateCertificateChain() + } + + override fun afterAll(context: ExtensionContext) { + CertificateManager.getInstance().customTrustManager.apply { + certificates.toList().forEach { removeCertificate(it) } + } + + assertThat(CertificateManager.getInstance().customTrustManager.certificates).isEmpty() + } + + @Test + fun `returns chain from server if leaf is trust anchor`() { + mockWithOptions( + { + it.keystorePath( + Files.createTempFile("certs", "jks") + .toAbsolutePath() + .apply { + CertificateGenerator.saveToKeyStore( + this, + certs.values.first(), + certs.keys.take(2).toTypedArray(), + ) + } + .toString() + ) + } + ) { + val trustChain = TrustChainUtil.getTrustChain(URI("https://localhost:${it.httpsPort()}")) + // leaf, intermediate + assertThat(trustChain) + .isEqualTo(certs.keys.take(2).toList()) + } + } + + @Test + fun `returns entire chain if CA is trusted`() { + CertificateManager.getInstance().customTrustManager.addCertificate(certs.keys.last()) + + mockWithOptions( + { + it.keystorePath( + Files.createTempFile("certs", "jks") + .toAbsolutePath() + .apply { + CertificateGenerator.saveToKeyStore( + this, + certs.values.first(), + certs.keys.take(2).toTypedArray(), + ) + } + .toString() + ) + } + ) { + val trustChain = TrustChainUtil.getTrustChain(URI("https://localhost:${it.httpsPort()}")) + // leaf, intermediate, root + assertThat(trustChain) + .isEqualTo(certs.keys.toList()) + } } @Test - fun `TrustChainUtil should return a valid trust chain`() { - val trustChain = TrustChainUtil.getTrustChain(URI("https://localhost:${wm1.httpsPort}")) - println(trustChain) - assert(trustChain.isNotEmpty()) + fun `returns entire chain if CA is trusted but only returns leaf`() { + CertificateManager.getInstance().customTrustManager.addCertificate(certs.keys.last()) + + mockWithOptions( + { + it.keystorePath( + Files.createTempFile("certs", "jks") + .toAbsolutePath() + .apply { + CertificateGenerator.saveToKeyStore( + this, + certs.values.first(), + certs.keys.take(1).toTypedArray(), + ) + } + .toString() + ) + } + ) { + val trustChain = TrustChainUtil.getTrustChain(URI("https://localhost:${it.httpsPort()}")) + // leaf, intermediate, root + assertThat(trustChain) + .isEqualTo(certs.keys.toList()) + } + } + + private fun mockWithOptions(options: (WireMockConfiguration) -> Unit, runnable: (WireMockServer) -> Unit) { + val server = WireMockServer( + wireMockConfig() + .httpDisabled(true) + .http2TlsDisabled(true) + .keystoreType("jks") + .keystorePassword("changeit") + .keyManagerPassword("changeit") + .dynamicHttpsPort() + .notifier(Slf4jNotifier(true)) + .apply { options(this) } + ) + + try { + server.start() + + runnable(server) + } finally { + server.stop() + } } } @@ -66,7 +159,7 @@ class CertificateGenerator { private const val SIGNATURE_ALGORITHM = "SHA256withRSA" private const val KEY_SIZE = 4096 - fun generateCertificateChain(keystorePath: Path) { + fun generateCertificateChain(): Map { // Generate Root CA val rootKeyPair = generateKeyPair() val rootCert = generateRootCertificate(rootKeyPair) @@ -87,12 +180,10 @@ class CertificateGenerator { intermediateKeyPair.private ) - // Store in KeyStore - saveToKeyStore( - keystorePath, - rootKeyPair, rootCert, - intermediateKeyPair, intermediateCert, - leafKeyPair, leafCert + return linkedMapOf( + leafCert to leafKeyPair, + intermediateCert to intermediateKeyPair, + rootCert to rootKeyPair, ) } @@ -213,7 +304,7 @@ class CertificateGenerator { GeneralName(GeneralName.dNSName, "localhost"), GeneralName(GeneralName.iPAddress, "127.0.0.1"), GeneralName(GeneralName.iPAddress, "::1") - ) + ) ) addExtension( @@ -230,14 +321,10 @@ class CertificateGenerator { .getCertificate(certBuilder.build(signer)) } - private fun saveToKeyStore( + fun saveToKeyStore( keystorePath: Path, - rootKeyPair: KeyPair, - rootCert: X509Certificate, - intermediateKeyPair: KeyPair, - intermediateCert: X509Certificate, leafKeyPair: KeyPair, - leafCert: X509Certificate + trustChain: Array ) { val password = "changeit".toCharArray() @@ -246,28 +333,12 @@ class CertificateGenerator { load(null, password) } - // Store root CA -// keyStore.setKeyEntry( -// "root", -// rootKeyPair.private, -// password, -// arrayOf(rootCert) -// ) - -// // Store intermediate CA -// keyStore.setKeyEntry( -// "intermediate", -// intermediateKeyPair.private, -// password, -// arrayOf(intermediateCert, rootCert) -// ) - // Store leaf certificate keyStore.setKeyEntry( "leaf", leafKeyPair.private, password, - arrayOf(leafCert, intermediateCert) + trustChain ) // Save to file From 37fd98777eadaa6ed294cc2f273bea05799cc3b5 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Fri, 25 Apr 2025 11:52:35 -0700 Subject: [PATCH 6/8] tst --- .../services/amazonq/lsp/AmazonQLspService.kt | 8 +- .../services/amazonq/lsp/TrustChainUtil.kt | 80 +++++---- .../amazonq/lsp/TrustChainUtilTest.kt | 155 ++++++++++++++++-- 3 files changed, 191 insertions(+), 52 deletions(-) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index 4290a705fbd..3d1514d7947 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -267,13 +267,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs val rtsTrustChain = TrustChainUtil.getTrustChain(qUri) val extraCaCerts = Files.createTempFile("q-extra-ca", ".pem").apply { writeText( - buildList { - rtsTrustChain.forEach { - add("-----BEGIN CERTIFICATE-----") - add(Base64.getMimeEncoder(64, System.lineSeparator().toByteArray()).encodeToString(it.encoded)) - add("-----END CERTIFICATE-----") - } - }.joinToString(separator = System.lineSeparator()) + TrustChainUtil.certsToPem(rtsTrustChain) ) } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt index a63f56d8708..e706abb7b2a 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt @@ -11,8 +11,10 @@ import org.apache.http.conn.ssl.DefaultHostnameVerifier import org.apache.http.impl.client.HttpClientBuilder import org.apache.http.impl.client.SystemDefaultCredentialsProvider import org.apache.http.impl.conn.SystemDefaultRoutePlanner +import org.jetbrains.annotations.TestOnly import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.warn +import software.aws.toolkits.core.utils.writeText import java.net.URI import java.security.KeyStore import java.security.cert.CertPathBuilder @@ -23,11 +25,15 @@ import java.security.cert.PKIXBuilderParameters import java.security.cert.PKIXCertPathBuilderResult import java.security.cert.X509CertSelector import java.security.cert.X509Certificate +import java.util.Base64 import kotlin.collections.ifEmpty object TrustChainUtil { private val LOG = getLogger() + @TestOnly + fun resolveTrustChain(certs: Collection, trustAnchors: Collection) = resolveTrustChain(certs, keystoreFromCertificates(trustAnchors)) + /** * Build and validate the complete certificate chain * @param certs The end-entity certificate @@ -35,34 +41,41 @@ object TrustChainUtil { * @return The complete certificate chain */ fun resolveTrustChain(certs: Collection, trustAnchors: KeyStore): List { - // Create the selector for the certificate - val selector = X509CertSelector() - selector.certificate = certs.first() - - // Create the parameters for path validation - val pkixParams = PKIXBuilderParameters(trustAnchors, selector) - - // Disable CRL checking since we just want to build the path - pkixParams.isRevocationEnabled = false - - // Create a CertStore containing the certificate we want to validate - val ccsp = CollectionCertStoreParameters(certs) - val certStore = CertStore.getInstance("Collection", ccsp) - pkixParams.addCertStore(certStore) - - // Get the certification path - val builder = CertPathBuilder.getInstance("PKIX") - val result = builder.build(pkixParams) as PKIXCertPathBuilderResult - val certPath = result.certPath - val chain = (certPath.certificates as List).toMutableList() - - // Add the trust anchor (root CA) to complete the chain - val trustAnchorCert = result.trustAnchor.trustedCert - if (trustAnchorCert != null) { - chain.add(trustAnchorCert) - } + try { + // Create the selector for the certificate + val selector = X509CertSelector() + selector.certificate = certs.first() + + // Create the parameters for path validation + val pkixParams = PKIXBuilderParameters(trustAnchors, selector) + + // Disable CRL checking since we just want to build the path + pkixParams.isRevocationEnabled = false + + // Create a CertStore containing the certificate we want to validate + val ccsp = CollectionCertStoreParameters(certs) + val certStore = CertStore.getInstance("Collection", ccsp) + pkixParams.addCertStore(certStore) + + // Get the certification path + val builder = CertPathBuilder.getInstance("PKIX") + val result = builder.build(pkixParams) as PKIXCertPathBuilderResult + val certPath = result.certPath + val chain = (certPath.certificates as List).toMutableList() + + // Add the trust anchor (root CA) to complete the chain + val trustAnchorCert = result.trustAnchor.trustedCert + if (trustAnchorCert != null) { + chain.add(trustAnchorCert) + } - return chain + return chain + } catch (e: Exception) { + // Java PKIX is happy with leaf cert in certification path, but Node.JS will not respect in NODE_CA_CERTS + LOG.warn(e) { "Could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not intermediate" } + + return emptyList() + } } fun getTrustChain(uri: URI): List { @@ -81,7 +94,7 @@ object TrustChainUtil { .setSSLContext(CertificateManager.getInstance().sslContext) // client request will fail if user did not accept cert - client.build().execute(RequestBuilder.options(uri).build()) + client.build().use { it.execute(RequestBuilder.options(uri).build()) } val certificates = peerCerts as Array @@ -95,7 +108,7 @@ object TrustChainUtil { resolveTrustChain(certificates.toList(), ks) } catch (e: Exception) { // Java PKIX is happy with leaf cert in certification path, but Node.JS will not respect in NODE_CA_CERTS - LOG.warn(e) { "Passed Apache PKIX verification but could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not intermediate" } + LOG.warn(e) { "Passed Apache PKIX verification but could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not root" } emptyList() } @@ -106,6 +119,15 @@ object TrustChainUtil { } } + fun certsToPem(certs: List): String = + buildList { + certs.forEach { + add("-----BEGIN CERTIFICATE-----") + add(Base64.getMimeEncoder(64, System.lineSeparator().toByteArray()).encodeToString(it.encoded)) + add("-----END CERTIFICATE-----") + } + }.joinToString(separator = System.lineSeparator()) + private fun keystoreFromCertificates(certificates: Collection): KeyStore { val ks = KeyStore.getInstance(KeyStore.getDefaultType()) ks.load(null, null) diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt index 380c83df282..6c44d538a37 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt @@ -7,6 +7,8 @@ import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.common.Slf4jNotifier import com.github.tomakehurst.wiremock.core.WireMockConfiguration import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.util.ExecUtil import com.intellij.testFramework.ApplicationExtension import com.intellij.util.net.ssl.CertificateManager import org.assertj.core.api.Assertions.assertThat @@ -26,9 +28,11 @@ import org.bouncycastle.asn1.x509.KeyUsage import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder -import org.junit.jupiter.api.extension.AfterAllCallback -import org.junit.jupiter.api.extension.ExtensionContext +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.TestInstance import software.aws.toolkits.core.utils.outputStream +import software.aws.toolkits.core.utils.writeText import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair @@ -38,12 +42,15 @@ import java.time.temporal.ChronoUnit import java.util.Date @ExtendWith(ApplicationExtension::class) -class TrustChainUtilTest : AfterAllCallback { +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class TrustChainUtilTest { companion object { private val certs = CertificateGenerator.generateCertificateChain() } - override fun afterAll(context: ExtensionContext) { + @BeforeEach + @AfterEach + fun clearCerts() { CertificateManager.getInstance().customTrustManager.apply { certificates.toList().forEach { removeCertificate(it) } } @@ -77,7 +84,7 @@ class TrustChainUtilTest : AfterAllCallback { } @Test - fun `returns entire chain if CA is trusted`() { + fun `returns entire chain if CA is trust anchor`() { CertificateManager.getInstance().customTrustManager.addCertificate(certs.keys.last()) mockWithOptions( @@ -97,16 +104,43 @@ class TrustChainUtilTest : AfterAllCallback { } ) { val trustChain = TrustChainUtil.getTrustChain(URI("https://localhost:${it.httpsPort()}")) - // leaf, intermediate, root + // leaf, intermediate assertThat(trustChain) .isEqualTo(certs.keys.toList()) } } @Test - fun `returns entire chain if CA is trusted but only returns leaf`() { - CertificateManager.getInstance().customTrustManager.addCertificate(certs.keys.last()) + fun `returns empty if CA is trusted but does not provide intermediate`() { + val (leaf, _, root) = certs.keys.take(3) + assertThat( + TrustChainUtil.resolveTrustChain(listOf(leaf), listOf(root)) + ).isEmpty() + } + + @Test + fun `returns entire chain if CA is trusted and provides intermediate`() { + val (leaf, intermediate, root) = certs.keys.take(3) + assertThat( + TrustChainUtil.resolveTrustChain(listOf(leaf, intermediate), listOf(root)) + ).isEqualTo( + listOf(leaf, intermediate, root) + ) + } + @Test + fun `returns empty if CA is not trusted`() { + val (leaf, intermediate) = certs.keys.take(2) + assertThat( + TrustChainUtil.resolveTrustChain( + listOf(leaf, intermediate), + listOf(CertificateManager.getInstance().trustManager.acceptedIssuers.first()) + ) + ).isEmpty() + } + + @Test + fun `node accepts full chain`() { mockWithOptions( { it.keystorePath( @@ -116,20 +150,108 @@ class TrustChainUtilTest : AfterAllCallback { CertificateGenerator.saveToKeyStore( this, certs.values.first(), - certs.keys.take(1).toTypedArray(), + certs.keys.take(2).toTypedArray(), ) } .toString() ) + }, + { + val pemFile = Files.createTempFile("test", ".pem").apply { + writeText( + TrustChainUtil.certsToPem(certs.keys.toList()) + ) + } + + val output = ExecUtil.execAndGetOutput( + GeneralCommandLine( + "node", + "--use-bundled-ca", + Files.createTempFile("test", ".js").apply { writeText(nodeTest(it.httpsPort())) }.toAbsolutePath().toString(), + ).withEnvironment("NODE_EXTRA_CA_CERTS", pemFile.toAbsolutePath().toString()) + ) + + assertThat(output.exitCode).withFailMessage { "node validation failed: ${output.stdout}\n${output.stderr}" } + .isEqualTo(0) } - ) { - val trustChain = TrustChainUtil.getTrustChain(URI("https://localhost:${it.httpsPort()}")) - // leaf, intermediate, root - assertThat(trustChain) - .isEqualTo(certs.keys.toList()) - } + ) + } + + @Test + fun `node does not accept intermediate only`() { + mockWithOptions( + { + it.keystorePath( + Files.createTempFile("certs", "jks") + .toAbsolutePath() + .apply { + CertificateGenerator.saveToKeyStore( + this, + certs.values.first(), + certs.keys.take(2).toTypedArray(), + ) + } + .toString() + ) + }, + { + val pemFile = Files.createTempFile("test", ".pem").apply { + writeText( + TrustChainUtil.certsToPem(certs.keys.take(2).toList()) + ) + } + + // node does not support partial chains + val output = ExecUtil.execAndGetOutput( + GeneralCommandLine( + "node", + "--use-bundled-ca", + Files.createTempFile("test", ".js").apply { writeText(nodeTest(it.httpsPort())) }.toAbsolutePath().toString(), + ).withEnvironment("NODE_EXTRA_CA_CERTS", pemFile.toAbsolutePath().toString()) + ) + + assertThat(output.exitCode).withFailMessage { "node validation succeeded instead of failed: ${output.stdout}\n${output.stderr}" } + .isEqualTo(1) + } + ) } + // language=JavaScript + private fun nodeTest(port: Int) = """ + const https = require("https"); + + async function main() { // Wrapped in async function for better error handling + try { + const options = { + host: "localhost", + port: $port, + path: "/", + requestCert: true, + rejectUnauthorized: true, + }; + + const req = https.get(options, (res) => { + console.log("Certificate authorized:", res.socket.authorized); + const cert = res.socket.getPeerCertificate(); + console.log("Certificate details:", cert); + process.exit(0) + }); + + req.on("error", (err) => { // Added error handling + console.error("Request error:", err); + process.exit(1) + }); + + req.end(); + } catch (error) { + console.error("Error:", error); + process.exit(1) + } + } + + main(); + """.trimIndent() + private fun mockWithOptions(options: (WireMockConfiguration) -> Unit, runnable: (WireMockServer) -> Unit) { val server = WireMockServer( wireMockConfig() @@ -251,7 +373,8 @@ class CertificateGenerator { addExtension( Extension.basicConstraints, true, - BasicConstraints(true) + // not allowed to issue sub-CA + BasicConstraints(0) ) addExtension( Extension.keyUsage, From 22765921e332e3ca8a95992724d3178386f0b4fd Mon Sep 17 00:00:00 2001 From: Richard Li Date: Fri, 25 Apr 2025 13:41:40 -0700 Subject: [PATCH 7/8] lint --- .../services/amazonq/lsp/AmazonQLspService.kt | 1 - .../services/amazonq/lsp/TrustChainUtil.kt | 8 +- .../amazonq/lsp/TrustChainUtilTest.kt | 358 +++++++++--------- 3 files changed, 183 insertions(+), 184 deletions(-) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index 3d1514d7947..e38b3fcebd4 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -71,7 +71,6 @@ import java.net.Proxy import java.net.URI import java.nio.charset.StandardCharsets import java.nio.file.Files -import java.util.Base64 import java.util.concurrent.Future import kotlin.time.Duration.Companion.seconds diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt index e706abb7b2a..ff82c544307 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt @@ -14,7 +14,6 @@ import org.apache.http.impl.conn.SystemDefaultRoutePlanner import org.jetbrains.annotations.TestOnly import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.warn -import software.aws.toolkits.core.utils.writeText import java.net.URI import java.security.KeyStore import java.security.cert.CertPathBuilder @@ -32,7 +31,10 @@ object TrustChainUtil { private val LOG = getLogger() @TestOnly - fun resolveTrustChain(certs: Collection, trustAnchors: Collection) = resolveTrustChain(certs, keystoreFromCertificates(trustAnchors)) + fun resolveTrustChain(certs: Collection, trustAnchors: Collection) = resolveTrustChain( + certs, + keystoreFromCertificates(trustAnchors) + ) /** * Build and validate the complete certificate chain @@ -133,7 +135,7 @@ object TrustChainUtil { ks.load(null, null) certificates.forEachIndexed { index, cert -> ks.setCertificateEntry( - cert.getSubjectX500Principal().toString() + "-" + DigestUtil.sha256Hex(cert.encoded), + cert.subjectX500Principal.toString() + "-" + DigestUtil.sha256Hex(cert.encoded), cert ) } diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt index 6c44d538a37..ae2c9cd765c 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtilTest.kt @@ -12,13 +12,6 @@ import com.intellij.execution.util.ExecUtil import com.intellij.testFramework.ApplicationExtension import com.intellij.util.net.ssl.CertificateManager import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -import org.junit.jupiter.api.extension.ExtendWith -import java.net.URI -import java.security.cert.X509Certificate -import java.math.BigInteger -import java.security.KeyPairGenerator -import java.security.KeyStore import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.BasicConstraints import org.bouncycastle.asn1.x509.Extension @@ -30,13 +23,20 @@ import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.extension.ExtendWith import software.aws.toolkits.core.utils.outputStream import software.aws.toolkits.core.utils.writeText +import java.math.BigInteger +import java.net.URI import java.nio.file.Files import java.nio.file.Path import java.security.KeyPair +import java.security.KeyPairGenerator +import java.security.KeyStore import java.security.PrivateKey +import java.security.cert.X509Certificate import java.time.Instant import java.time.temporal.ChronoUnit import java.util.Date @@ -275,199 +275,197 @@ class TrustChainUtilTest { } } -class CertificateGenerator { - companion object { - private const val KEY_ALGORITHM = "RSA" - private const val SIGNATURE_ALGORITHM = "SHA256withRSA" - private const val KEY_SIZE = 4096 - - fun generateCertificateChain(): Map { - // Generate Root CA - val rootKeyPair = generateKeyPair() - val rootCert = generateRootCertificate(rootKeyPair) - - // Generate Intermediate CA - val intermediateKeyPair = generateKeyPair() - val intermediateCert = generateIntermediateCertificate( - intermediateKeyPair, - rootCert, - rootKeyPair.private - ) +object CertificateGenerator { + private const val KEY_ALGORITHM = "RSA" + private const val SIGNATURE_ALGORITHM = "SHA256withRSA" + private const val KEY_SIZE = 4096 + + fun generateCertificateChain(): Map { + // Generate Root CA + val rootKeyPair = generateKeyPair() + val rootCert = generateRootCertificate(rootKeyPair) + + // Generate Intermediate CA + val intermediateKeyPair = generateKeyPair() + val intermediateCert = generateIntermediateCertificate( + intermediateKeyPair, + rootCert, + rootKeyPair.private + ) - // Generate Leaf Certificate - val leafKeyPair = generateKeyPair() - val leafCert = generateLeafCertificate( - leafKeyPair, - intermediateCert, - intermediateKeyPair.private - ) + // Generate Leaf Certificate + val leafKeyPair = generateKeyPair() + val leafCert = generateLeafCertificate( + leafKeyPair, + intermediateCert, + intermediateKeyPair.private + ) - return linkedMapOf( - leafCert to leafKeyPair, - intermediateCert to intermediateKeyPair, - rootCert to rootKeyPair, + return linkedMapOf( + leafCert to leafKeyPair, + intermediateCert to intermediateKeyPair, + rootCert to rootKeyPair, + ) + } + + private fun generateKeyPair(): KeyPair = + KeyPairGenerator.getInstance(KEY_ALGORITHM).apply { + initialize(KEY_SIZE) + }.generateKeyPair() + + private fun generateRootCertificate(keyPair: KeyPair): X509Certificate { + val name = X500Name("CN=Root CA,O=My Organization,C=US") + + val now = Instant.now() + val startDate = Date.from(now) + val endDate = Date.from(now.plus(3650, ChronoUnit.DAYS)) // 10 years validity + + val certBuilder = JcaX509v3CertificateBuilder( + name, // issuer + BigInteger.valueOf(System.currentTimeMillis()), + startDate, + endDate, + name, // subject (same as issuer for root CA) + keyPair.public + ).apply { + // Add Extensions + addExtension( + Extension.basicConstraints, + true, + BasicConstraints(true) + ) + addExtension( + Extension.keyUsage, + true, + KeyUsage(KeyUsage.keyCertSign or KeyUsage.cRLSign) ) } - private fun generateKeyPair(): KeyPair = - KeyPairGenerator.getInstance(KEY_ALGORITHM).apply { - initialize(KEY_SIZE) - }.generateKeyPair() - - private fun generateRootCertificate(keyPair: KeyPair): X509Certificate { - val name = X500Name("CN=Root CA,O=My Organization,C=US") - - val now = Instant.now() - val startDate = Date.from(now) - val endDate = Date.from(now.plus(3650, ChronoUnit.DAYS)) // 10 years validity - - val certBuilder = JcaX509v3CertificateBuilder( - name, // issuer - BigInteger.valueOf(System.currentTimeMillis()), - startDate, - endDate, - name, // subject (same as issuer for root CA) - keyPair.public - ).apply { - // Add Extensions - addExtension( - Extension.basicConstraints, - true, - BasicConstraints(true) - ) - addExtension( - Extension.keyUsage, - true, - KeyUsage(KeyUsage.keyCertSign or KeyUsage.cRLSign) - ) - } + // Sign the certificate + val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM) + .build(keyPair.private) - // Sign the certificate - val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM) - .build(keyPair.private) + return JcaX509CertificateConverter() + .getCertificate(certBuilder.build(signer)) + } - return JcaX509CertificateConverter() - .getCertificate(certBuilder.build(signer)) + private fun generateIntermediateCertificate( + intermediateKeyPair: KeyPair, + issuerCert: X509Certificate, + issuerPrivateKey: PrivateKey, + ): X509Certificate { + val subjectName = X500Name("CN=Intermediate CA,O=My Organization,C=US") + + val now = Instant.now() + val startDate = Date.from(now) + val endDate = Date.from(now.plus(1825, ChronoUnit.DAYS)) // 5 years validity + + val certBuilder = JcaX509v3CertificateBuilder( + issuerCert, + BigInteger.valueOf(System.currentTimeMillis()), + startDate, + endDate, + subjectName, + intermediateKeyPair.public + ).apply { + // Add Extensions + addExtension( + Extension.basicConstraints, + true, + // not allowed to issue sub-CA + BasicConstraints(0) + ) + addExtension( + Extension.keyUsage, + true, + KeyUsage(KeyUsage.keyCertSign or KeyUsage.cRLSign) + ) } - private fun generateIntermediateCertificate( - intermediateKeyPair: KeyPair, - issuerCert: X509Certificate, - issuerPrivateKey: PrivateKey - ): X509Certificate { - val subjectName = X500Name("CN=Intermediate CA,O=My Organization,C=US") - - val now = Instant.now() - val startDate = Date.from(now) - val endDate = Date.from(now.plus(1825, ChronoUnit.DAYS)) // 5 years validity - - val certBuilder = JcaX509v3CertificateBuilder( - issuerCert, - BigInteger.valueOf(System.currentTimeMillis()), - startDate, - endDate, - subjectName, - intermediateKeyPair.public - ).apply { - // Add Extensions - addExtension( - Extension.basicConstraints, - true, - // not allowed to issue sub-CA - BasicConstraints(0) - ) - addExtension( - Extension.keyUsage, - true, - KeyUsage(KeyUsage.keyCertSign or KeyUsage.cRLSign) - ) - } - - val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM) - .build(issuerPrivateKey) + val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM) + .build(issuerPrivateKey) - return JcaX509CertificateConverter() - .getCertificate(certBuilder.build(signer)) - } + return JcaX509CertificateConverter() + .getCertificate(certBuilder.build(signer)) + } - private fun generateLeafCertificate( - leafKeyPair: KeyPair, - issuerCert: X509Certificate, - issuerPrivateKey: PrivateKey - ): X509Certificate { - val subjectName = X500Name("CN=localhost,O=My Organization,C=US") - - val now = Instant.now() - val startDate = Date.from(now) - val endDate = Date.from(now.plus(365, ChronoUnit.DAYS)) // 1 year validity - - val certBuilder = JcaX509v3CertificateBuilder( - issuerCert, - BigInteger.valueOf(System.currentTimeMillis()), - startDate, - endDate, - subjectName, - leafKeyPair.public - ).apply { - // Add Extensions - addExtension( - Extension.basicConstraints, - true, - BasicConstraints(false) - ) - addExtension( - Extension.keyUsage, - true, - KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment) - ) + private fun generateLeafCertificate( + leafKeyPair: KeyPair, + issuerCert: X509Certificate, + issuerPrivateKey: PrivateKey, + ): X509Certificate { + val subjectName = X500Name("CN=localhost,O=My Organization,C=US") + + val now = Instant.now() + val startDate = Date.from(now) + val endDate = Date.from(now.plus(365, ChronoUnit.DAYS)) // 1 year validity + + val certBuilder = JcaX509v3CertificateBuilder( + issuerCert, + BigInteger.valueOf(System.currentTimeMillis()), + startDate, + endDate, + subjectName, + leafKeyPair.public + ).apply { + // Add Extensions + addExtension( + Extension.basicConstraints, + true, + BasicConstraints(false) + ) + addExtension( + Extension.keyUsage, + true, + KeyUsage(KeyUsage.digitalSignature or KeyUsage.keyEncipherment) + ) - // Add Subject Alternative Names (SAN) - val subjectAltNames = GeneralNames( - arrayOf( - GeneralName(GeneralName.dNSName, "localhost"), - GeneralName(GeneralName.iPAddress, "127.0.0.1"), - GeneralName(GeneralName.iPAddress, "::1") - ) + // Add Subject Alternative Names (SAN) + val subjectAltNames = GeneralNames( + arrayOf( + GeneralName(GeneralName.dNSName, "localhost"), + GeneralName(GeneralName.iPAddress, "127.0.0.1"), + GeneralName(GeneralName.iPAddress, "::1") ) + ) - addExtension( - Extension.subjectAlternativeName, - false, - subjectAltNames - ) - } + addExtension( + Extension.subjectAlternativeName, + false, + subjectAltNames + ) + } - val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM) - .build(issuerPrivateKey) + val signer = JcaContentSignerBuilder(SIGNATURE_ALGORITHM) + .build(issuerPrivateKey) - return JcaX509CertificateConverter() - .getCertificate(certBuilder.build(signer)) - } + return JcaX509CertificateConverter() + .getCertificate(certBuilder.build(signer)) + } - fun saveToKeyStore( - keystorePath: Path, - leafKeyPair: KeyPair, - trustChain: Array - ) { - val password = "changeit".toCharArray() + fun saveToKeyStore( + keystorePath: Path, + leafKeyPair: KeyPair, + trustChain: Array, + ) { + val password = "changeit".toCharArray() - // Create KeyStore - val keyStore = KeyStore.getInstance("JKS").apply { - load(null, password) - } + // Create KeyStore + val keyStore = KeyStore.getInstance("JKS").apply { + load(null, password) + } - // Store leaf certificate - keyStore.setKeyEntry( - "leaf", - leafKeyPair.private, - password, - trustChain - ) + // Store leaf certificate + keyStore.setKeyEntry( + "leaf", + leafKeyPair.private, + password, + trustChain + ) - // Save to file - keystorePath.outputStream().use { fos -> - keyStore.store(fos, password) - } + // Save to file + keystorePath.outputStream().use { fos -> + keyStore.store(fos, password) } } } From 1644d9659bae127c0e6e33cb2495f6c311b94958 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Thu, 8 May 2025 12:03:41 -0700 Subject: [PATCH 8/8] build --- .../services/amazonq/lsp/AmazonQLspService.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index e38b3fcebd4..35caf5723ee 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -20,8 +20,8 @@ import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Key import com.intellij.openapi.util.SystemInfo import com.intellij.util.io.await +import com.intellij.util.net.HttpConfigurable import com.intellij.util.net.JdkProxyProvider -import com.intellij.util.net.ProxyAuthentication import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.TimeoutCancellationException @@ -58,7 +58,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.createExtended import software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument.TextDocumentServiceHandler import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler -import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints +import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata import software.aws.toolkits.jetbrains.settings.LspSettings import java.io.IOException @@ -259,10 +259,10 @@ private class AmazonQServerInstance(private val project: Project, private val cs // will cause slow service init, but maybe fine for now. will not block UI since fetch/extract will be under background progress val artifact = runBlocking { ArtifactManager(project, manifestRange = null).fetchArtifact() }.toAbsolutePath() - // more slowness + // more network calls // make assumption that all requests will resolve to the same CA // also terrible assumption that default endpoint is reachable - val qUri = URI(QEndpoints.Q_DEFAULT_SERVICE_CONFIG.ENDPOINT) + val qUri = URI(QDefaultServiceConfig.ENDPOINT) val rtsTrustChain = TrustChainUtil.getTrustChain(qUri) val extraCaCerts = Files.createTempFile("q-extra-ca", ".pem").apply { writeText( @@ -290,9 +290,9 @@ private class AmazonQServerInstance(private val project: Project, private val cs put( "HTTPS_PROXY", URIBuilder("http://${address.hostName}:${address.port}").apply { - val login = ProxyAuthentication.getInstance().getKnownAuthentication(address.hostName, address.port) + val login = HttpConfigurable.getInstance().proxyLogin if (login != null) { - setUserInfo(login.userName, login.getPasswordAsString()) + setUserInfo(login, HttpConfigurable.getInstance().plainProxyPassword) } }.build().toASCIIString() )