Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
48 changes: 48 additions & 0 deletions crates/utils/lib/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,27 @@ pub fn bundle_download_url(version: &str, arch: &str, os: &str) -> String {
)
}

/// Returns the GitHub release download URL for the `checksums.sha256` asset
/// listing the SHA-256 digest of every asset in the release.
pub fn checksums_download_url(version: &str) -> String {
format!(
"https://github.com/{GITHUB_ORG}/{MICROSANDBOX_REPO}/releases/download/v{version}/checksums.sha256"
)
}

/// Extracts the digest published for `filename` from `sha256sum`-formatted
/// checksums, as released in the `checksums.sha256` asset. Returns `None`
/// when no entry matches.
pub fn bundle_digest_from_checksums(checksums: &str, filename: &str) -> Option<String> {
checksums.lines().find_map(|line| {
let mut fields = line.split_whitespace();
let digest = fields.next()?;
// sha256sum marks binary-mode entries with a leading `*`.
let name = fields.next()?.trim_start_matches('*');
(name == filename).then(|| digest.to_owned())
})
}

/// Returns an HTTP client configured for release asset downloads.
#[cfg(feature = "http-client")]
pub fn http_client() -> ureq::Agent {
Expand Down Expand Up @@ -287,6 +308,33 @@ pub fn is_windows_drive_separator_at(s: &str, index: usize) -> bool {
mod tests {
use super::*;

#[test]
fn bundle_digest_is_selected_from_sha256sum_checksums() {
let checksums = format!(
"{} agentd-aarch64\n\
{} microsandbox-darwin-aarch64.tar.gz\n\
{} *microsandbox-linux-x86_64.tar.gz\n\
malformed line\n",
"a".repeat(64),
"b".repeat(64),
"c".repeat(64),
);

assert_eq!(
bundle_digest_from_checksums(&checksums, "microsandbox-darwin-aarch64.tar.gz"),
Some("b".repeat(64)),
);
// Binary-mode `*` markers are stripped before matching.
assert_eq!(
bundle_digest_from_checksums(&checksums, "microsandbox-linux-x86_64.tar.gz"),
Some("c".repeat(64)),
);
assert_eq!(
bundle_digest_from_checksums(&checksums, "microsandbox-windows-x86_64.tar.gz"),
None,
);
}

/// `MSB_HOME` is honoured verbatim (no `.microsandbox` suffix appended)
/// so callers can isolate state per process without disturbing tooling
/// that reads `$HOME` (npm cache, ssh keys, etc.).
Expand Down
140 changes: 104 additions & 36 deletions sdk/go/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package microsandbox

import (
"archive/tar"
"bytes"
"compress/gzip"
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
Expand Down Expand Up @@ -109,7 +112,9 @@ var (

// EnsureInstalled ensures the msb + libkrunfw runtime is present at
// ~/.microsandbox/ and downloads it from the matching GitHub release
// if not. It is OPTIONAL: the SDK's FFI library is embedded in the
// if not, verifying the bundle against the SHA-256 digest published in
// the release's checksums.sha256 asset before extracting anything.
// It is OPTIONAL: the SDK's FFI library is embedded in the
// Go binary and loads automatically on first use, so EnsureInstalled
// only governs the optional msb runtime download.
//
Expand Down Expand Up @@ -212,7 +217,7 @@ func materializeFFI(dir string) (string, error) {
return "", fmt.Errorf("create %s: %w", libDir, err)
}
dest := filepath.Join(libDir, bundle.Filename())
if existing, err := os.ReadFile(dest); err == nil && bytesEqual(existing, ffiBytes) {
if existing, err := os.ReadFile(dest); err == nil && bytes.Equal(existing, ffiBytes) {
return dest, nil
}
if err := writeFile(dest, ffiBytes, 0o755); err != nil {
Expand Down Expand Up @@ -349,8 +354,14 @@ func osStringFor(goos string) (string, error) {
}
}

// bundleURL is the GitHub release asset URL for the current OS/arch.
func bundleURL() (string, error) {
// releaseDownloadBase is the GitHub release download URL prefix. A var so
// tests can point downloads at a local server.
var releaseDownloadBase = fmt.Sprintf(
"https://github.com/%s/%s/releases/download", githubOrg, githubRepo)

// bundleFilename is the release asset name of the msb + libkrunfw bundle
// for the current OS/arch.
func bundleFilename() (string, error) {
arch, err := archString()
if err != nil {
return "", err
Expand All @@ -359,15 +370,30 @@ func bundleURL() (string, error) {
if err != nil {
return "", err
}
return fmt.Sprintf(
"https://github.com/%s/%s/releases/download/v%s/%s-%s-%s.tar.gz",
githubOrg, githubRepo, sdkVersion, githubRepo, osName, arch,
), nil
return fmt.Sprintf("%s-%s-%s.tar.gz", githubRepo, osName, arch), nil
}

// bundleURL is the GitHub release asset URL for the current OS/arch.
func bundleURL() (string, error) {
name, err := bundleFilename()
if err != nil {
return "", err
}
return fmt.Sprintf("%s/v%s/%s", releaseDownloadBase, sdkVersion, name), nil
}

// checksumsURL is the GitHub release asset URL for the checksums.sha256 file
// listing the SHA-256 digest of every asset in the release.
func checksumsURL() string {
return fmt.Sprintf("%s/v%s/checksums.sha256", releaseDownloadBase, sdkVersion)
}

// downloadMsbAndKrunfw fetches the release bundle and extracts msb +
// libkrunfw into <installDir>/{bin,lib}/. The FFI library inside the
// tarball is ignored (the SDK ships it embedded).
// downloadMsbAndKrunfw fetches the release bundle, verifies it against the
// SHA-256 digest published in the release's checksums.sha256 asset, and
// extracts msb + libkrunfw into <installDir>/{bin,lib}/. Verification is
// fail-closed: if the digest cannot be fetched or does not match, nothing
// is extracted. The FFI library inside the tarball is ignored (the SDK
// ships it embedded).
func downloadMsbAndKrunfw(ctx context.Context, installDir string) error {
binDir := filepath.Join(installDir, "bin")
libDir := filepath.Join(installDir, "lib")
Expand All @@ -378,31 +404,34 @@ func downloadMsbAndKrunfw(ctx context.Context, installDir string) error {
return err
}

filename, err := bundleFilename()
if err != nil {
return err
}
url, err := bundleURL()
if err != nil {
return err
}

reqCtx, cancel := context.WithTimeout(ctx, httpTimeout)
defer cancel()

req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil)
sums, err := httpGet(ctx, checksumsURL())
if err != nil {
return fmt.Errorf("fetch release checksums: %w", err)
}
want, err := bundleDigestFromChecksums(string(sums), filename)
if err != nil {
return err
}

client := &http.Client{Timeout: httpTimeout}
resp, err := client.Do(req)
data, err := httpGet(ctx, url)
if err != nil {
return fmt.Errorf("GET %s: %w", url, err)
return err
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return fmt.Errorf("GET %s: HTTP %d", url, resp.StatusCode)
sum := sha256.Sum256(data)
if got := hex.EncodeToString(sum[:]); got != want {
return fmt.Errorf("%s SHA-256 mismatch: expected %s, got %s", filename, want, got)
}

if err := extractMsbAndKrunfw(resp.Body, binDir, libDir); err != nil {
if err := extractMsbAndKrunfw(bytes.NewReader(data), binDir, libDir); err != nil {
return err
}

Expand All @@ -427,6 +456,58 @@ func downloadMsbAndKrunfw(ctx context.Context, installDir string) error {
return nil
}

// httpGet fetches url and returns the full response body.
func httpGet(ctx context.Context, url string) ([]byte, error) {
reqCtx, cancel := context.WithTimeout(ctx, httpTimeout)
defer cancel()

req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}

client := &http.Client{Timeout: httpTimeout}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("GET %s: %w", url, err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GET %s: HTTP %d", url, resp.StatusCode)
}

body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("GET %s: %w", url, err)
}
return body, nil
}

// bundleDigestFromChecksums extracts the digest for filename from the
// sha256sum-formatted checksums published with each release.
func bundleDigestFromChecksums(checksums, filename string) (string, error) {
for _, line := range strings.Split(checksums, "\n") {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
// sha256sum marks binary-mode entries with a leading "*".
if strings.TrimPrefix(fields[1], "*") != filename {
continue
}
digest := strings.ToLower(fields[0])
if len(digest) != 64 {
return "", fmt.Errorf("checksums.sha256 publishes an invalid SHA-256 for %s: %s", filename, fields[0])
}
if _, err := hex.DecodeString(digest); err != nil {
return "", fmt.Errorf("checksums.sha256 publishes an invalid SHA-256 for %s: %s", filename, fields[0])
}
return digest, nil
}
return "", fmt.Errorf("checksums.sha256 has no entry for %s", filename)
}

// extractMsbAndKrunfw streams a tar.gz from r and copies msb + libkrunfw*
// into the appropriate dirs. Any libmicrosandbox_go_ffi entries are
// skipped — the SDK ships its FFI library embedded.
Expand Down Expand Up @@ -510,16 +591,3 @@ func writeFile(dest string, data []byte, mode os.FileMode) error {
}
return nil
}

// bytesEqual is a tiny byte-slice equality without an `bytes` import.
func bytesEqual(a, b []byte) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
Loading
Loading