Skip to content

impl: add support for disabling CLI signature verification #564

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 30, 2025
Merged
Show file tree
Hide file tree
Changes from 7 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

## Unreleased

### Added

- support for skipping CLI signature verification

## 2.22.0 - 2025-07-25

### Added
Expand Down
64 changes: 64 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,70 @@ There are three ways to get into a workspace:

Currently the first two will configure SSH but the third does not yet.

## GPG Signature Verification

The Coder Gateway plugin starting with version *2.22.0* implements a comprehensive GPG signature verification system to
ensure the authenticity and integrity of downloaded Coder CLI binaries. This security feature helps protect users from
running potentially malicious or tampered binaries.

### How It Works

1. **Binary Download**: When connecting to a Coder deployment, the plugin downloads the appropriate Coder CLI binary for
the user's operating system and architecture from the deployment's `/bin/` endpoint.

2. **Signature Download**: After downloading the binary, the plugin attempts to download the corresponding `.asc`
signature file from the same location. The signature file is named according to the binary (e.g.,
`coder-linux-amd64.asc` for `coder-linux-amd64`).

3. **Fallback Signature Sources**: If the signature is not available from the deployment, the plugin can optionally fall
back to downloading signatures from `releases.coder.com`. This is controlled by the `fallbackOnCoderForSignatures`
setting.

4. **GPG Verification**: The plugin uses the BouncyCastle library shipped with Gateway app to verify the detached GPG
signature against the downloaded binary using Coder's trusted public key.

5. **User Interaction**: If signature verification fails or signatures are unavailable, the plugin presents security
warnings
to users, allowing them to accept the risk and continue or abort the operation.

### Verification Process

The verification process involves several components:

- **`GPGVerifier`**: Handles the core GPG signature verification logic using BouncyCastle
- **`VerificationResult`**: Represents the outcome of verification (Valid, Invalid, Failed, SignatureNotFound)
- **`CoderDownloadService`**: Manages downloading both binaries and their signatures
- **`CoderCLIManager`**: Orchestrates the download and verification workflow

### Configuration Options

Users can control signature verification behavior through plugin settings:

- **`disableSignatureVerification`**: When enabled, skips all signature verification. This is useful for clients running
custom CLI builds, or
customers with old deployment versions that don't have a signature published on `releases.coder.com`.
- **`fallbackOnCoderForSignatures`**: When enabled, allows downloading signatures from `releases.coder.com` if not
available from the deployment

### Security Considerations

- The plugin embeds Coder's trusted public key in the plugin resources
- Verification uses detached signatures, which are more secure than attached signatures
- Users are warned about security risks when verification fails
- The system gracefully handles cases where signatures are unavailable
- All verification failures are logged for debugging purposes

### Error Handling

The system handles various failure scenarios:

- **Missing signatures**: Prompts user to accept risk or abort
- **Invalid signatures**: Warns user about potential tampering and prompts user to accept risk or abort
- **Verification failures**: Prompts user to accept risk or abort

This signature verification system ensures that users can trust the Coder CLI binaries they download through the plugin,
protecting against supply chain attacks and ensuring binary integrity.

## Development

To manually install a local build:
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pluginGroup=com.coder.gateway
artifactName=coder-gateway
pluginName=Coder
# SemVer format -> https://semver.org
pluginVersion=2.22.0
pluginVersion=2.22.1
# See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html
# for insight into build numbers and IntelliJ Platform versions.
pluginSinceBuild=243.26574
Expand Down
48 changes: 30 additions & 18 deletions src/main/kotlin/com/coder/gateway/CoderSettingsConfigurable.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import com.intellij.openapi.components.service
import com.intellij.openapi.options.BoundConfigurable
import com.intellij.openapi.ui.DialogPanel
import com.intellij.openapi.ui.ValidationInfo
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.AlignX
import com.intellij.ui.dsl.builder.Cell
import com.intellij.ui.dsl.builder.RowLayout
import com.intellij.ui.dsl.builder.bindSelected
import com.intellij.ui.dsl.builder.bindText
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.builder.selected
import com.intellij.ui.layout.ValidationInfoBuilder
import com.intellij.ui.layout.not
import java.net.URL
import java.nio.file.Path

