Skip to content

Commit 0ad31dd

Browse files
authored
impl: add support for disabling CLI signature verification (#166)
This PR implements a new configurable option to allow users to disable GPG signature verification for downloaded Coder CLI binaries. This feature provides flexibility for environments where signature verification may not be required or where fallback signature sources are not accessible. A new option `disableSignatureVerification` is now available only from the Settings page, with no quick shortcut in the main page to discourage users from quickly disabling this option. The `fallbackOnCoderForSignatures` is hidden/not available for configuration once signature verification is disabled. Additionally a rough draft for developer facing documentation regarding CLI signature verification was added. To make things more consistent with Coder Gateway, the fallback setting is always displayed if signature verification is enabled, we no longer display it only once in the main page. This PR is a port of coder/jetbrains-coder#564 from Coder Gateway. <img width="486" height="746" alt="image" src="https://github.com/user-attachments/assets/eff6f944-57ea-4926-857a-d5c5fd5d3901" /> <img width="486" height="746" alt="image" src="https://github.com/user-attachments/assets/7f1d39da-9777-4d5c-a329-e056fe38bf22" />
1 parent 82eee1f commit 0ad31dd

File tree

11 files changed

+116
-10
lines changed

11 files changed

+116
-10
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- support for skipping CLI signature verification
8+
59
### Changed
610

711
- URL validation is stricter in the connection screen and URI protocol handler

JETBRAINS_COMPLIANCE.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ This configuration includes JetBrains-specific rules that check for:
3939
- **ForbiddenImport**: Detects potentially bundled libraries
4040
- **Standard code quality rules**: Complexity, naming, performance, etc.
4141

42-
43-
4442
## CI/CD Integration
4543

4644
The GitHub Actions workflow `.github/workflows/jetbrains-compliance.yml` runs compliance checks on every PR and push.
@@ -55,8 +53,6 @@ The GitHub Actions workflow `.github/workflows/jetbrains-compliance.yml` runs co
5553
open build/reports/detekt/detekt.html
5654
```
5755

58-
59-
6056
## Understanding Results
6157

6258
### Compliance Check Results

README.md

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,69 @@ If `ide_product_code` and `ide_build_number` is missing, Toolbox will only open
109109
page. Coder Toolbox will attempt to start the workspace if it’s not already running; however, for the most reliable
110110
experience, it’s recommended to ensure the workspace is running prior to initiating the connection.
111111

112+
## GPG Signature Verification
113+
114+
The Coder Toolbox plugin starting with version *0.5.0* implements a comprehensive GPG signature verification system to
115+
ensure the authenticity and integrity of downloaded Coder CLI binaries. This security feature helps protect users from
116+
running potentially malicious or tampered binaries.
117+
118+
### How It Works
119+
120+
1. **Binary Download**: When connecting to a Coder deployment, the plugin downloads the appropriate Coder CLI binary for
121+
the user's operating system and architecture from the deployment's `/bin/` endpoint.
122+
123+
2. **Signature Download**: After downloading the binary, the plugin attempts to download the corresponding `.asc`
124+
signature file from the same location. The signature file is named according to the binary (e.g.,
125+
`coder-linux-amd64.asc` for `coder-linux-amd64`).
126+
127+
3. **Fallback Signature Sources**: If the signature is not available from the deployment, the plugin can optionally fall
128+
back to downloading signatures from `releases.coder.com`. This is controlled by the `fallbackOnCoderForSignatures`
129+
setting.
130+
131+
4. **GPG Verification**: The plugin uses the BouncyCastle library to verify the detached GPG signature against the
132+
downloaded binary using Coder's trusted public key.
133+
134+
5. **User Interaction**: If signature verification fails or signatures are unavailable, the plugin presents security
135+
warnings to users, allowing them to accept the risk and continue or abort the operation.
136+
137+
### Verification Process
138+
139+
The verification process involves several components:
140+
141+
- **`GPGVerifier`**: Handles the core GPG signature verification logic using BouncyCastle
142+
- **`VerificationResult`**: Represents the outcome of verification (Valid, Invalid, Failed, SignatureNotFound)
143+
- **`CoderDownloadService`**: Manages downloading both binaries and their signatures
144+
- **`CoderCLIManager`**: Orchestrates the download and verification workflow
145+
146+
### Configuration Options
147+
148+
Users can control signature verification behavior through plugin settings:
149+
150+
- **`disableSignatureVerification`**: When enabled, skips all signature verification. This is useful for clients running
151+
custom CLI builds, or customers with old deployment versions that don't have a signature published on
152+
`releases.coder.com`.
153+
- **`fallbackOnCoderForSignatures`**: When enabled, allows downloading signatures from `releases.coder.com` if not
154+
available from the deployment.
155+
156+
### Security Considerations
157+
158+
- The plugin embeds Coder's trusted public key in the plugin resources
159+
- Verification uses detached signatures, which are more secure than attached signatures
160+
- Users are warned about security risks when verification fails
161+
- The system gracefully handles cases where signatures are unavailable
162+
- All verification failures are logged for debugging purposes
163+
164+
### Error Handling
165+
166+
The system handles various failure scenarios:
167+
168+
- **Missing signatures**: Prompts user to accept risk or abort
169+
- **Invalid signatures**: Warns user about potential tampering and prompts user to accept risk or abort
170+
- **Verification failures**: Prompts user to accept risk or abort
171+
172+
This signature verification system ensures that users can trust the Coder CLI binaries they download through the plugin,
173+
protecting against supply chain attacks and ensuring binary integrity.
174+
112175
## Configuring and Testing workspace polling with HTTP & SOCKS5 Proxy
113176

114177
This section explains how to set up a local proxy and verify that

gradle.properties

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

src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ class CoderCLIManager(
181181
}
182182
}
183183

184+
if (context.settingsStore.disableSignatureVerification) {
185+
downloader.commit()
186+
context.logger.info("Skipping over CLI signature verification, it is disabled by the user")
187+
return true
188+
}
189+
184190
var signatureResult = withContext(Dispatchers.IO) {
185191
downloader.downloadSignature(showTextProgress)
186192
}

src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ interface ReadOnlyCoderSettings {
2929
val binaryDirectory: String?
3030

3131
/**
32-
* Controls whether we fall back release.coder.com
32+
* Controls whether we verify the cli signature
33+
*/
34+
val disableSignatureVerification: Boolean
35+
36+
/**
37+
* Controls whether we fall back on release.coder.com for signatures if signature validation is enabled
3338
*/
3439
val fallbackOnCoderForSignatures: SignatureFallbackStrategy
3540

src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ class CoderSettingsStore(
3838
override val defaultURL: String get() = store[DEFAULT_URL] ?: "https://dev.coder.com"
3939
override val binarySource: String? get() = store[BINARY_SOURCE]
4040
override val binaryDirectory: String? get() = store[BINARY_DIRECTORY]
41+
override val disableSignatureVerification: Boolean
42+
get() = store[DISABLE_SIGNATURE_VALIDATION]?.toBooleanStrictOrNull() ?: false
4143
override val fallbackOnCoderForSignatures: SignatureFallbackStrategy
4244
get() = SignatureFallbackStrategy.fromValue(store[FALLBACK_ON_CODER_FOR_SIGNATURES])
4345
override val defaultCliBinaryNameByOsAndArch: String get() = getCoderCLIForOS(getOS(), getArch())
@@ -166,6 +168,10 @@ class CoderSettingsStore(
166168
store[ENABLE_DOWNLOADS] = shouldEnableDownloads.toString()
167169
}
168170

171+
fun updateDisableSignatureVerification(shouldDisableSignatureVerification: Boolean) {
172+
store[DISABLE_SIGNATURE_VALIDATION] = shouldDisableSignatureVerification.toString()
173+
}
174+
169175
fun updateSignatureFallbackStrategy(fallback: Boolean) {
170176
store[FALLBACK_ON_CODER_FOR_SIGNATURES] = when (fallback) {
171177
true -> SignatureFallbackStrategy.ALLOW.toString()

src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ internal const val BINARY_SOURCE = "binarySource"
1010

1111
internal const val BINARY_DIRECTORY = "binaryDirectory"
1212

13+
internal const val DISABLE_SIGNATURE_VALIDATION = "disableSignatureValidation"
14+
1315
internal const val FALLBACK_ON_CODER_FOR_SIGNATURES = "signatureFallbackStrategy"
1416

1517
internal const val BINARY_NAME = "binaryName"

src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import com.jetbrains.toolbox.api.ui.components.CheckboxField
66
import com.jetbrains.toolbox.api.ui.components.TextField
77
import com.jetbrains.toolbox.api.ui.components.TextType
88
import com.jetbrains.toolbox.api.ui.components.UiField
9+
import kotlinx.coroutines.Job
910
import kotlinx.coroutines.channels.Channel
1011
import kotlinx.coroutines.channels.ClosedSendChannelException
1112
import kotlinx.coroutines.flow.MutableStateFlow
@@ -20,7 +21,7 @@ import kotlinx.coroutines.launch
2021
* TODO@JB: There is no scroll, and our settings do not fit. As a consequence,
2122
* I have not been able to test this page.
2223
*/
23-
class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel<Boolean>) :
24+
class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConfig: Channel<Boolean>) :
2425
CoderPage(MutableStateFlow(context.i18n.ptrl("Coder Settings")), false) {
2526
private val settings = context.settingsStore.readOnly()
2627

@@ -33,6 +34,11 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel<
3334
TextField(context.i18n.ptrl("Data directory"), settings.dataDirectory ?: "", TextType.General)
3435
private val enableDownloadsField =
3536
CheckboxField(settings.enableDownloads, context.i18n.ptrl("Enable downloads"))
37+
38+
private val disableSignatureVerificationField = CheckboxField(
39+
settings.disableSignatureVerification,
40+
context.i18n.ptrl("Disable Coder CLI signature verification")
41+
)
3642
private val signatureFallbackStrategyField =
3743
CheckboxField(
3844
settings.fallbackOnCoderForSignatures.isAllowed(),
@@ -65,13 +71,14 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel<
6571
private val networkInfoDirField =
6672
TextField(context.i18n.ptrl("SSH network metrics directory"), settings.networkInfoDir, TextType.General)
6773

68-
74+
private lateinit var visibilityUpdateJob: Job
6975
override val fields: StateFlow<List<UiField>> = MutableStateFlow(
7076
listOf(
7177
binarySourceField,
7278
enableDownloadsField,
7379
binaryDirectoryField,
7480
enableBinaryDirectoryFallbackField,
81+
disableSignatureVerificationField,
7582
signatureFallbackStrategyField,
7683
dataDirectoryField,
7784
headerCommandField,
@@ -94,6 +101,7 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel<
94101
context.settingsStore.updateBinaryDirectory(binaryDirectoryField.contentState.value)
95102
context.settingsStore.updateDataDirectory(dataDirectoryField.contentState.value)
96103
context.settingsStore.updateEnableDownloads(enableDownloadsField.checkedState.value)
104+
context.settingsStore.updateDisableSignatureVerification(disableSignatureVerificationField.checkedState.value)
97105
context.settingsStore.updateSignatureFallbackStrategy(signatureFallbackStrategyField.checkedState.value)
98106
context.settingsStore.updateBinaryDirectoryFallback(enableBinaryDirectoryFallbackField.checkedState.value)
99107
context.settingsStore.updateHeaderCommand(headerCommandField.contentState.value)
@@ -182,5 +190,19 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel<
182190
networkInfoDirField.contentState.update {
183191
settings.networkInfoDir
184192
}
193+
194+
visibilityUpdateJob = context.cs.launch {
195+
disableSignatureVerificationField.checkedState.collect { state ->
196+
signatureFallbackStrategyField.visibility.update {
197+
// the fallback checkbox should not be visible
198+
// if signature verification is disabled
199+
!state
200+
}
201+
}
202+
}
203+
}
204+
205+
override fun afterHide() {
206+
visibilityUpdateJob.cancel()
185207
}
186208
}

src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package com.coder.toolbox.views
22

33
import com.coder.toolbox.CoderToolboxContext
4-
import com.coder.toolbox.settings.SignatureFallbackStrategy
54
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
65
import com.coder.toolbox.util.toURL
76
import com.coder.toolbox.util.validateStrictWebUrl
@@ -41,7 +40,7 @@ class DeploymentUrlStep(
4140

4241
override val panel: RowGroup
4342
get() {
44-
if (context.settingsStore.fallbackOnCoderForSignatures == SignatureFallbackStrategy.NOT_CONFIGURED) {
43+
if (!context.settingsStore.disableSignatureVerification) {
4544
return RowGroup(
4645
RowGroup.RowField(urlField),
4746
RowGroup.RowField(emptyLine),

0 commit comments

Comments
 (0)