Skip to content

Commit cb3aae6

Browse files
authored
impl: verify cli signature (#148)
This PR introduces support for verifying the CLI binary using a detached PGP signature. Starting with version 2.24, Coder signs all CLI binaries. For clients using older versions or running TBX in air-gapped environments, unsigned CLIs can still be executed — but users will have to confirm it each time. In terms of code changes - the PR includes a big refactor around CLI downloading with most of the code refactored and extracted in various components that provide clean steps and result state in the main download method. Then the pgp verification logic was added on top, with some particularities: - the pgp public key is embedded in the plugin as a jar resource - we support multiple key rings in the public key - the user has the option of running the CLI if no signature was found - the signature search has a fallback approach: first we look in the Coder deployment, and then fall back to releases.coder.com to search for the signature if the user allows it. - we expect the signature to be under the same relative path as the CLI (we have an option which allows user to pick the CLI from a different source other than the Coder deployment)
1 parent e02c866 commit cb3aae6

23 files changed

+1230
-322
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

77
- support for matching workspace agent in the URI via the agent name
8+
- support for checking if CLI is signed
89

910
### Removed
1011

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ dependencies {
6363
ksp(libs.moshi.codegen)
6464
implementation(libs.retrofit)
6565
implementation(libs.retrofit.moshi)
66+
implementation(libs.bundles.bouncycastle)
6667
testImplementation(kotlin("test"))
6768
testImplementation(libs.mokk)
6869
testImplementation(libs.bundles.toolbox.plugin.api)

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
version=0.4.0
1+
version=0.5.0
22
group=com.coder.toolbox
33
name=coder-toolbox

gradle/libs.versions.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ gettext = "0.7.0"
1616
plugin-structure = "3.310"
1717
mockk = "1.14.4"
1818
detekt = "1.23.8"
19+
bouncycastle = "1.81"
1920

2021
[libraries]
2122
toolbox-core-api = { module = "com.jetbrains.toolbox:core-api", version.ref = "toolbox-plugin-api" }
@@ -34,10 +35,13 @@ retrofit-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.re
3435
plugin-structure = { module = "org.jetbrains.intellij.plugins:structure-toolbox", version.ref = "plugin-structure" }
3536
mokk = { module = "io.mockk:mockk", version.ref = "mockk" }
3637
marketplace-client = { module = "org.jetbrains.intellij:plugin-repository-rest-client", version.ref = "marketplace-client" }
38+
bouncycastle-bcpg = { module = "org.bouncycastle:bcpg-jdk18on", version.ref = "bouncycastle" }
39+
bouncycastle-bcprov = { module = "org.bouncycastle:bcprov-jdk18on", version.ref = "bouncycastle" }
3740

3841
[bundles]
3942
serialization = ["serialization-core", "serialization-json", "serialization-json-okio"]
4043
toolbox-plugin-api = ["toolbox-core-api", "toolbox-ui-api", "toolbox-remote-dev-api"]
44+
bouncycastle = ["bouncycastle-bcpg", "bouncycastle-bcprov"]
4145

4246
[plugins]
4347
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

src/main/kotlin/com/coder/toolbox/CoderRemoteProvider.kt

Lines changed: 40 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import kotlinx.coroutines.launch
3535
import kotlinx.coroutines.selects.onTimeout
3636
import kotlinx.coroutines.selects.select
3737
import java.net.URI
38+
import java.util.UUID
3839
import kotlin.coroutines.cancellation.CancellationException
3940
import kotlin.time.Duration.Companion.seconds
4041
import kotlin.time.TimeSource
@@ -302,31 +303,51 @@ class CoderRemoteProvider(
302303
* Handle incoming links (like from the dashboard).
303304
*/
304305
override suspend fun handleUri(uri: URI) {
305-
linkHandler.handle(
306-
uri, shouldDoAutoSetup(),
307-
{
308-
coderHeaderPage.isBusyCreatingNewEnvironment.update {
309-
true
306+
try {
307+
linkHandler.handle(
308+
uri, shouldDoAutoSetup(),
309+
{
310+
coderHeaderPage.isBusyCreatingNewEnvironment.update {
311+
true
312+
}
313+
},
314+
{
315+
coderHeaderPage.isBusyCreatingNewEnvironment.update {
316+
false
317+
}
310318
}
311-
},
312-
{
313-
coderHeaderPage.isBusyCreatingNewEnvironment.update {
319+
) { restClient, cli ->
320+
// stop polling and de-initialize resources
321+
close()
322+
isInitialized.update {
314323
false
315324
}
325+
// start initialization with the new settings
326+
this@CoderRemoteProvider.client = restClient
327+
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))
328+
329+
environments.showLoadingMessage()
330+
pollJob = poll(restClient, cli)
331+
isInitialized.waitForTrue()
316332
}
317-
) { restClient, cli ->
318-
// stop polling and de-initialize resources
319-
close()
320-
isInitialized.update {
333+
} catch (ex: Exception) {
334+
context.logger.error(ex, "")
335+
val textError = if (ex is APIResponseException) {
336+
if (!ex.reason.isNullOrBlank()) {
337+
ex.reason
338+
} else ex.message
339+
} else ex.message
340+
341+
context.ui.showSnackbar(
342+
UUID.randomUUID().toString(),
343+
context.i18n.ptrl("Error encountered while handling Coder URI"),
344+
context.i18n.pnotr(textError ?: ""),
345+
context.i18n.ptrl("Dismiss")
346+
)
347+
} finally {
348+
coderHeaderPage.isBusyCreatingNewEnvironment.update {
321349
false
322350
}
323-
// start initialization with the new settings
324-
this@CoderRemoteProvider.client = restClient
325-
coderHeaderPage.setTitle(context.i18n.pnotr(restClient.url.toString()))
326-
327-
environments.showLoadingMessage()
328-
pollJob = poll(restClient, cli)
329-
isInitialized.waitForTrue()
330351
}
331352
}
332353

0 commit comments

Comments
 (0)