From c2680474b17fa3d53a0c0951ec0209ce43e5c28c Mon Sep 17 00:00:00 2001 From: Boris Rybalkin Date: Wed, 1 Jul 2026 22:47:14 +0100 Subject: [PATCH] devicestate: drop trusted-baked assertions from serial ancillary batch The syncloud brand account and root account-key are baked into the trusted assertion set (asserts/sysdb.trusted), but the device service still streams them alongside the serial when registering the device. Committing the returned ancillary batch fails because the assertion database refuses to add an assertion whose primary key clashes with a trusted one, so device registration (the 'Initialize device' change) perpetually shows Error in 'snap changes': cannot add "account-key" assertion with primary key clashing with a trusted assertion: [hIedp1AvrWlcDI4uS_qjoFLzjKl5enu4G2FYJpgB3Pj...] Drop ancillary assertions that already exist in the trusted set when building the batch. When the batch ends up empty the serial is accepted via the acceptSerialOnly path, whose fetcher resolves the trusted signing key via FindPredefined and skips it, so registration completes cleanly and the noise disappears. This keeps the change small and contained; the eventual store-side fix (not streaming already-trusted assertions) would let an unmodified upstream snapd work as well. --- .../devicestate/devicestate_serial_test.go | 13 ++++++++ overlord/devicestate/export_test.go | 2 ++ overlord/devicestate/handlers_serial.go | 32 +++++++++++++++++++ 3 files changed, 47 insertions(+) diff --git a/overlord/devicestate/devicestate_serial_test.go b/overlord/devicestate/devicestate_serial_test.go index 633db7df5f0..44e1699260d 100644 --- a/overlord/devicestate/devicestate_serial_test.go +++ b/overlord/devicestate/devicestate_serial_test.go @@ -2376,3 +2376,16 @@ func (s *deviceMgrSerialSuite) TestDeviceSerialRestoreHappy(c *C) { c.Check(log.String(), testutil.Contains, fmt.Sprintf("restored serial serial-1234 for my-brand/pc-20 signed with key %v", devKey.PublicKey().ID())) } + +func (s *deviceMgrSerialSuite) TestClashesWithTrusted(c *C) { + trusted := sysdb.Trusted() + c.Assert(trusted, Not(HasLen), 0) + + for _, a := range trusted { + c.Check(devicestate.ClashesWithTrusted(a), Equals, true, Commentf("trusted %s %v should clash", a.Type().Name, a.Ref().PrimaryKey)) + } + + notTrusted := s.storeSigning.StoreAccountKey("") + c.Assert(notTrusted, NotNil) + c.Check(devicestate.ClashesWithTrusted(notTrusted), Equals, false) +} diff --git a/overlord/devicestate/export_test.go b/overlord/devicestate/export_test.go index e44739f3e28..2e54b71d10d 100644 --- a/overlord/devicestate/export_test.go +++ b/overlord/devicestate/export_test.go @@ -565,3 +565,5 @@ func CleanUpEncryptionSetupDataInCache(st *state.State, label string) { key := encryptionSetupDataKey{label} st.Cache(key, nil) } + +var ClashesWithTrusted = clashesWithTrusted diff --git a/overlord/devicestate/handlers_serial.go b/overlord/devicestate/handlers_serial.go index e3e5a82f4a1..4339640dd1c 100644 --- a/overlord/devicestate/handlers_serial.go +++ b/overlord/devicestate/handlers_serial.go @@ -34,6 +34,7 @@ import ( "gopkg.in/tomb.v2" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/sysdb" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/httputil" "github.com/snapcore/snapd/logger" @@ -431,6 +432,27 @@ func prepareSerialRequest(t *state.Task, regCtx registrationContext, privKey ass var errPoll = errors.New("serial-request accepted, poll later") +func clashesWithTrusted(a asserts.Assertion) bool { + ref := a.Ref() + for _, trusted := range sysdb.Trusted() { + tref := trusted.Ref() + if tref.Type != ref.Type || len(tref.PrimaryKey) != len(ref.PrimaryKey) { + continue + } + same := true + for i := range tref.PrimaryKey { + if tref.PrimaryKey[i] != ref.PrimaryKey[i] { + same = false + break + } + } + if same { + return true + } + } + return false +} + func submitSerialRequest(t *state.Task, serialRequest string, client *http.Client, cfg *serialRequestConfig) (*asserts.Serial, *asserts.Batch, error) { st := t.State() st.Unlock() @@ -477,6 +499,16 @@ func submitSerialRequest(t *state.Task, serialRequest string, client *http.Clien } serial = got.(*asserts.Serial) } else { + // Syncloud: the brand account and root account-key are baked + // into the trusted assertion set (asserts/sysdb), yet the device + // service still streams them alongside the serial. Re-adding an + // assertion whose primary key clashes with a trusted one is + // refused by the assertion database and fails registration; drop + // such assertions as they are already known. + if clashesWithTrusted(got) { + logger.Noticef("Syncloud: dropping ancillary %q assertion already present in the trusted set: %v", got.Type().Name, got.Ref().PrimaryKey) + continue + } if batch == nil { batch = asserts.NewBatch(nil) }