-
Notifications
You must be signed in to change notification settings - Fork 1
impl: verify cli signature #148
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
Changes from 22 commits
Commits
Show all changes
43 commits
Select commit
Hold shift + click to select a range
4227ebd
impl: new UI setting for running unsigned binary execution
fioan89 021e53a
chore: refactor CLI downloading logic
fioan89 fb6e784
impl: support for downloading the cli signature
fioan89 dbf8560
impl: support for downloading the releases.coder.com signature
fioan89 6754300
fix: read fresh values from the config store
fioan89 8d768ee
impl: prompt user when running unsigned binaries
fioan89 ea3e379
fix: used proper result to verify if signature is downloaded
fioan89 3668d46
chore: compact code and run signature download on the IO thread
fioan89 a476364
chore: add support for bouncycastle
fioan89 45a72fb
chore: update i18n bundle with new strings related to signature verif…
fioan89 ad44346
impl: verify gpg signed cli binaries
fioan89 4cd5148
impl: embed the pgp public key as a plugin resource
fioan89 fbe68de
impl: load the public key from a resource file
fioan89 270b949
impl: run the signature verification on the IO thread
fioan89 d5ae289
fix: find the key id in multiple key rings
fioan89 96663e6
fix: remove the cli if it is not properly signed
fioan89 6a79995
fix: avoid out of memory when verifying signatures
fioan89 5fcb4b9
fix: don't run signature verification
fioan89 3543377
chore: fix UTs
fioan89 0a5de76
Merge branch 'main' into impl-verify-cli-signature
fioan89 97dbc8d
chore: next version is 0.5.0
fioan89 27066d8
fix: more UTs
fioan89 811fc85
fix: display errors that happened while handling URIs
fioan89 9851dec
impl: check if the cli exists before running it to spill out the version
fioan89 306848f
impl: download retroactive cli signatures from releases.coder.com/cod…
fioan89 5dcdff0
fix: UTs after fallback to signatures from releases.coder.com were pu…
fioan89 5bf0792
chore: refactor code around signature name
fioan89 bce103b
chore: remove code around URL building
fioan89 8342a21
fix: raise the original error when cli can't be downloaded
fioan89 881a662
impl: download the cli to a temporary location
fioan89 aeb79e5
impl: prompt the user if when signature verification fails
fioan89 f57c07d
impl: introduce signature fallback setting
fioan89 dcba5ec
impl: ask the user only once in the login screen for fallback strategy
fioan89 a8767d2
fix: the settings page doesn't see changes done from other screens
fioan89 0ad2121
impl: prompt user for allowing unverified binaries to run
fioan89 eba8118
chore: always run unsigned binaries in the UTs
fioan89 26a94c1
Merge branch 'main' into impl-verify-cli-signature
fioan89 3856d57
fix: fallback to releases.coder.com was not properly treated
fioan89 65eb1ec
fix: report cli download progress with the real name
fioan89 736325e
fix: don't report version for signatures while downloading
fioan89 5ea0967
impl: improve progress reporting while downloading the cli
fioan89 6529069
chore: improve fallback setting text
fioan89 53d5b32
fix: don't send custom request headers when accessing release.coder.com
fioan89 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
version=0.4.0 | ||
version=0.5.0 | ||
group=com.coder.toolbox | ||
name=coder-toolbox |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
273 changes: 141 additions & 132 deletions
273
src/main/kotlin/com/coder/toolbox/cli/CoderCLIManager.kt
Large diffs are not rendered by default.
Oops, something went wrong.
29 changes: 29 additions & 0 deletions
29
src/main/kotlin/com/coder/toolbox/cli/downloader/CoderDownloadApi.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
package com.coder.toolbox.cli.downloader | ||
|
||
import okhttp3.ResponseBody | ||
import retrofit2.Response | ||
import retrofit2.http.GET | ||
import retrofit2.http.Header | ||
import retrofit2.http.HeaderMap | ||
import retrofit2.http.Streaming | ||
import retrofit2.http.Url | ||
|
||
/** | ||
* Retrofit API for downloading CLI | ||
*/ | ||
interface CoderDownloadApi { | ||
@GET | ||
@Streaming | ||
suspend fun downloadCli( | ||
@Url url: String, | ||
@Header("If-None-Match") eTag: String? = null, | ||
@HeaderMap headers: Map<String, String> = emptyMap(), | ||
@Header("Accept-Encoding") acceptEncoding: String = "gzip", | ||
): Response<ResponseBody> | ||
|
||
@GET | ||
suspend fun downloadSignature( | ||
@Url url: String, | ||
@HeaderMap headers: Map<String, String> = emptyMap() | ||
): Response<ResponseBody> | ||
} |
194 changes: 194 additions & 0 deletions
194
src/main/kotlin/com/coder/toolbox/cli/downloader/CoderDownloadService.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,194 @@ | ||
package com.coder.toolbox.cli.downloader | ||
|
||
import com.coder.toolbox.CoderToolboxContext | ||
import com.coder.toolbox.cli.ex.ResponseException | ||
import com.coder.toolbox.util.OS | ||
import com.coder.toolbox.util.getHeaders | ||
import com.coder.toolbox.util.getOS | ||
import com.coder.toolbox.util.sha1 | ||
import com.coder.toolbox.util.withLastSegment | ||
import okhttp3.ResponseBody | ||
import retrofit2.Response | ||
import java.io.FileInputStream | ||
import java.net.HttpURLConnection.HTTP_NOT_FOUND | ||
import java.net.HttpURLConnection.HTTP_NOT_MODIFIED | ||
import java.net.HttpURLConnection.HTTP_OK | ||
import java.net.URI | ||
import java.net.URL | ||
import java.nio.file.Files | ||
import java.nio.file.Path | ||
import java.nio.file.StandardOpenOption | ||
import java.util.zip.GZIPInputStream | ||
import kotlin.io.path.name | ||
import kotlin.io.path.notExists | ||
|
||
/** | ||
* Handles the download steps of Coder CLI | ||
*/ | ||
class CoderDownloadService( | ||
private val context: CoderToolboxContext, | ||
private val downloadApi: CoderDownloadApi, | ||
private val deploymentUrl: URL, | ||
forceDownloadToData: Boolean, | ||
) { | ||
val remoteBinaryURL: URL = context.settingsStore.binSource(deploymentUrl) | ||
val localBinaryPath: Path = context.settingsStore.binPath(deploymentUrl, forceDownloadToData) | ||
|
||
suspend fun downloadCli(buildVersion: String, showTextProgress: (String) -> Unit): DownloadResult { | ||
val eTag = calculateLocalETag() | ||
if (eTag != null) { | ||
context.logger.info("Found existing binary at $localBinaryPath; calculated hash as $eTag") | ||
} | ||
val response = downloadApi.downloadCli( | ||
url = remoteBinaryURL.toString(), | ||
eTag = eTag?.let { "\"$it\"" }, | ||
headers = getRequestHeaders() | ||
) | ||
|
||
return when (response.code()) { | ||
HTTP_OK -> { | ||
context.logger.info("Downloading binary to $localBinaryPath") | ||
response.saveToDisk(localBinaryPath, showTextProgress, buildVersion)?.makeExecutable() | ||
DownloadResult.Downloaded(remoteBinaryURL, localBinaryPath) | ||
} | ||
|
||
HTTP_NOT_MODIFIED -> { | ||
context.logger.info("Using cached binary at $localBinaryPath") | ||
showTextProgress("Using cached binary") | ||
DownloadResult.Skipped | ||
} | ||
|
||
else -> { | ||
throw ResponseException( | ||
"Unexpected response from $remoteBinaryURL", | ||
response.code() | ||
) | ||
} | ||
} | ||
} | ||
|
||
private fun calculateLocalETag(): String? { | ||
return try { | ||
if (localBinaryPath.notExists()) { | ||
return null | ||
} | ||
sha1(FileInputStream(localBinaryPath.toFile())) | ||
} catch (e: Exception) { | ||
context.logger.warn(e, "Unable to calculate hash for $localBinaryPath") | ||
null | ||
} | ||
} | ||
|
||
private fun getRequestHeaders(): Map<String, String> { | ||
return if (context.settingsStore.headerCommand.isNullOrBlank()) { | ||
emptyMap() | ||
} else { | ||
getHeaders(deploymentUrl, context.settingsStore.headerCommand) | ||
} | ||
} | ||
|
||
private fun Response<ResponseBody>.saveToDisk( | ||
localPath: Path, | ||
showTextProgress: (String) -> Unit, | ||
buildVersion: String? = null | ||
): Path? { | ||
val responseBody = this.body() ?: return null | ||
Files.deleteIfExists(localPath) | ||
Files.createDirectories(localPath.parent) | ||
|
||
val outputStream = Files.newOutputStream( | ||
localPath, | ||
StandardOpenOption.CREATE, | ||
StandardOpenOption.TRUNCATE_EXISTING | ||
) | ||
val contentEncoding = this.headers()["Content-Encoding"] | ||
val sourceStream = if (contentEncoding?.contains("gzip", ignoreCase = true) == true) { | ||
GZIPInputStream(responseBody.byteStream()) | ||
} else { | ||
responseBody.byteStream() | ||
} | ||
|
||
val buffer = ByteArray(DEFAULT_BUFFER_SIZE) | ||
var bytesRead: Int | ||
var totalRead = 0L | ||
// caching this because the settings store recomputes it every time | ||
val binaryName = localPath.name | ||
sourceStream.use { source -> | ||
outputStream.use { sink -> | ||
while (source.read(buffer).also { bytesRead = it } != -1) { | ||
sink.write(buffer, 0, bytesRead) | ||
totalRead += bytesRead | ||
showTextProgress( | ||
"$binaryName $buildVersion - ${totalRead.toHumanReadableSize()} downloaded" | ||
) | ||
} | ||
} | ||
} | ||
return localBinaryPath | ||
} | ||
|
||
|
||
private fun Path.makeExecutable() { | ||
if (getOS() != OS.WINDOWS) { | ||
context.logger.info("Making $this executable...") | ||
this.toFile().setExecutable(true) | ||
} | ||
} | ||
|
||
private fun Long.toHumanReadableSize(): String { | ||
if (this < 1024) return "$this B" | ||
|
||
val kb = this / 1024.0 | ||
if (kb < 1024) return String.format("%.1f KB", kb) | ||
|
||
val mb = kb / 1024.0 | ||
if (mb < 1024) return String.format("%.1f MB", mb) | ||
|
||
val gb = mb / 1024.0 | ||
return String.format("%.1f GB", gb) | ||
} | ||
|
||
suspend fun downloadSignature(showTextProgress: (String) -> Unit): DownloadResult { | ||
return downloadSignature(remoteBinaryURL, showTextProgress) | ||
} | ||
|
||
private suspend fun downloadSignature(url: URL, showTextProgress: (String) -> Unit): DownloadResult { | ||
val defaultCliNameWithoutExt = context.settingsStore.defaultCliBinaryNameByOsAndArch.split('.').first() | ||
val signatureName = "$defaultCliNameWithoutExt.asc" | ||
|
||
val signatureURL = url.withLastSegment(signatureName) | ||
val localSignaturePath = localBinaryPath.parent.resolve(signatureName) | ||
context.logger.info("Downloading signature from $signatureURL") | ||
|
||
val response = downloadApi.downloadSignature( | ||
url = signatureURL.toString(), | ||
headers = getRequestHeaders() | ||
fioan89 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
) | ||
|
||
return when (response.code()) { | ||
HTTP_OK -> { | ||
response.saveToDisk(localSignaturePath, showTextProgress) | ||
DownloadResult.Downloaded(signatureURL, localSignaturePath) | ||
} | ||
|
||
HTTP_NOT_FOUND -> { | ||
context.logger.warn("Signature file not found at $signatureURL") | ||
DownloadResult.NotFound | ||
} | ||
|
||
else -> { | ||
DownloadResult.Failed( | ||
ResponseException( | ||
"Failed to download signature from $signatureURL", | ||
response.code() | ||
) | ||
) | ||
} | ||
} | ||
|
||
} | ||
|
||
suspend fun downloadReleasesSignature(showTextProgress: (String) -> Unit): DownloadResult { | ||
fioan89 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return downloadSignature(URI.create("https://releases.coder.com/bin").toURL(), showTextProgress) | ||
} | ||
} |
23 changes: 23 additions & 0 deletions
23
src/main/kotlin/com/coder/toolbox/cli/downloader/DownloadResult.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package com.coder.toolbox.cli.downloader | ||
|
||
import java.net.URL | ||
import java.nio.file.Path | ||
|
||
|
||
/** | ||
* Result of a download operation | ||
*/ | ||
sealed class DownloadResult { | ||
object Skipped : DownloadResult() | ||
object NotFound : DownloadResult() | ||
data class Downloaded(val source: URL, val dst: Path) : DownloadResult() | ||
data class Failed(val error: Exception) : DownloadResult() | ||
|
||
fun isSkipped(): Boolean = this is Skipped | ||
|
||
fun isNotFoundOrFailed(): Boolean = this is NotFound || this is Failed | ||
|
||
fun isDownloaded(): Boolean = this is Downloaded | ||
|
||
fun isNotDownloaded(): Boolean = this !is Downloaded | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.