Files
Adam Shiervani 11b99d59fd Signed Releases (#1188)
* Implement GPG signature verification for OTA updates

- Added GPG signature verification to the OTA update process, ensuring that updates requiring signatures cannot be applied without them.
- Introduced a new GPGVerifier struct to handle fetching and verifying signatures.
- Updated the updateApp and updateSystem methods to check for signature URLs and download signatures as needed.
- Enhanced error handling for missing signatures and verification failures.
- Removed the old release.sh script as its functionality has been integrated into the Makefile for better release management.

* Add tests for GPG signature verification in OTA updates

* Refactor error message for missing GPG signature URL in OTA updates

* Refactor OTA update process to improve signature handling

- Introduced a new method for downloading component signatures, ensuring that updates requiring signatures cannot proceed without them.
- Updated the Makefile to allow E2E tests to optionally include OTA tests based on the SKIP_OTA_E2E variable.
- Enhanced the test_local_update.sh script to support signature file verification and inclusion during tests.
- Improved error handling for missing signature URLs and added context cancellation checks in GPG key fetching.

* Refactor GPG key caching to validate keyring before storing

* Update Makefile to enhance E2E test process with optional OTA signature verification

* Comment out non-working keyservers and update root key fingerprint

* Add Ubunutu keyserver

* Update root key fingerprint for GPG signature verification in OTA updates

* Add signed OTA E2E test and full E2E test suite to Makefile

- Introduced `test_e2e_signed` target for testing signed OTA updates with GPG signature verification.
- Added `test_e2e_full` target to run both regular and signed OTA tests, requiring a signing key fingerprint.
- Enhanced error handling for missing parameters in both test targets.

* Update IP address extraction in test_local_update.sh to exclude all localhost addresses

* Add GPG public key fetching tests with caching and error handling

* Enhance build and testing scripts for signed OTA updates

* Add fingerprint extraction and validation for GPG keys

* Simplify bypass mechanism of OTA signature checks

* Refactor E2E testing and release workflows

* Enhance OTA testing framework and scripts

* Improve local network IP detection in OTA helpers by implementing route-based detection as a primary method, falling back to interface scanning if necessary.

* Add support for unsigned OTA version testing

- Introduced a new script to test unsigned OTA updates with specific version checks.
- Updated Makefile to include the new test script for unsigned OTA.
- Enhanced existing E2E tests to validate version differences and ensure proper OTA behavior.
- Improved error handling for required environment variables in the testing framework.

* Update Makefile to include core E2E tests and enhance dev release validation

- Added `test_core_e2e.sh` script execution to both production and development release workflows.
- Improved user confirmation prompt before proceeding with the dev release.
- Added completion messages to indicate successful test execution and readiness for release.

* Enhance Makefile and testing scripts for improved OTA validation

- Added a new script execution for testing unsigned OTA updates in the Makefile.
- Updated E2E test configurations to exclude specific OTA tests and improve retry logic for video stream dimension retrieval.
- Refactored mouse round-trip tests to remove unnecessary settle time parameters.

* Final release confirmation of prod releases

* Cleanup OTA code: eliminate redundant parsing, TOCTOU, and duplication

- Remove double parseAndValidateKeyring call by threading validated
  keyring through fetchFromSingleKeyserver → fetchFromKeyservers →
  updateMemoryCache
- Extract getKeyring() helper to deduplicate VerifySignature and
  VerifySignatureFromFile preamble
- Replace os.Stat+os.Remove TOCTOU pattern with direct os.Remove
  ignoring os.ErrNotExist in downloadFile
- Remove unnecessary fs.existsSync in mock server handler; check
  signaturePath variable directly

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* E2E: fix flaky tests, add unsigned OTA to dev test lane

- Fix mouse roundtrip flakiness by increasing MOUSE_SETTLE_MS (50→150ms)
- Export sshExec from helpers for ota-helpers.ts
- Reduce overly conservative delays (polling, animations, reconnects)
- Add waitForVideoDimensions helper with proper polling
- Improve ensureLocalAuthMode to try known passwords before SSH reset
- Add unsigned specific-version OTA test to `make test_e2e` target
- Build baseline + dev binary with pinned VERSION_DEV to avoid timestamp drift

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Remove stale dev_release checklist item from PR templates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 18:26:23 +01:00

110 lines
3.1 KiB
Go

package ota
import (
"bytes"
"context"
"os/exec"
"time"
)
const (
systemUpdatePath = "/userdata/jetkvm/update_system.tar"
)
// DO NOT call it directly, it's not thread safe
// Mutex is currently held by the caller, e.g. doUpdate
func (s *State) updateSystem(ctx context.Context, systemUpdate *componentUpdateStatus, bypassSignatureCheck bool) error {
l := s.l.With().Str("path", systemUpdatePath).Logger()
// Validate signature requirement and download if available
signature, err := s.downloadComponentSignature(ctx, systemUpdate, "system", &l, bypassSignatureCheck)
if err != nil {
return s.componentUpdateError("Error with system signature", err, &l)
}
if err := s.downloadFile(ctx, systemUpdatePath, systemUpdate.url, "system"); err != nil {
return s.componentUpdateError("Error downloading system update", err, &l)
}
downloadFinished := time.Now()
systemUpdate.downloadFinishedAt = downloadFinished
systemUpdate.downloadProgress = 1
s.triggerComponentUpdateState("system", systemUpdate)
if err := s.verifyFile(
ctx,
systemUpdatePath,
systemUpdate.hash,
signature,
&systemUpdate.verificationProgress,
); err != nil {
return s.componentUpdateError("Error verifying system update", err, &l)
}
verifyFinished := time.Now()
systemUpdate.verifiedAt = verifyFinished
systemUpdate.verificationProgress = 1
systemUpdate.updatedAt = verifyFinished
systemUpdate.updateProgress = 1
s.triggerComponentUpdateState("system", systemUpdate)
l.Info().Msg("System update downloaded")
l.Info().Msg("Starting rk_ota command")
cmd := exec.Command("rk_ota", "--misc=update", "--tar_path=/userdata/jetkvm/update_system.tar", "--save_dir=/userdata/jetkvm/ota_save", "--partition=all")
var b bytes.Buffer
cmd.Stdout = &b
cmd.Stderr = &b
if err := cmd.Start(); err != nil {
return s.componentUpdateError("Error starting rk_ota command", err, &l)
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
ticker := time.NewTicker(1800 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ticker.C:
if systemUpdate.updateProgress >= 0.99 {
return
}
systemUpdate.updateProgress += 0.01
if systemUpdate.updateProgress > 0.99 {
systemUpdate.updateProgress = 0.99
}
s.triggerComponentUpdateState("system", systemUpdate)
case <-ctx.Done():
return
}
}
}()
err = cmd.Wait()
cancel()
rkLogger := s.l.With().
Str("output", b.String()).
Int("exitCode", cmd.ProcessState.ExitCode()).Logger()
if err != nil {
return s.componentUpdateError("Error executing rk_ota command", err, &rkLogger)
}
rkLogger.Info().Msg("rk_ota success")
s.rebootNeeded = true
systemUpdate.updateProgress = 1
systemUpdate.updatedAt = verifyFinished
s.triggerComponentUpdateState("system", systemUpdate)
return nil
}
func (s *State) confirmCurrentSystem() {
output, err := exec.Command("rk_ota", "--misc=now").CombinedOutput()
if err != nil {
s.l.Warn().Str("output", string(output)).Msg("failed to set current partition in A/B setup")
}
s.l.Trace().Str("output", string(output)).Msg("current partition in A/B setup set")
}