Expand Down Expand Up @@ -60,22 +64,27 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
.bindText(state::binaryDirectory)
.comment(CoderGatewayBundle.message("gateway.connector.settings.binary-destination.comment"))
}.layout(RowLayout.PARENT_GRID)
row {
cell() // For alignment.
checkBox(CoderGatewayBundle.message("gateway.connector.settings.enable-binary-directory-fallback.title"))
.bindSelected(state::enableBinaryDirectoryFallback)
.comment(
CoderGatewayBundle.message("gateway.connector.settings.enable-binary-directory-fallback.comment"),
)
}.layout(RowLayout.PARENT_GRID)
row {
cell() // For alignment.
checkBox(CoderGatewayBundle.message("gateway.connector.settings.fallback-on-coder-for-signatures.title"))
.bindSelected(state::fallbackOnCoderForSignatures)
.comment(
CoderGatewayBundle.message("gateway.connector.settings.fallback-on-coder-for-signatures.comment"),
)
}.layout(RowLayout.PARENT_GRID)
group {
lateinit var signatureVerificationCheckBox: Cell<JBCheckBox>
row {
cell() // For alignment.
signatureVerificationCheckBox =
checkBox(CoderGatewayBundle.message("gateway.connector.settings.disable-signature-validation.title"))
.bindSelected(state::disableSignatureVerification)
.comment(
CoderGatewayBundle.message("gateway.connector.settings.disable-signature-validation.comment"),
)
}.layout(RowLayout.PARENT_GRID)
row {
cell() // For alignment.
checkBox(CoderGatewayBundle.message("gateway.connector.settings.fallback-on-coder-for-signatures.title"))
.bindSelected(state::fallbackOnCoderForSignatures)
.comment(
CoderGatewayBundle.message("gateway.connector.settings.fallback-on-coder-for-signatures.comment"),
)
}.visibleIf(signatureVerificationCheckBox.selected.not())
.layout(RowLayout.PARENT_GRID)
}
row(CoderGatewayBundle.message("gateway.connector.settings.header-command.title")) {
textField().resizableColumn().align(AlignX.FILL)
.bindText(state::headerCommand)
Expand Down Expand Up @@ -122,7 +131,10 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
textArea().resizableColumn().align(AlignX.FILL)
.bindText(state::sshConfigOptions)
.comment(
CoderGatewayBundle.message("gateway.connector.settings.ssh-config-options.comment", CODER_SSH_CONFIG_OPTIONS),
CoderGatewayBundle.message(
"gateway.connector.settings.ssh-config-options.comment",
CODER_SSH_CONFIG_OPTIONS
),
)
}.layout(RowLayout.PARENT_GRID)
row(CoderGatewayBundle.message("gateway.connector.settings.setup-command.title")) {
Expand Down Expand Up @@ -162,7 +174,7 @@ class CoderSettingsConfigurable : BoundConfigurable("Coder") {
.bindText(state::defaultIde)
.comment(
"The default IDE version to display in the IDE selection dropdown. " +
"Example format: CL 2023.3.6 233.15619.8",
"Example format: CL 2023.3.6 233.15619.8",
)
}
row(CoderGatewayBundle.message("gateway.connector.settings.check-ide-updates.heading")) {
Expand Down
5 changes: 5 additions & 0 deletions src/main/kotlin/com/coder/gateway/cli/CoderCLIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ class CoderCLIManager(
else -> result as DownloadResult.Downloaded
}
}
if (settings.disableSignatureVerification) {
downloader.commit()
logger.info("Skipping over CLI signature verification, it is disabled by the user")
return true
}

var signatureResult = withContext(Dispatchers.IO) {
downloader.downloadSignature(showTextProgress)
Expand Down
13 changes: 12 additions & 1 deletion src/main/kotlin/com/coder/gateway/settings/CoderSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ open class CoderSettingsState(
open var enableBinaryDirectoryFallback: Boolean = false,

/**
* Controls whether we fall back release.coder.com
* Controls whether we verify the cli signature
*/
open var disableSignatureVerification: Boolean = false,

/**
* Controls whether we fall back release.coder.com if signature validation is enabled
*/
open var fallbackOnCoderForSignatures: Boolean = false,

Expand Down Expand Up @@ -160,6 +165,12 @@ open class CoderSettings(
val enableBinaryDirectoryFallback: Boolean
get() = state.enableBinaryDirectoryFallback

/**
* Controls whether we verify the cli signature
*/
val disableSignatureVerification: Boolean
get() = state.disableSignatureVerification

/**
* Controls whether we fall back release.coder.com
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ class CoderWorkspacesStepView :
CoderGatewayBundle.message("gateway.connector.settings.fallback-on-coder-for-signatures.comment"),
)

}.layout(RowLayout.PARENT_GRID)
}.visible(state.disableSignatureVerification.not()).layout(RowLayout.PARENT_GRID)
row {
scrollCell(
toolbar.createPanel().apply {
Expand Down
4 changes: 2 additions & 2 deletions src/main/resources/messages/CoderGatewayBundle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@ gateway.connector.settings.enable-binary-directory-fallback.title=Fall back to d
gateway.connector.settings.enable-binary-directory-fallback.comment=Checking this \
box will allow the plugin to fall back to the data directory when the CLI \
directory is not writable.

gateway.connector.settings.disable-signature-validation.title=Disable Coder CLI signature verification
gateway.connector.settings.disable-signature-validation.comment=Useful if you run an unsigned fork for the binary
gateway.connector.settings.fallback-on-coder-for-signatures.title=Fall back on releases.coder.com for signatures
gateway.connector.settings.fallback-on-coder-for-signatures.comment=Verify binary signature using releases.coder.com when CLI signatures are not available from the deployment

gateway.connector.settings.header-command.title=Header command
gateway.connector.settings.header-command.comment=An external command that \
outputs additional HTTP headers added to all requests. The command must \
Expand Down
Loading