diff --git a/asserts/account_key.go b/asserts/account_key.go index 7d6d9996245..051909f1981 100644 --- a/asserts/account_key.go +++ b/asserts/account_key.go @@ -351,8 +351,8 @@ func (akr *AccountKeyRequest) PublicKeyID() string { } // signKey returns the underlying public key of the requested account key. -func (akr *AccountKeyRequest) signKey() PublicKey { - return akr.pubKey +func (akr *AccountKeyRequest) signKey(db RODatabase) (PublicKey, error) { + return akr.pubKey, nil } // Implement further consistency checks. diff --git a/asserts/asserts.go b/asserts/asserts.go index 5f680c18faf..f48181d41da 100644 --- a/asserts/asserts.go +++ b/asserts/asserts.go @@ -569,7 +569,7 @@ type SequenceMember interface { // customSigner represents an assertion with special arrangements for its signing key (e.g. self-signed), rather than the usual case where an assertion is signed by its authority. type customSigner interface { // signKey returns the public key material for the key that signed this assertion. See also SignKeyID. - signKey() PublicKey + signKey(db RODatabase) (PublicKey, error) } // MediaType is the media type for encoded assertions on the wire. diff --git a/asserts/confdb.go b/asserts/confdb.go index 9a8196723cd..b4e31f75f33 100644 --- a/asserts/confdb.go +++ b/asserts/confdb.go @@ -119,6 +119,38 @@ type ConfdbControl struct { operators map[string]*confdb.Operator } +// expected interfaces are implemented +var ( + _ customSigner = (*ConfdbControl)(nil) +) + +// 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 err != nil { + return nil, fmt.Errorf("cannot find matching device serial assertion: %w", 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 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/confdb_test.go b/asserts/confdb_test.go index 80925626ad9..d068a1ca435 100644 --- a/asserts/confdb_test.go +++ b/asserts/confdb_test.go @@ -20,6 +20,7 @@ package asserts_test import ( + "path/filepath" "strings" "time" @@ -201,7 +202,9 @@ func (s *confdbSuite) TestAssembleAndSignChecksSchemaFormatFail(c *C) { c.Assert(err, ErrorMatches, `assertion confdb: JSON in body must be indented with 2 spaces and sort object entries by key`) } -type confdbCtrlSuite struct{} +type confdbCtrlSuite struct { + db *asserts.Database +} var _ = Suite(&confdbCtrlSuite{}) @@ -236,6 +239,42 @@ sign-key-sha3-384: t9yuKGLyiezBq_PXMJZsGdkTukmL7MgrgqXAlxxiZF4TYryOjZcy48nnjDmEH AXNpZw==` ) +func (s *confdbCtrlSuite) SetUpTest(c *C) { + topDir := filepath.Join(c.MkDir(), "asserts-db") + bs, err := asserts.OpenFSBackstore(topDir) + c.Assert(err, IsNil) + cfg := &asserts.DatabaseConfig{ + Backstore: bs, + Trusted: []asserts.Assertion{ + asserts.BootstrapAccountForTest("canonical"), + asserts.BootstrapAccountKeyForTest("canonical", testPrivKey0.PublicKey()), + }, + } + db, err := asserts.OpenDatabase(cfg) + c.Assert(err, IsNil) + s.db = db +} + +func (s *confdbCtrlSuite) addSerial(c *C) { + pubKey := testPrivKey0.PublicKey() + encodedPubKey, err := asserts.EncodePublicKey(pubKey) + c.Assert(err, IsNil) + + serial, err := asserts.AssembleAndSignInTest(asserts.SerialType, map[string]interface{}{ + "authority-id": "canonical", + "brand-id": "canonical", + "model": "pc", + "serial": "42", + "device-key": string(encodedPubKey), + "device-key-sha3-384": pubKey.ID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, testPrivKey0) + c.Assert(err, IsNil) + + err = s.db.Add(serial) + c.Assert(err, IsNil) +} + func (s *confdbCtrlSuite) TestDecodeOK(c *C) { encoded := confdbControlExample @@ -350,3 +389,60 @@ func (s *confdbCtrlSuite) TestDecodeInvalid(c *C) { c.Assert(err, ErrorMatches, validationSetErrPrefix+test.expectedErr, Commentf("test %d/%d failed", i+1, len(invalidTests))) } } + +func (s *confdbCtrlSuite) TestPrerequisites(c *C) { + a, err := asserts.Decode([]byte(confdbControlExample)) + c.Assert(err, IsNil) + + prereqs := a.Prerequisites() + c.Assert(prereqs, HasLen, 1) + c.Check(prereqs[0], DeepEquals, &asserts.Ref{ + Type: asserts.SerialType, + PrimaryKey: []string{"generic", "generic-classic", "03961d5d-26e5-443f-838d-6db046126bea"}, + }) +} + +func (s *confdbCtrlSuite) TestAckAssertionNoSerial(c *C) { + headers := map[string]interface{}{ + "brand-id": "canonical", "model": "pc", "serial": "42", "groups": []interface{}{}, + } + a, err := asserts.AssembleAndSignInTest(asserts.ConfdbControlType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + err = s.db.Add(a) + c.Assert( + err, + ErrorMatches, + `cannot check no-authority assertion type "confdb-control": cannot find matching device serial assertion: .* not found`, + ) +} + +func (s *confdbCtrlSuite) TestAckAssertionKeysMismatch(c *C) { + s.addSerial(c) + + headers := map[string]interface{}{ + "brand-id": "canonical", "model": "pc", "serial": "42", "groups": []interface{}{}, + } + a, err := asserts.AssembleAndSignInTest(asserts.ConfdbControlType, headers, nil, testPrivKey2) + c.Assert(err, IsNil) + + err = s.db.Add(a) + c.Assert( + err, + ErrorMatches, + `cannot check no-authority assertion type "confdb-control": confdb-control's signing key doesn't match the device key`, + ) +} + +func (s *confdbCtrlSuite) TestAckAssertionOK(c *C) { + s.addSerial(c) + + headers := map[string]interface{}{ + "brand-id": "canonical", "model": "pc", "serial": "42", "groups": []interface{}{}, + } + a, err := asserts.AssembleAndSignInTest(asserts.ConfdbControlType, headers, nil, testPrivKey0) + c.Assert(err, IsNil) + + err = s.db.Add(a) + c.Assert(err, IsNil) +} diff --git a/asserts/database.go b/asserts/database.go index d22e23d05e7..39b07698f0f 100644 --- a/asserts/database.go +++ b/asserts/database.go @@ -783,8 +783,13 @@ func CheckSignature(assert Assertion, signingKey *AccountKey, roDB RODatabase, c if !ok { return fmt.Errorf("cannot check no-authority assertion type %q", assert.Type().Name) } - pubKey = custom.signKey() + + pubKey, err = custom.signKey(roDB) + if err != nil { + return fmt.Errorf("cannot check no-authority assertion type %q: %w", assert.Type().Name, err) + } } + 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..f9708cdbdc9 100644 --- a/overlord/devicestate/devicemgr.go +++ b/overlord/devicestate/devicemgr.go @@ -26,6 +26,7 @@ import ( "os" "path/filepath" "regexp" + "strconv" "strings" "time" @@ -1963,6 +1964,32 @@ 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{}, revision int) (*asserts.ConfdbControl, error) { + serial, err := m.Serial() + if err != nil { + return nil, fmt.Errorf("cannot sign confdb-control without a serial") + } + + privKey, err := m.keyPair() + if err != nil { + return nil, fmt.Errorf("cannot sign confdb-control without device key") + } + + a, err := asserts.SignWithoutAuthority(asserts.ConfdbControlType, map[string]interface{}{ + "brand-id": serial.BrandID(), + "model": serial.Model(), + "serial": serial.Serial(), + "revision": strconv.Itoa(revision), + "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..43ba35d129f 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 @@ -461,6 +461,22 @@ func (s *deviceMgrBaseSuite) makeSerialAssertionInState(c *C, brandID, model, se return makeSerialAssertionInState(c, s.brands, s.state, brandID, model, serialN) } +func (s *deviceMgrBaseSuite) addKeyToManagerInState(c *C) { + device, err := devicestatetest.Device(s.state) + c.Assert(err, IsNil) + + err = devicestate.KeypairManager(s.mgr).Put(devKey) + c.Assert(err, IsNil) + + err = devicestatetest.SetDevice(s.state, &auth.DeviceState{ + Brand: device.Brand, + Model: device.Model, + Serial: device.Serial, + KeyID: devKey.PublicKey().ID(), + }) + c.Assert(err, IsNil) +} + func (s *deviceMgrSuite) SetUpTest(c *C) { classic := false s.setupBaseTest(c, classic) @@ -2785,3 +2801,65 @@ func (s *deviceMgrSuite) TestDefaultRecoverySystem(c *C) { c.Assert(err, IsNil) c.Check(*system, Equals, expectedSystem) } + +func (s *deviceMgrSuite) TestSignConfdbControlNoSerial(c *C) { + s.state.Lock() + defer s.state.Unlock() + + _, err := s.mgr.SignConfdbControl([]interface{}{}, 2) + c.Assert(err, ErrorMatches, "cannot sign confdb-control without a serial") +} + +func (s *deviceMgrSuite) TestSignConfdbControlNoKey(c *C) { + s.setPCModelInState(c) + s.state.Lock() + defer s.state.Unlock() + + s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial") + + _, err := s.mgr.SignConfdbControl([]interface{}{}, 3) + c.Assert(err, ErrorMatches, "cannot sign confdb-control without device key") +} + +func (s *deviceMgrSuite) TestSignConfdbControlInvalid(c *C) { + s.setPCModelInState(c) + s.state.Lock() + defer s.state.Unlock() + + s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial") + s.addKeyToManagerInState(c) + + groups := []interface{}{map[string]interface{}{"operator-id": "jane"}} + _, err := s.mgr.SignConfdbControl(groups, 4) + c.Assert( + err, + ErrorMatches, + "cannot assemble assertion confdb-control: cannot parse group at position 1: \"authentication\" must be provided", + ) +} + +func (s *deviceMgrSuite) TestSignConfdbControlOK(c *C) { + s.setPCModelInState(c) + s.state.Lock() + defer s.state.Unlock() + + s.makeSerialAssertionInState(c, "canonical", "pc", "serialserialserial") + s.addKeyToManagerInState(c) + + jane := map[string]interface{}{ + "operator-id": "jane", + "authentication": []interface{}{"operator-key"}, + "views": []interface{}{ + "canonical/network/observe-interfaces", + "canonical/network/control-interfaces", + }, + } + groups := []interface{}{jane} + + cc, err := s.mgr.SignConfdbControl(groups, 5) + c.Assert(err, IsNil) + c.Assert(cc.Revision(), Equals, 5) + + // Confirm we can ack it + assertstatetest.AddMany(s.state, cc) +}