Skip to content

Commit

Permalink
o/devicestate,asserts: sign confdb-control assertions with the device…
Browse files Browse the repository at this point in the history
… key (#14723)

* o/devicestate,asserts: sign confdb-control assertions with the device key

* o/devicestate,asserts: add deviceSigner tests

* o/devicestate: pass revision when signing confdb-control assertion

* asserts: remove deviceSigner interface

* o/devicestate: refactor confdb-control signing tests

* asserts: refactor confdb-control ack tests

* asserts,o/devicestate: fix error messages
  • Loading branch information
st3v3nmw authored Jan 8, 2025
1 parent 9913192 commit a24fe7a
Show file tree
Hide file tree
Showing 7 changed files with 244 additions and 6 deletions.
4 changes: 2 additions & 2 deletions asserts/account_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion asserts/asserts.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
32 changes: 32 additions & 0 deletions asserts/confdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
98 changes: 97 additions & 1 deletion asserts/confdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
package asserts_test

import (
"path/filepath"
"strings"
"time"

Expand Down Expand Up @@ -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{})

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
}
7 changes: 6 additions & 1 deletion asserts/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
27 changes: 27 additions & 0 deletions overlord/devicestate/devicemgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -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
Expand Down
80 changes: 79 additions & 1 deletion overlord/devicestate/devicestate_test.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}

0 comments on commit a24fe7a

Please sign in to comment.