mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
feat(health): add speaker_ca_bundle integrity check
Two per-device checks run against each speaker's CA bundle via a
single SSH probe round-trip:
(1) Every PEM block from ca-bundle.crt.original (the factory backup
written by TrustCACertFromBytes on first CA injection) must be
present in the live ca-bundle.crt. A missing block means the
original trust store was truncated, which would break external
HTTPS (Spotify, Amazon, firmware updates).
(2) The AfterTouch CA sentinel (# AfterTouch) must be present in
the live bundle. Without it the speaker rejects AfterTouch's
TLS cert and migration is effectively inactive.
Both findings carry a QuickFix:
- FixIDRestoreAndInjectCA: cp .original → live bundle over SSH,
then TrustCACert to re-inject the AfterTouch CA.
- FixIDInjectCACert: TrustCACert only (original certs intact).
Graceful degradation:
- SSH unavailable → SeverityInfo, no fix offered.
- .original absent (device never had install-ca run) → SeverityWarning,
suggest install-ca; check (2) still runs.
Infrastructure changes:
- ssh_probe.go: add ca-bundle.crt.original to probeFilePaths (free
in the existing single-round-trip batch).
- setup.go: export ProbeCABundles and RestoreCABundleFromOriginal so
the handlers package can use them without exposing speakerProbe.
- Fix executors live in handlers (need setup.Manager) per the
established boundary used by completeSpeakerPairingFix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
172e14dc26
commit
118e3fc4a0
@@ -1198,6 +1198,53 @@ func (m *Manager) EnsureRemoteServices(deviceIP string) (string, error) {
|
||||
return logs, fmt.Errorf("failed to enable remote services in any of the locations: %v", locations)
|
||||
}
|
||||
|
||||
// ProbeCABundles returns the live CA bundle and the .original factory backup
|
||||
// from the speaker at deviceIP via a single SSH round-trip. sshOK is false
|
||||
// when SSH is unreachable or authentication failed; in that case both bundle
|
||||
// strings are empty. Either bundle string may be empty if the corresponding
|
||||
// file does not exist on the device (e.g. .original is absent on speakers
|
||||
// that have never had the AfterTouch CA injected).
|
||||
//
|
||||
// Used by the health check to verify bundle integrity without exposing the
|
||||
// unexported speakerProbe type.
|
||||
func (m *Manager) ProbeCABundles(deviceIP string) (current, original string, sshOK bool) {
|
||||
probe := m.probeSpeakerSSH(deviceIP)
|
||||
|
||||
return probe.Files["/etc/pki/tls/certs/ca-bundle.crt"],
|
||||
probe.Files["/etc/pki/tls/certs/ca-bundle.crt.original"],
|
||||
probe.SSHOK
|
||||
}
|
||||
|
||||
// RestoreCABundleFromOriginal copies the .original factory backup over the
|
||||
// live ca-bundle.crt on the speaker. It is called by the health-check fix
|
||||
// executor when check (1) — "original certs are contained in current bundle"
|
||||
// — fails, before re-injecting the AfterTouch CA via TrustCACert.
|
||||
//
|
||||
// The operation requires SSH access and a read-write root filesystem; both
|
||||
// are attempted via the same remount path as TrustCACertFromBytes.
|
||||
func (m *Manager) RestoreCABundleFromOriginal(deviceIP string) error {
|
||||
if m.NewSSH == nil {
|
||||
return errors.New("RestoreCABundleFromOriginal: no SSH factory configured")
|
||||
}
|
||||
|
||||
bundlePath := "/etc/pki/tls/certs/ca-bundle.crt"
|
||||
client := m.NewSSH(deviceIP)
|
||||
|
||||
if _, err := client.Run("(rw || mount -o remount,rw /)"); err != nil {
|
||||
return fmt.Errorf("remount rw: %w", err)
|
||||
}
|
||||
|
||||
if _, err := client.Run(fmt.Sprintf("[ -f %s.original ]", bundlePath)); err != nil {
|
||||
return fmt.Errorf("original backup %s.original not found on speaker — run `setup install-ca` first", bundlePath)
|
||||
}
|
||||
|
||||
if _, err := client.Run(fmt.Sprintf("cp %s.original %s", bundlePath, bundlePath)); err != nil {
|
||||
return fmt.Errorf("restore from original: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TrustCACert injects the local CA certificate into the device's shared
|
||||
// trust store. The cert is read from disk via Manager.Crypto — used by
|
||||
// the in-process migration flow where the CLI and the certmanager share
|
||||
|
||||
@@ -35,11 +35,12 @@ type speakerProbe struct {
|
||||
// with the consumers in GetMigrationSummary.
|
||||
var (
|
||||
probeFilePaths = []string{
|
||||
SoundTouchSdkPrivateCfgPath, // current XML config
|
||||
SoundTouchSdkPrivateCfgPath + ".original", // backup XML config
|
||||
"/etc/resolv.conf", // DNS resolver
|
||||
"/etc/hosts", // hostname overrides
|
||||
"/etc/pki/tls/certs/ca-bundle.crt", // CA trust store
|
||||
SoundTouchSdkPrivateCfgPath, // current XML config
|
||||
SoundTouchSdkPrivateCfgPath + ".original", // backup XML config
|
||||
"/etc/resolv.conf", // DNS resolver
|
||||
"/etc/hosts", // hostname overrides
|
||||
"/etc/pki/tls/certs/ca-bundle.crt", // CA trust store
|
||||
"/etc/pki/tls/certs/ca-bundle.crt.original", // factory backup written by TrustCACertFromBytes
|
||||
}
|
||||
|
||||
probeExistsPaths = []string{
|
||||
|
||||
Reference in New Issue
Block a user