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
  • Loading branch information
st3v3nmw committed Dec 10, 2024
1 parent 3cefe04 commit 4ae7fad
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 4 deletions.
43 changes: 43 additions & 0 deletions asserts/confdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Check warning on line 146 in asserts/confdb.go

View check run for this annotation

Codecov / codecov/patch

asserts/confdb.go#L135-L146

Added lines #L135 - L146 were not covered by tests

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")
}

Check warning on line 153 in asserts/confdb.go

View check run for this annotation

Codecov / codecov/patch

asserts/confdb.go#L148-L153

Added lines #L148 - L153 were not covered by tests

return key, nil

Check warning on line 155 in asserts/confdb.go

View check run for this annotation

Codecov / codecov/patch

asserts/confdb.go#L155

Added line #L155 was not covered by tests
}

// 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()}},
}

Check warning on line 162 in asserts/confdb.go

View check run for this annotation

Codecov / codecov/patch

asserts/confdb.go#L159-L162

Added lines #L159 - L162 were not covered by tests
}

// BrandID returns the brand identifier of the device.
func (cc *ConfdbControl) BrandID() string {
return cc.HeaderString("brand-id")
Expand Down
14 changes: 11 additions & 3 deletions asserts/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Check warning on line 790 in asserts/database.go

View check run for this annotation

Codecov / codecov/patch

asserts/database.go#L785-L790

Added lines #L785 - L790 were not covered by tests
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 {
Expand Down
28 changes: 28 additions & 0 deletions overlord/devicestate/devicemgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Check warning on line 1979 in overlord/devicestate/devicemgr.go

View check run for this annotation

Codecov / codecov/patch

overlord/devicestate/devicemgr.go#L1978-L1979

Added lines #L1978 - L1979 were not covered by tests

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
}

Check warning on line 1989 in overlord/devicestate/devicemgr.go

View check run for this annotation

Codecov / codecov/patch

overlord/devicestate/devicemgr.go#L1988-L1989

Added lines #L1988 - L1989 were not covered by tests

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
68 changes: 67 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 @@ -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)
}

0 comments on commit 4ae7fad

Please sign in to comment.