diff --git a/asserts/confdb.go b/asserts/confdb.go index 9a8196723cd..b5cdea69d69 100644 --- a/asserts/confdb.go +++ b/asserts/confdb.go @@ -119,6 +119,49 @@ type ConfdbControl struct { operators map[string]*confdb.Operator } +// expected interfaces are implemented +var ( + _ deviceSigner = (*ConfdbControl)(nil) +) + +// deviceSigner represents an assertion that is signed by the device. +// unlike a customSigner, the assertion doesn't carry the signing key in its body. +type deviceSigner interface { + // signKey returns the public key material for the key that signed this assertion. + signKey(db RODatabase) (PublicKey, error) +} + +// signKey returns the public key of the device that signed this assertion. +func (cc *ConfdbControl) signKey(db RODatabase) (PublicKey, error) { + a, err := db.Find(SerialType, map[string]string{ + "brand-id": cc.BrandID(), + "model": cc.Model(), + "serial": cc.Serial(), + }) + if errors.Is(err, &NotFoundError{}) { + return nil, errors.New("no matching serial assertion found") + } + if err != nil { + return nil, err + } + + serial := a.(*Serial) + + key := serial.DeviceKey() + if key.ID() != cc.SignKeyID() { + return nil, errors.New("confdb-control's signing key doesn't match the device's key") + } + + return key, nil +} + +// Prerequisites returns references to this confdb-control's prerequisite assertions. +func (cc *ConfdbControl) Prerequisites() []*Ref { + return []*Ref{ + {Type: SerialType, PrimaryKey: []string{cc.BrandID(), cc.Model(), cc.Serial()}}, + } +} + // BrandID returns the brand identifier of the device. func (cc *ConfdbControl) BrandID() string { return cc.HeaderString("brand-id") diff --git a/asserts/database.go b/asserts/database.go index d22e23d05e7..2304278cf72 100644 --- a/asserts/database.go +++ b/asserts/database.go @@ -779,12 +779,20 @@ func CheckSignature(assert Assertion, signingKey *AccountKey, roDB RODatabase, c return fmt.Errorf("assertion does not match signing constraints for public key %q from %q", assert.SignKeyID(), assert.AuthorityID()) } } else { - custom, ok := assert.(customSigner) - if !ok { + switch signer := assert.(type) { + case customSigner: + pubKey = signer.signKey() + case deviceSigner: + var err error + pubKey, err = signer.signKey(roDB) + if err != nil { + return err + } + default: return fmt.Errorf("cannot check no-authority assertion type %q", assert.Type().Name) } - pubKey = custom.signKey() } + content, encSig := assert.Signature() signature, err := decodeSignature(encSig) if err != nil { diff --git a/overlord/devicestate/devicemgr.go b/overlord/devicestate/devicemgr.go index a255e2f2ac2..8afe1e573c1 100644 --- a/overlord/devicestate/devicemgr.go +++ b/overlord/devicestate/devicemgr.go @@ -1963,6 +1963,34 @@ func (m *DeviceManager) keyPair() (asserts.PrivateKey, error) { return privKey, nil } +// SignConfdbControl signs a confdb-control assertion using the device's key as it needs to be attested by the device. +func (m *DeviceManager) SignConfdbControl(groups []interface{}) (*asserts.ConfdbControl, error) { + serial, err := m.Serial() + if err != nil { + return nil, fmt.Errorf("internal error: cannot sign confdb-control without a serial: %w", err) + } + + privKey, err := m.keyPair() + if errors.Is(err, state.ErrNoState) { + return nil, fmt.Errorf("internal error: inconsistent state with serial but no device key") + } + if err != nil { + return nil, err + } + + a, err := asserts.SignWithoutAuthority(asserts.ConfdbControlType, map[string]interface{}{ + "brand-id": serial.BrandID(), + "model": serial.Model(), + "serial": serial.Serial(), + "groups": groups, + }, nil, privKey) + if err != nil { + return nil, err + } + + return a.(*asserts.ConfdbControl), nil +} + // Registered returns a channel that is closed when the device is known to have been registered. func (m *DeviceManager) Registered() <-chan struct{} { return m.reg diff --git a/overlord/devicestate/devicestate_test.go b/overlord/devicestate/devicestate_test.go index e7787e9f65d..d50b6645663 100644 --- a/overlord/devicestate/devicestate_test.go +++ b/overlord/devicestate/devicestate_test.go @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2022 Canonical Ltd + * Copyright (C) 2016-2024 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -2785,3 +2785,69 @@ func (s *deviceMgrSuite) TestDefaultRecoverySystem(c *C) { c.Assert(err, IsNil) c.Check(*system, Equals, expectedSystem) } + +func (s *deviceMgrSuite) TestSignConfdbControl(c *C) { + s.state.Lock() + defer s.state.Unlock() + + s.makeModelAssertionInState(c, "canonical", "pc", map[string]interface{}{ + "architecture": "amd64", + "kernel": "pc-kernel", + "gadget": "pc", + }) + + jane := map[string]interface{}{ + "operator-id": "jane", + "authentication": []interface{}{"operator-key"}, + "views": []interface{}{ + "canonical/network/observe-interfaces", + "canonical/network/control-interfaces", + }, + } + groups := []interface{}{jane} + + // No serial assertion exists yet + _, err := s.mgr.SignConfdbControl(groups) + c.Assert(err, ErrorMatches, "internal error: cannot sign confdb-control without a serial: no state entry for key") + + // Add serial assertion + encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey()) + c.Check(err, IsNil) + serial, err := s.storeSigning.Sign(asserts.SerialType, map[string]interface{}{ + "brand-id": "canonical", + "model": "pc", + "serial": "42", + "device-key": string(encDevKey), + "device-key-sha3-384": devKey.PublicKey().ID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + assertstatetest.AddMany(s.state, serial) + + devicestatetest.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + Serial: "42", + }) + + _, err = s.mgr.SignConfdbControl(groups) + c.Assert(err, ErrorMatches, "internal error: inconsistent state with serial but no device key") + + // Add device key to manager + devicestate.KeypairManager(s.mgr).Put(devKey) + + devicestatetest.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + Serial: "42", + KeyID: devKey.PublicKey().ID(), + }) + + // Sign assertion + cc, err := s.mgr.SignConfdbControl(groups) + c.Assert(err, IsNil) + + // Confirm we can ack it + // AddMany panics on error, that's why we aren't c.Assert'ing anything + assertstatetest.AddMany(s.state, cc) +}