Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.io.await
import com.intellij.util.net.JdkProxyProvider
import com.intellij.util.net.ProxyAuthentication
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.TimeoutCancellationException
Expand All @@ -29,6 +31,7 @@
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
Expand All @@ -45,6 +48,7 @@
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
Expand All @@ -54,6 +58,7 @@
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
Expand All @@ -62,7 +67,11 @@
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

Check warning on line 74 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
import java.util.concurrent.Future
import kotlin.time.Duration.Companion.seconds

Expand Down Expand Up @@ -250,13 +259,49 @@
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: slowness added to which process?

// 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(
TrustChainUtil.certsToPem(rtsTrustChain)
)
}

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 = ProxyAuthentication.getInstance().getKnownAuthentication(address.hostName, address.port)
if (login != null) {
setUserInfo(login.userName, login.getPasswordAsString())
}
}.build().toASCIIString()
)
}
}
}
)
.withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)

launcherHandler = KillableColoredProcessHandler.Silent(cmd)
val inputWrapper = LSPProcessListener()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// 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 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

Check warning on line 17 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Unused import directive

Unused import directive
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 java.util.Base64
import kotlin.collections.ifEmpty

object TrustChainUtil {
private val LOG = getLogger<TrustChainUtil>()

@TestOnly
fun resolveTrustChain(certs: Collection<X509Certificate>, trustAnchors: Collection<X509Certificate>) = resolveTrustChain(certs, keystoreFromCertificates(trustAnchors))

/**
* 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<X509Certificate>, trustAnchors: KeyStore): List<X509Certificate> {

Check notice on line 43 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Class member can have 'private' visibility

Function 'resolveTrustChain' could be private
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<X509Certificate>).toMutableList()

// Add the trust anchor (root CA) to complete the chain
val trustAnchorCert = result.trustAnchor.trustedCert
if (trustAnchorCert != null) {
chain.add(trustAnchorCert)
}

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<X509Certificate> {
val proxyProvider = JdkProxyProvider.getInstance()
var peerCerts: Array<Certificate> = 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().use { it.execute(RequestBuilder.options(uri).build()) }

val certificates = peerCerts as Array<X509Certificate>

// 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) {
// 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 root" }
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()
}
}

fun certsToPem(certs: List<X509Certificate>): 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<X509Certificate>): KeyStore {
val ks = KeyStore.getInstance(KeyStore.getDefaultType())
ks.load(null, null)
certificates.forEachIndexed { index, cert ->
ks.setCertificateEntry(
cert.getSubjectX500Principal().toString() + "-" + DigestUtil.sha256Hex(cert.encoded),

Check notice on line 136 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/TrustChainUtil.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Accessor call that can be replaced with property access syntax

Use of getter method instead of property access syntax
cert
)
}
return ks
}
}
Loading
Loading