Skip to content

Commit 50da220

Browse files
committed
Merge branch 'main' into impl-lenient-http-client
2 parents 4f1c279 + 0ad31dd commit 50da220

File tree

14 files changed

+260
-16
lines changed

14 files changed

+260
-16
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,13 @@
22

33
## Unreleased
44

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

11+
- URL validation is stricter in the connection screen and URI protocol handler
712
- the http client has relaxed syntax rules when deserializing JSON responses
813

914
## 0.6.0 - 2025-07-25

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/util/CoderProtocolHandler.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import com.coder.toolbox.sdk.CoderRestClient
99
import com.coder.toolbox.sdk.v2.models.Workspace
1010
import com.coder.toolbox.sdk.v2.models.WorkspaceAgent
1111
import com.coder.toolbox.sdk.v2.models.WorkspaceStatus
12+
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
1213
import com.jetbrains.toolbox.api.remoteDev.connection.RemoteToolsHelper
1314
import kotlinx.coroutines.Job
1415
import kotlinx.coroutines.TimeoutCancellationException
@@ -107,6 +108,11 @@ open class CoderProtocolHandler(
107108
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "Query parameter \"$URL\" is missing from URI")
108109
return null
109110
}
111+
val validationResult = deploymentURL.validateStrictWebUrl()
112+
if (validationResult is Invalid) {
113+
context.logAndShowError(CAN_T_HANDLE_URI_TITLE, "\"$URL\" is invalid: ${validationResult.reason}")
114+
return null
115+
}
110116
return deploymentURL
111117
}
112118

src/main/kotlin/com/coder/toolbox/util/URLExtensions.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,44 @@
11
package com.coder.toolbox.util
22

3+
import com.coder.toolbox.util.WebUrlValidationResult.Invalid
4+
import com.coder.toolbox.util.WebUrlValidationResult.Valid
35
import java.net.IDN
46
import java.net.URI
57
import java.net.URL
68

79
fun String.toURL(): URL = URI.create(this).toURL()
810

11+
fun String.validateStrictWebUrl(): WebUrlValidationResult = try {
12+
val uri = URI(this)
13+
14+
when {
15+
uri.isOpaque -> Invalid(
16+
"The URL \"$this\" is invalid because it is not in the standard format. " +
17+
"Please enter a full web address like \"https://example.com\""
18+
)
19+
20+
!uri.isAbsolute -> Invalid(
21+
"The URL \"$this\" is missing a scheme (like https://). " +
22+
"Please enter a full web address like \"https://example.com\""
23+
)
24+
uri.scheme?.lowercase() !in setOf("http", "https") ->
25+
Invalid(
26+
"The URL \"$this\" must start with http:// or https://, not \"${uri.scheme}\""
27+
)
28+
uri.authority.isNullOrBlank() ->
29+
Invalid(
30+
"The URL \"$this\" does not include a valid website name. " +
31+
"Please enter a full web address like \"https://example.com\""
32+
)
33+
else -> Valid
34+
}
35+
} catch (_: Exception) {
36+
Invalid(
37+
"The input \"$this\" is not a valid web address. " +
38+
"Please enter a full web address like \"https://example.com\""
39+
)
40+
}
41+
942
fun URL.withPath(path: String): URL = URL(
1043
this.protocol,
1144
this.host,
@@ -30,3 +63,8 @@ fun URI.toQueryParameters(): Map<String, String> = (this.query ?: "")
3063
parts[0] to ""
3164
}
3265
}
66+
67+
sealed class WebUrlValidationResult {
68+
object Valid : WebUrlValidationResult()
69+
data class Invalid(val reason: String) : WebUrlValidationResult()
70+
}

0 commit comments

Comments
 (0)