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 28c28100f97..5b3b4dd107b 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 @@ -22,9 +22,12 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.openapi.util.Key import com.intellij.openapi.util.SystemInfo +import com.intellij.util.EnvironmentUtil +import com.intellij.util.io.DigestUtil import com.intellij.util.io.await import com.intellij.util.net.HttpConfigurable import com.intellij.util.net.JdkProxyProvider +import com.intellij.util.net.ssl.CertificateManager import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job @@ -77,6 +80,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument.TextDoc 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.QDefaultServiceConfig +import software.aws.toolkits.jetbrains.services.amazonq.profile.QEndpoints import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata import software.aws.toolkits.jetbrains.settings.LspSettings @@ -369,21 +373,60 @@ 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 { service().fetchArtifact(project) }.toAbsolutePath() - // 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(QDefaultServiceConfig.ENDPOINT) - val extraCaCerts = try { - val rtsTrustChain = TrustChainUtil.getTrustChain(qUri) - - Files.createTempFile("q-extra-ca", ".pem").apply { - writeText( - TrustChainUtil.certsToPem(rtsTrustChain) - ) + // make some network calls for troubleshooting + listOf(*QEndpoints.listRegionEndpoints().map { it.endpoint }.toTypedArray(), QDefaultServiceConfig.ENDPOINT).forEach { endpoint -> + try { + val qUri = URI(endpoint) + val rtsTrustChain = TrustChainUtil.getTrustChain(qUri) + val trustRoot = rtsTrustChain.last() + // ATS is cross-signed against starfield certs: https://www.amazontrust.com/repository/ + if (listOf("Amazon Root CA", "Starfield Technologies").any { trustRoot.subjectX500Principal.name.contains(it) }) { + LOG.info { "Trust chain for $endpoint ends with public-like CA with sha256 fingerprint: ${DigestUtil.sha256Hex(trustRoot.encoded)}" } + } else { + LOG.info { + """ + |Trust chain for $endpoint transits private CA: + |${buildString { + rtsTrustChain.forEach { cert -> + append("Issuer: ${cert.issuerX500Principal}, ") + append("Subject: ${cert.subjectX500Principal}, ") + append("Fingerprint: ${DigestUtil.sha256Hex(cert.encoded)}\n\t") + } + }} + """.trimMargin("|") + } + LOG.debug { "Full trust chain info for $endpoint: $rtsTrustChain" } + } + } catch (e: Exception) { + LOG.info { "${e.message}: Could not resolve trust chain for $endpoint" } } - } catch (e: Exception) { - LOG.info(e) { "Could not resolve trust chain for $qUri, skipping NODE_EXTRA_CA_CERTS" } + } + + val userEnvNodeCaCerts = EnvironmentUtil.getValue("NODE_EXTRA_CA_CERTS") + // if user has NODE_EXTRA_CA_CERTS in their environment, assume they know what they're doing + val extraCaCerts = if (!userEnvNodeCaCerts.isNullOrEmpty()) { + LOG.info { "Skipping injection of IDE trust store, user already defines NODE_EXTRA_CA_CERTS: $userEnvNodeCaCerts" } + null + } else { + try { + // otherwise include everything the IDE knows about + val allAcceptedIssuers = CertificateManager.getInstance().trustManager.acceptedIssuers + val customIssuers = CertificateManager.getInstance().customTrustManager.acceptedIssuers + LOG.info { + "Injecting ${allAcceptedIssuers.size} IDE trusted certificates (${customIssuers.size} from IDE custom manager) into NODE_EXTRA_CA_CERTS" + } + + Files.createTempFile("q-extra-ca", ".pem").apply { + writeText( + TrustChainUtil.certsToPem(allAcceptedIssuers.toList()) + ) + }.toAbsolutePath().toString() + } catch (e: Exception) { + LOG.warn(e) { "Could not inject IDE trust store into NODE_EXTRA_CA_CERTS" } + + null + } } val node = if (SystemInfo.isWindows) "node.exe" else "node" @@ -396,8 +439,13 @@ private class AmazonQServerInstance(private val project: Project, private val cs "--set-credentials-encryption-key", ).withEnvironment( buildMap { - extraCaCerts?.let { put("NODE_EXTRA_CA_CERTS", it.toAbsolutePath().toString()) } + extraCaCerts?.let { + LOG.info { "Starting Flare with NODE_EXTRA_CA_CERTS: $it" } + put("NODE_EXTRA_CA_CERTS", it) + } + // assume default endpoint will pick correct proxy if needed + val qUri = URI(QDefaultServiceConfig.ENDPOINT) val proxy = JdkProxyProvider.getInstance().proxySelector.select(qUri) // log if only socks proxy available .firstOrNull { it.type() == Proxy.Type.HTTP }