diff --git a/CHANGELOG.md b/CHANGELOG.md index b79d7d7..640bb7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- support for skipping CLI signature verification + ### Changed - URL validation is stricter in the connection screen and URI protocol handler diff --git a/JETBRAINS_COMPLIANCE.md b/JETBRAINS_COMPLIANCE.md index 306d684..91162ed 100644 --- a/JETBRAINS_COMPLIANCE.md +++ b/JETBRAINS_COMPLIANCE.md @@ -39,8 +39,6 @@ This configuration includes JetBrains-specific rules that check for: - **ForbiddenImport**: Detects potentially bundled libraries - **Standard code quality rules**: Complexity, naming, performance, etc. - - ## CI/CD Integration 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 open build/reports/detekt/detekt.html ``` - - ## Understanding Results ### Compliance Check Results diff --git a/README.md b/README.md index 41d430d..0c671ce 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,69 @@ If `ide_product_code` and `ide_build_number` is missing, Toolbox will only open page. Coder Toolbox will attempt to start the workspace if it’s not already running; however, for the most reliable experience, it’s recommended to ensure the workspace is running prior to initiating the connection. +## GPG Signature Verification + +The Coder Toolbox plugin starting with version *0.5.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 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. + ## Configuring and Testing workspace polling with HTTP & SOCKS5 Proxy This section explains how to set up a local proxy and verify that diff --git a/gradle.properties b/gradle.properties index 0becc24..b31ebe6 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ -version=0.6.0 +version=0.6.1 group=com.coder.toolbox name=coder-toolbox \ No newline at end of file diff --git a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt index 8afd954..582a85b 100644 --- a/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt +++ b/src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt @@ -181,6 +181,12 @@ class CoderCLIManager( } } + if (context.settingsStore.disableSignatureVerification) { + downloader.commit() + context.logger.info("Skipping over CLI signature verification, it is disabled by the user") + return true + } + var signatureResult = withContext(Dispatchers.IO) { downloader.downloadSignature(showTextProgress) } diff --git a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt index 693c1fd..0775a63 100644 --- a/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt +++ b/src/main/kotlin/com/coder/toolbox/settings/ReadOnlyCoderSettings.kt @@ -29,7 +29,12 @@ interface ReadOnlyCoderSettings { val binaryDirectory: String? /** - * Controls whether we fall back release.coder.com + * Controls whether we verify the cli signature + */ + val disableSignatureVerification: Boolean + + /** + * Controls whether we fall back on release.coder.com for signatures if signature validation is enabled */ val fallbackOnCoderForSignatures: SignatureFallbackStrategy diff --git a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt index 0fa4914..82b6e80 100644 --- a/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt +++ b/src/main/kotlin/com/coder/toolbox/store/CoderSettingsStore.kt @@ -38,6 +38,8 @@ class CoderSettingsStore( override val defaultURL: String get() = store[DEFAULT_URL] ?: "https://dev.coder.com" override val binarySource: String? get() = store[BINARY_SOURCE] override val binaryDirectory: String? get() = store[BINARY_DIRECTORY] + override val disableSignatureVerification: Boolean + get() = store[DISABLE_SIGNATURE_VALIDATION]?.toBooleanStrictOrNull() ?: false override val fallbackOnCoderForSignatures: SignatureFallbackStrategy get() = SignatureFallbackStrategy.fromValue(store[FALLBACK_ON_CODER_FOR_SIGNATURES]) override val defaultCliBinaryNameByOsAndArch: String get() = getCoderCLIForOS(getOS(), getArch()) @@ -166,6 +168,10 @@ class CoderSettingsStore( store[ENABLE_DOWNLOADS] = shouldEnableDownloads.toString() } + fun updateDisableSignatureVerification(shouldDisableSignatureVerification: Boolean) { + store[DISABLE_SIGNATURE_VALIDATION] = shouldDisableSignatureVerification.toString() + } + fun updateSignatureFallbackStrategy(fallback: Boolean) { store[FALLBACK_ON_CODER_FOR_SIGNATURES] = when (fallback) { true -> SignatureFallbackStrategy.ALLOW.toString() diff --git a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt index cd1a05d..1626ce1 100644 --- a/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt +++ b/src/main/kotlin/com/coder/toolbox/store/StoreKeys.kt @@ -10,6 +10,8 @@ internal const val BINARY_SOURCE = "binarySource" internal const val BINARY_DIRECTORY = "binaryDirectory" +internal const val DISABLE_SIGNATURE_VALIDATION = "disableSignatureValidation" + internal const val FALLBACK_ON_CODER_FOR_SIGNATURES = "signatureFallbackStrategy" internal const val BINARY_NAME = "binaryName" diff --git a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt index 448a20f..d27a1c0 100644 --- a/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt +++ b/src/main/kotlin/com/coder/toolbox/views/CoderSettingsPage.kt @@ -6,6 +6,7 @@ import com.jetbrains.toolbox.api.ui.components.CheckboxField import com.jetbrains.toolbox.api.ui.components.TextField import com.jetbrains.toolbox.api.ui.components.TextType import com.jetbrains.toolbox.api.ui.components.UiField +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ClosedSendChannelException import kotlinx.coroutines.flow.MutableStateFlow @@ -20,7 +21,7 @@ import kotlinx.coroutines.launch * TODO@JB: There is no scroll, and our settings do not fit. As a consequence, * I have not been able to test this page. */ -class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel) : +class CoderSettingsPage(private val context: CoderToolboxContext, triggerSshConfig: Channel) : CoderPage(MutableStateFlow(context.i18n.ptrl("Coder Settings")), false) { private val settings = context.settingsStore.readOnly() @@ -33,6 +34,11 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel< TextField(context.i18n.ptrl("Data directory"), settings.dataDirectory ?: "", TextType.General) private val enableDownloadsField = CheckboxField(settings.enableDownloads, context.i18n.ptrl("Enable downloads")) + + private val disableSignatureVerificationField = CheckboxField( + settings.disableSignatureVerification, + context.i18n.ptrl("Disable Coder CLI signature verification") + ) private val signatureFallbackStrategyField = CheckboxField( settings.fallbackOnCoderForSignatures.isAllowed(), @@ -65,13 +71,14 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel< private val networkInfoDirField = TextField(context.i18n.ptrl("SSH network metrics directory"), settings.networkInfoDir, TextType.General) - + private lateinit var visibilityUpdateJob: Job override val fields: StateFlow> = MutableStateFlow( listOf( binarySourceField, enableDownloadsField, binaryDirectoryField, enableBinaryDirectoryFallbackField, + disableSignatureVerificationField, signatureFallbackStrategyField, dataDirectoryField, headerCommandField, @@ -94,6 +101,7 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel< context.settingsStore.updateBinaryDirectory(binaryDirectoryField.contentState.value) context.settingsStore.updateDataDirectory(dataDirectoryField.contentState.value) context.settingsStore.updateEnableDownloads(enableDownloadsField.checkedState.value) + context.settingsStore.updateDisableSignatureVerification(disableSignatureVerificationField.checkedState.value) context.settingsStore.updateSignatureFallbackStrategy(signatureFallbackStrategyField.checkedState.value) context.settingsStore.updateBinaryDirectoryFallback(enableBinaryDirectoryFallbackField.checkedState.value) context.settingsStore.updateHeaderCommand(headerCommandField.contentState.value) @@ -182,5 +190,19 @@ class CoderSettingsPage(context: CoderToolboxContext, triggerSshConfig: Channel< networkInfoDirField.contentState.update { settings.networkInfoDir } + + visibilityUpdateJob = context.cs.launch { + disableSignatureVerificationField.checkedState.collect { state -> + signatureFallbackStrategyField.visibility.update { + // the fallback checkbox should not be visible + // if signature verification is disabled + !state + } + } + } + } + + override fun afterHide() { + visibilityUpdateJob.cancel() } } diff --git a/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt b/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt index 0608347..34b027c 100644 --- a/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt +++ b/src/main/kotlin/com/coder/toolbox/views/DeploymentUrlStep.kt @@ -1,7 +1,6 @@ package com.coder.toolbox.views import com.coder.toolbox.CoderToolboxContext -import com.coder.toolbox.settings.SignatureFallbackStrategy import com.coder.toolbox.util.WebUrlValidationResult.Invalid import com.coder.toolbox.util.toURL import com.coder.toolbox.util.validateStrictWebUrl @@ -41,7 +40,7 @@ class DeploymentUrlStep( override val panel: RowGroup get() { - if (context.settingsStore.fallbackOnCoderForSignatures == SignatureFallbackStrategy.NOT_CONFIGURED) { + if (!context.settingsStore.disableSignatureVerification) { return RowGroup( RowGroup.RowField(urlField), RowGroup.RowField(emptyLine), diff --git a/src/main/resources/localization/defaultMessages.po b/src/main/resources/localization/defaultMessages.po index f176105..30c4484 100644 --- a/src/main/resources/localization/defaultMessages.po +++ b/src/main/resources/localization/defaultMessages.po @@ -164,4 +164,7 @@ msgid "Abort" msgstr "" msgid "Run anyway" +msgstr "" + +msgid "Disable Coder CLI signature verification" msgstr "" \ No newline at end of file