Skip to content

Commit 20f1abc

Browse files
authored
feat(lsp): respect IDE user proxy settings / forward trust store (#5553)
1 parent 7b34c28 commit 20f1abc

File tree

3 files changed

+659
-0
lines changed

3 files changed

+659
-0
lines changed

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import com.intellij.openapi.util.Disposer
2020
import com.intellij.openapi.util.Key
2121
import com.intellij.openapi.util.SystemInfo
2222
import com.intellij.util.io.await
23+
import com.intellij.util.net.HttpConfigurable
24+
import com.intellij.util.net.JdkProxyProvider
2325
import kotlinx.coroutines.CoroutineScope
2426
import kotlinx.coroutines.Deferred
2527
import kotlinx.coroutines.async
@@ -28,6 +30,7 @@ import kotlinx.coroutines.runBlocking
2830
import kotlinx.coroutines.sync.Mutex
2931
import kotlinx.coroutines.sync.withLock
3032
import kotlinx.coroutines.withTimeout
33+
import org.apache.http.client.utils.URIBuilder
3134
import org.eclipse.lsp4j.ClientCapabilities
3235
import org.eclipse.lsp4j.ClientInfo
3336
import org.eclipse.lsp4j.DidChangeConfigurationParams
@@ -46,6 +49,7 @@ import org.slf4j.event.Level
4649
import software.aws.toolkits.core.utils.getLogger
4750
import software.aws.toolkits.core.utils.info
4851
import software.aws.toolkits.core.utils.warn
52+
import software.aws.toolkits.core.utils.writeText
4953
import software.aws.toolkits.jetbrains.isDeveloperMode
5054
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager
5155
import software.aws.toolkits.jetbrains.services.amazonq.lsp.auth.DefaultAuthCredentialsService
@@ -58,6 +62,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.createExtended
5862
import software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument.TextDocumentServiceHandler
5963
import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders
6064
import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler
65+
import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig
6166
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
6267
import software.aws.toolkits.jetbrains.settings.LspSettings
6368
import java.io.IOException
@@ -66,7 +71,10 @@ import java.io.PipedInputStream
6671
import java.io.PipedOutputStream
6772
import java.io.PrintWriter
6873
import java.io.StringWriter
74+
import java.net.Proxy
75+
import java.net.URI
6976
import java.nio.charset.StandardCharsets
77+
import java.nio.file.Files
7078
import java.util.Collections
7179
import java.util.concurrent.Future
7280
import kotlin.time.Duration.Companion.seconds
@@ -261,13 +269,49 @@ private class AmazonQServerInstance(private val project: Project, private val cs
261269
init {
262270
// will cause slow service init, but maybe fine for now. will not block UI since fetch/extract will be under background progress
263271
val artifact = runBlocking { service<ArtifactManager>().fetchArtifact(project) }.toAbsolutePath()
272+
273+
// more network calls
274+
// make assumption that all requests will resolve to the same CA
275+
// also terrible assumption that default endpoint is reachable
276+
val qUri = URI(QDefaultServiceConfig.ENDPOINT)
277+
val rtsTrustChain = TrustChainUtil.getTrustChain(qUri)
278+
val extraCaCerts = Files.createTempFile("q-extra-ca", ".pem").apply {
279+
writeText(
280+
TrustChainUtil.certsToPem(rtsTrustChain)
281+
)
282+
}
283+
264284
val node = if (SystemInfo.isWindows) "node.exe" else "node"
265285
val cmd = GeneralCommandLine(
266286
artifact.resolve(node).toString(),
267287
LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(),
268288
"--stdio",
269289
"--set-credentials-encryption-key",
290+
).withEnvironment(
291+
buildMap {
292+
put("NODE_EXTRA_CA_CERTS", extraCaCerts.toAbsolutePath().toString())
293+
294+
val proxy = JdkProxyProvider.getInstance().proxySelector.select(qUri)
295+
// log if only socks proxy available
296+
.firstOrNull { it.type() == Proxy.Type.HTTP }
297+
298+
if (proxy != null) {
299+
val address = proxy.address()
300+
if (address is java.net.InetSocketAddress) {
301+
put(
302+
"HTTPS_PROXY",
303+
URIBuilder("http://${address.hostName}:${address.port}").apply {
304+
val login = HttpConfigurable.getInstance().proxyLogin
305+
if (login != null) {
306+
setUserInfo(login, HttpConfigurable.getInstance().plainProxyPassword)
307+
}
308+
}.build().toASCIIString()
309+
)
310+
}
311+
}
312+
}
270313
)
314+
.withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)
271315

272316
launcherHandler = KillableColoredProcessHandler.Silent(cmd)
273317
val inputWrapper = LSPProcessListener()
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.lsp
5+
6+
import com.intellij.util.io.DigestUtil
7+
import com.intellij.util.net.JdkProxyProvider
8+
import com.intellij.util.net.ssl.CertificateManager
9+
import org.apache.http.client.methods.RequestBuilder
10+
import org.apache.http.conn.ssl.DefaultHostnameVerifier
11+
import org.apache.http.impl.client.HttpClientBuilder
12+
import org.apache.http.impl.client.SystemDefaultCredentialsProvider
13+
import org.apache.http.impl.conn.SystemDefaultRoutePlanner
14+
import org.jetbrains.annotations.TestOnly
15+
import software.aws.toolkits.core.utils.getLogger
16+
import software.aws.toolkits.core.utils.warn
17+
import java.net.URI
18+
import java.security.KeyStore
19+
import java.security.cert.CertPathBuilder
20+
import java.security.cert.CertStore
21+
import java.security.cert.Certificate
22+
import java.security.cert.CollectionCertStoreParameters
23+
import java.security.cert.PKIXBuilderParameters
24+
import java.security.cert.PKIXCertPathBuilderResult
25+
import java.security.cert.X509CertSelector
26+
import java.security.cert.X509Certificate
27+
import java.util.Base64
28+
import kotlin.collections.ifEmpty
29+
30+
object TrustChainUtil {
31+
private val LOG = getLogger<TrustChainUtil>()
32+
33+
@TestOnly
34+
fun resolveTrustChain(certs: Collection<X509Certificate>, trustAnchors: Collection<X509Certificate>) = resolveTrustChain(
35+
certs,
36+
keystoreFromCertificates(trustAnchors)
37+
)
38+
39+
/**
40+
* Build and validate the complete certificate chain
41+
* @param certs The end-entity certificate
42+
* @param trustAnchors The truststore containing trusted CA certificates
43+
* @return The complete certificate chain
44+
*/
45+
fun resolveTrustChain(certs: Collection<X509Certificate>, trustAnchors: KeyStore): List<X509Certificate> {
46+
try {
47+
// Create the selector for the certificate
48+
val selector = X509CertSelector()
49+
selector.certificate = certs.first()
50+
51+
// Create the parameters for path validation
52+
val pkixParams = PKIXBuilderParameters(trustAnchors, selector)
53+
54+
// Disable CRL checking since we just want to build the path
55+
pkixParams.isRevocationEnabled = false
56+
57+
// Create a CertStore containing the certificate we want to validate
58+
val ccsp = CollectionCertStoreParameters(certs)
59+
val certStore = CertStore.getInstance("Collection", ccsp)
60+
pkixParams.addCertStore(certStore)
61+
62+
// Get the certification path
63+
val builder = CertPathBuilder.getInstance("PKIX")
64+
val result = builder.build(pkixParams) as PKIXCertPathBuilderResult
65+
val certPath = result.certPath
66+
val chain = (certPath.certificates as List<X509Certificate>).toMutableList()
67+
68+
// Add the trust anchor (root CA) to complete the chain
69+
val trustAnchorCert = result.trustAnchor.trustedCert
70+
if (trustAnchorCert != null) {
71+
chain.add(trustAnchorCert)
72+
}
73+
74+
return chain
75+
} catch (e: Exception) {
76+
// Java PKIX is happy with leaf cert in certification path, but Node.JS will not respect in NODE_CA_CERTS
77+
LOG.warn(e) { "Could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not intermediate" }
78+
79+
return emptyList()
80+
}
81+
}
82+
83+
fun getTrustChain(uri: URI): List<X509Certificate> {
84+
val proxyProvider = JdkProxyProvider.getInstance()
85+
var peerCerts: Array<Certificate> = emptyArray()
86+
val verifierDelegate = DefaultHostnameVerifier()
87+
val client = HttpClientBuilder.create()
88+
.setRoutePlanner(SystemDefaultRoutePlanner(proxyProvider.proxySelector))
89+
.setDefaultCredentialsProvider(SystemDefaultCredentialsProvider())
90+
.setSSLHostnameVerifier { hostname, sslSession ->
91+
peerCerts = sslSession.peerCertificates
92+
93+
verifierDelegate.verify(hostname, sslSession)
94+
}
95+
// prompt user via modal to accept certificate if needed; otherwise need to prompt separately prior to launching flare
96+
.setSSLContext(CertificateManager.getInstance().sslContext)
97+
98+
// client request will fail if user did not accept cert
99+
client.build().use { it.execute(RequestBuilder.options(uri).build()) }
100+
101+
val certificates = peerCerts as Array<X509Certificate>
102+
103+
// java default + custom system
104+
// excluding leaf cert for case where user has both leaf and issuing CA as trusted roots
105+
val allAccepted = CertificateManager.getInstance().trustManager.acceptedIssuers.toSet() - certificates.first()
106+
val ks = keystoreFromCertificates(allAccepted)
107+
108+
// if this throws then there is a bug because it passed PKIX validation in apache client
109+
val trustChain = try {
110+
resolveTrustChain(certificates.toList(), ks)
111+
} catch (e: Exception) {
112+
// Java PKIX is happy with leaf cert in certification path, but Node.JS will not respect in NODE_CA_CERTS
113+
LOG.warn(e) { "Passed Apache PKIX verification but could not build trust anchor via CertPathBuilder? maybe user accepted leaf cert but not root" }
114+
emptyList()
115+
}
116+
117+
// if trust chain is empty, then somehow user only trusts the leaf cert???
118+
return trustChain.ifEmpty {
119+
// so return the served certificate chain from the server and hope that works
120+
certificates.toList()
121+
}
122+
}
123+
124+
fun certsToPem(certs: List<X509Certificate>): String =
125+
buildList {
126+
certs.forEach {
127+
add("-----BEGIN CERTIFICATE-----")
128+
add(Base64.getMimeEncoder(64, System.lineSeparator().toByteArray()).encodeToString(it.encoded))
129+
add("-----END CERTIFICATE-----")
130+
}
131+
}.joinToString(separator = System.lineSeparator())
132+
133+
private fun keystoreFromCertificates(certificates: Collection<X509Certificate>): KeyStore {
134+
val ks = KeyStore.getInstance(KeyStore.getDefaultType())
135+
ks.load(null, null)
136+
certificates.forEachIndexed { index, cert ->
137+
ks.setCertificateEntry(
138+
cert.subjectX500Principal.toString() + "-" + DigestUtil.sha256Hex(cert.encoded),
139+
cert
140+
)
141+
}
142+
return ks
143+
}
144+
}

0 commit comments

Comments
 (0)