chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const maximumSimulatedFirmwareSize = 268435456
|
||||
|
||||
type simulatedDeviceCommand struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
}
|
||||
|
||||
type simulatedFirmwareManifest struct {
|
||||
ReleaseID string
|
||||
Version string
|
||||
Digest string
|
||||
Signature []byte
|
||||
SigningKeyID string
|
||||
DownloadPath string
|
||||
}
|
||||
|
||||
type firmwareExecutionOutcome string
|
||||
|
||||
const (
|
||||
firmwareCompleted firmwareExecutionOutcome = "completed"
|
||||
firmwareFailed firmwareExecutionOutcome = "failed"
|
||||
firmwareInterrupted firmwareExecutionOutcome = "interrupted"
|
||||
firmwareRebootRequired firmwareExecutionOutcome = "reboot_required"
|
||||
)
|
||||
|
||||
func (s *simulator) executeFirmwareCommand(ctx context.Context, command simulatedDeviceCommand) (firmwareExecutionOutcome, error) {
|
||||
manifest, err := parseSimulatedFirmwareManifest(command.Payload)
|
||||
if err != nil {
|
||||
if ackErr := s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"result_code": "MANIFEST_INVALID", "error_message": err.Error()}); ackErr != nil {
|
||||
return firmwareFailed, ackErr
|
||||
}
|
||||
return firmwareFailed, nil
|
||||
}
|
||||
fault, err := simulatedFirmwareFault()
|
||||
if err != nil {
|
||||
return firmwareFailed, err
|
||||
}
|
||||
delay, err := simulatorOptionalDuration("SIM_FIRMWARE_STEP_DELAY", 30*time.Second)
|
||||
if err != nil {
|
||||
return firmwareFailed, err
|
||||
}
|
||||
abEnabled, err := simulatedABFirmwareEnabled()
|
||||
if err != nil {
|
||||
return firmwareFailed, err
|
||||
}
|
||||
if !abEnabled && isABFirmwareFault(fault) {
|
||||
return firmwareFailed, errors.New("A/B firmware fault injection requires SIM_FIRMWARE_AB_UPDATE=true")
|
||||
}
|
||||
if abEnabled {
|
||||
if handled, outcome, resumeErr := s.resumeABFirmwareCommand(ctx, command, manifest, fault, delay); handled || resumeErr != nil {
|
||||
return outcome, resumeErr
|
||||
}
|
||||
}
|
||||
if err := s.ackDeviceCommand(ctx, command.ID, "ACKNOWLEDGED", map[string]any{"phase": "downloading", "progress_percent": 5, "result_code": "DOWNLOAD_STARTED"}); err != nil {
|
||||
return firmwareFailed, err
|
||||
}
|
||||
if fault == "download_failed" {
|
||||
return firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "downloading", "progress_percent": 5, "result_code": "DOWNLOAD_FAILED"})
|
||||
}
|
||||
content, err := s.downloadFirmware(ctx, manifest.DownloadPath)
|
||||
if err != nil {
|
||||
if ackErr := s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "downloading", "progress_percent": 5, "result_code": "DOWNLOAD_FAILED", "error_message": err.Error()}); ackErr != nil {
|
||||
return firmwareFailed, ackErr
|
||||
}
|
||||
return firmwareFailed, nil
|
||||
}
|
||||
digestValue := sha256.Sum256(content)
|
||||
actualDigest := hex.EncodeToString(digestValue[:])
|
||||
if fault == "digest_mismatch" || actualDigest != manifest.Digest {
|
||||
return firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "downloading", "progress_percent": 100, "result_code": "DIGEST_MISMATCH", "expected_sha256": manifest.Digest, "actual_sha256": actualDigest, "bytes_downloaded": len(content)})
|
||||
}
|
||||
publicKey, err := simulatedFirmwareTrustedKey(manifest.SigningKeyID)
|
||||
if err != nil {
|
||||
return firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "downloading", "progress_percent": 100, "result_code": "SIGNING_KEY_UNTRUSTED", "signing_key_id": manifest.SigningKeyID, "error_message": err.Error()})
|
||||
}
|
||||
signedManifest := fmt.Sprintf("firmware-manifest-v1\nmodel=%s\nhardware_revision=%s\nversion=%s\nsha256=%s\nsize=%d\n", env("SIM_DEVICE_MODEL", "simulated-terminal"), env("SIM_DEVICE_HARDWARE_REVISION", "sim-rev-a"), manifest.Version, manifest.Digest, len(content))
|
||||
if fault == "signature_mismatch" || !ed25519.Verify(publicKey, []byte(signedManifest), manifest.Signature) {
|
||||
return firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "downloading", "progress_percent": 100, "result_code": "SIGNATURE_MISMATCH", "signing_key_id": manifest.SigningKeyID, "verified_sha256": actualDigest, "bytes_downloaded": len(content)})
|
||||
}
|
||||
if err := s.ackDeviceCommand(ctx, command.ID, "ACKNOWLEDGED", map[string]any{"phase": "downloading", "progress_percent": 100, "result_code": "ARTIFACT_VERIFIED", "verified_sha256": actualDigest, "verified_signing_key_id": manifest.SigningKeyID, "bytes_downloaded": len(content)}); err != nil {
|
||||
return firmwareFailed, err
|
||||
}
|
||||
if fault == "interrupt_after_download" {
|
||||
return firmwareInterrupted, nil
|
||||
}
|
||||
if err := waitForContext(ctx, delay); err != nil {
|
||||
return firmwareInterrupted, err
|
||||
}
|
||||
if err := s.ackDeviceCommand(ctx, command.ID, "ACKNOWLEDGED", map[string]any{"phase": "installing", "progress_percent": 80, "result_code": "INSTALL_STARTED"}); err != nil {
|
||||
return firmwareFailed, err
|
||||
}
|
||||
if fault == "install_failed" {
|
||||
return firmwareFailed, s.ackDeviceCommand(ctx, command.ID, "FAILED", map[string]any{"phase": "installing", "progress_percent": 80, "result_code": "INSTALL_FAILED"})
|
||||
}
|
||||
if fault == "interrupt_after_install" {
|
||||
return firmwareInterrupted, nil
|
||||
}
|
||||
if err := waitForContext(ctx, delay); err != nil {
|
||||
return firmwareInterrupted, err
|
||||
}
|
||||
if abEnabled {
|
||||
return s.stageABFirmwareCommand(ctx, command, manifest, fault)
|
||||
}
|
||||
if err := s.ackDeviceCommand(ctx, command.ID, "SUCCEEDED", map[string]any{"phase": "confirmed", "progress_percent": 100, "result_code": "SIMULATED_OK", "installed_version": manifest.Version, "verified_sha256": actualDigest, "verified_signing_key_id": manifest.SigningKeyID}); err != nil {
|
||||
return firmwareFailed, err
|
||||
}
|
||||
return firmwareCompleted, nil
|
||||
}
|
||||
|
||||
func parseSimulatedFirmwareManifest(payload map[string]any) (simulatedFirmwareManifest, error) {
|
||||
digest := stringValue(payload, "sha256")
|
||||
signature, signatureErr := base64.StdEncoding.DecodeString(stringValue(payload, "signature"))
|
||||
manifest := simulatedFirmwareManifest{
|
||||
ReleaseID: stringValue(payload, "release_id"),
|
||||
Version: stringValue(payload, "version"),
|
||||
Digest: digest,
|
||||
Signature: signature,
|
||||
SigningKeyID: stringValue(payload, "signing_key_id"),
|
||||
DownloadPath: stringValue(payload, "download_path"),
|
||||
}
|
||||
if manifest.ReleaseID == "" || manifest.Version == "" || manifest.SigningKeyID == "" || len(manifest.Digest) != sha256.Size*2 || signatureErr != nil || len(manifest.Signature) != ed25519.SignatureSize {
|
||||
return simulatedFirmwareManifest{}, errors.New("firmware command requires release_id, version, signing_key_id, a 64-character SHA-256 and a 64-byte Ed25519 signature")
|
||||
}
|
||||
if _, err := hex.DecodeString(manifest.Digest); err != nil {
|
||||
return simulatedFirmwareManifest{}, errors.New("firmware command SHA-256 is not lowercase hexadecimal")
|
||||
}
|
||||
if manifest.Digest != strings.ToLower(manifest.Digest) {
|
||||
return simulatedFirmwareManifest{}, errors.New("firmware command SHA-256 is not lowercase hexadecimal")
|
||||
}
|
||||
parsedPath, err := url.Parse(manifest.DownloadPath)
|
||||
if err != nil || parsedPath.IsAbs() || parsedPath.Host != "" || parsedPath.RawQuery != "" || parsedPath.Fragment != "" || strings.Contains(parsedPath.Path, "..") || !strings.HasPrefix(parsedPath.Path, "/device/v2/firmware-releases/") || !strings.HasSuffix(parsedPath.Path, "/download") {
|
||||
return simulatedFirmwareManifest{}, errors.New("firmware command download_path is outside the fixed device firmware boundary")
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
func stringValue(value map[string]any, key string) string {
|
||||
item, _ := value[key].(string)
|
||||
return strings.TrimSpace(item)
|
||||
}
|
||||
|
||||
func simulatedFirmwareFault() (string, error) {
|
||||
value := strings.ToLower(strings.TrimSpace(os.Getenv("SIM_FIRMWARE_FAULT")))
|
||||
if value == "" || value == "none" {
|
||||
return "none", nil
|
||||
}
|
||||
switch value {
|
||||
case "download_failed", "digest_mismatch", "signature_mismatch", "install_failed", "interrupt_after_download", "interrupt_after_install", "power_loss_after_stage", "power_loss_during_boot", "boot_health_failed":
|
||||
return value, nil
|
||||
default:
|
||||
return "", fmt.Errorf("SIM_FIRMWARE_FAULT must be none, download_failed, digest_mismatch, signature_mismatch, install_failed, interrupt_after_download, interrupt_after_install, power_loss_after_stage, power_loss_during_boot or boot_health_failed")
|
||||
}
|
||||
}
|
||||
|
||||
func isABFirmwareFault(value string) bool {
|
||||
return value == "power_loss_after_stage" || value == "power_loss_during_boot" || value == "boot_health_failed"
|
||||
}
|
||||
|
||||
func simulatedFirmwareTrustedKey(keyID string) (ed25519.PublicKey, error) {
|
||||
raw := strings.TrimSpace(os.Getenv("SIM_FIRMWARE_TRUSTED_KEYS"))
|
||||
if raw == "" {
|
||||
return nil, errors.New("SIM_FIRMWARE_TRUSTED_KEYS is empty")
|
||||
}
|
||||
var encoded map[string]string
|
||||
if err := json.Unmarshal([]byte(raw), &encoded); err != nil {
|
||||
return nil, errors.New("SIM_FIRMWARE_TRUSTED_KEYS must be a JSON object of key IDs to Base64 Ed25519 public keys")
|
||||
}
|
||||
value, exists := encoded[keyID]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("signing key %s is not provisioned", keyID)
|
||||
}
|
||||
publicKey, err := base64.StdEncoding.DecodeString(value)
|
||||
if err != nil || len(publicKey) != ed25519.PublicKeySize {
|
||||
return nil, fmt.Errorf("signing key %s is not a 32-byte Base64 Ed25519 public key", keyID)
|
||||
}
|
||||
return ed25519.PublicKey(publicKey), nil
|
||||
}
|
||||
|
||||
func simulatorOptionalDuration(key string, maximum time.Duration) (time.Duration, error) {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
if value == "" {
|
||||
return 0, nil
|
||||
}
|
||||
parsed, err := time.ParseDuration(value)
|
||||
if err != nil || parsed < 0 || parsed > maximum {
|
||||
return 0, fmt.Errorf("%s must be a non-negative duration no greater than %s", key, maximum)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func (s *simulator) ackDeviceCommand(ctx context.Context, commandID, status string, result map[string]any) error {
|
||||
body, err := json.Marshal(map[string]any{"status": status, "result_summary": result})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.request(ctx, http.MethodPost, "/device/v2/commands/"+commandID+"/ack", body, "")
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *simulator) downloadFirmware(ctx context.Context, path string) ([]byte, error) {
|
||||
result, err := s.request(ctx, http.MethodGet, path, nil, "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var address struct {
|
||||
DownloadURL string `json:"download_url"`
|
||||
}
|
||||
if err := decodeData(result.Body, &address); err != nil || address.DownloadURL == "" {
|
||||
return nil, errors.New("firmware download response did not contain a private URL")
|
||||
}
|
||||
parsed, err := url.Parse(address.DownloadURL)
|
||||
if err != nil || (parsed.Scheme != "http" && parsed.Scheme != "https") || parsed.Host == "" {
|
||||
return nil, errors.New("firmware private download URL is invalid")
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, address.DownloadURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response, err := s.client.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("download firmware object: %w", err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("firmware object returned HTTP %d", response.StatusCode)
|
||||
}
|
||||
content, err := io.ReadAll(io.LimitReader(response.Body, maximumSimulatedFirmwareSize+1))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read firmware object: %w", err)
|
||||
}
|
||||
if len(content) == 0 || len(content) > maximumSimulatedFirmwareSize {
|
||||
return nil, fmt.Errorf("firmware object size %d is outside 1 to %d bytes", len(content), maximumSimulatedFirmwareSize)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
Reference in New Issue
Block a user