Skip to content

Commit

Permalink
asserts,seed: preseed authority delegation (#13034)
Browse files Browse the repository at this point in the history
* asserts/model: add preseedAuthority field to Model

* seed20: allow authority-id to differ from the brand-id

* fixup! asserts/model: add preseedAuthority field to Model

fix comment wording to PreseedAuthority

* fixup! seed20: allow authority-id to differ from the brand-id

clarify error message as "preseed authority-id"

* fixup! asserts/model: add preseedAuthority field to Model

standardize checkOptionalAuthority() signature and make acceptsAny bool explicit when invoking it

* fixup! seed20: allow authority-id to differ from the brand-id

fix ineffectual assignment to preseedAs2

* fixup! asserts/model: add preseedAuthority field to Model

bump copyright years for files touched by 5593e76

* fixup! seed20: allow authority-id to differ from the brand-id

bump copyright years for files touched by ce7ba34

* fixup! asserts/model: add preseedAuthority field to Model

asserts/model.go: rename "acceptsAny" to "acceptsWildcard"
  • Loading branch information
atomcult authored Sep 4, 2023
1 parent cbd51b2 commit 7c17cf2
Show file tree
Hide file tree
Showing 6 changed files with 172 additions and 37 deletions.
58 changes: 38 additions & 20 deletions asserts/model.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-2023 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 @@ -467,6 +467,7 @@ type Model struct {

serialAuthority []string
sysUserAuthority []string
preseedAuthority []string
timestamp time.Time
}

Expand Down Expand Up @@ -626,6 +627,13 @@ func (mod *Model) SystemUserAuthority() []string {
return mod.sysUserAuthority
}

// PreseedAuthority returns the authority ids that are accepted as
// signers of the preseed binary blob for this model. It always includes the
// brand of the model.
func (mod *Model) PreseedAuthority() []string {
return mod.preseedAuthority
}

// Timestamp returns the time when the model assertion was issued.
func (mod *Model) Timestamp() time.Time {
return mod.timestamp
Expand Down Expand Up @@ -666,31 +674,15 @@ func checkAuthorityMatchesBrand(a Assertion) error {
return nil
}

func checkOptionalSerialAuthority(headers map[string]interface{}, brandID string) ([]string, error) {
ids := []string{brandID}
const name = "serial-authority"
if _, ok := headers[name]; !ok {
return ids, nil
}
if lst, err := checkStringListMatches(headers, name, validAccountID); err == nil {
if !strutil.ListContains(lst, brandID) {
lst = append(ids, lst...)
}
return lst, nil
}
return nil, fmt.Errorf("%q header must be a list of account ids", name)
}

func checkOptionalSystemUserAuthority(headers map[string]interface{}, brandID string) ([]string, error) {
func checkOptionalAuthority(headers map[string]interface{}, name string, brandID string, acceptsWildcard bool) ([]string, error) {
ids := []string{brandID}
const name = "system-user-authority"
v, ok := headers[name]
if !ok {
return ids, nil
}
switch x := v.(type) {
case string:
if x == "*" {
if acceptsWildcard && x == "*" {
return nil, nil
}
case []interface{}:
Expand All @@ -702,7 +694,27 @@ func checkOptionalSystemUserAuthority(headers map[string]interface{}, brandID st
return lst, nil
}
}
return nil, fmt.Errorf("%q header must be '*' or a list of account ids", name)

if acceptsWildcard {
return nil, fmt.Errorf("%q header must be '*' or a list of account ids", name)
} else {
return nil, fmt.Errorf("%q header must be a list of account ids", name)
}
}

func checkOptionalSerialAuthority(headers map[string]interface{}, brandID string) ([]string, error) {
const acceptsWildcard = false
return checkOptionalAuthority(headers, "serial-authority", brandID, acceptsWildcard)
}

func checkOptionalSystemUserAuthority(headers map[string]interface{}, brandID string) ([]string, error) {
const acceptsWildcard = true
return checkOptionalAuthority(headers, "system-user-authority", brandID, acceptsWildcard)
}

func checkOptionalPreseedAuthority(headers map[string]interface{}, brandID string) ([]string, error) {
const acceptsWildcard = false
return checkOptionalAuthority(headers, "preseed-authority", brandID, acceptsWildcard)
}

func checkModelValidationSetAccountID(headers map[string]interface{}, what, brandID string) (string, error) {
Expand Down Expand Up @@ -1028,6 +1040,11 @@ func assembleModel(assert assertionBase) (Assertion, error) {
return nil, err
}

preseedAuthority, err := checkOptionalPreseedAuthority(assert.headers, brandID)
if err != nil {
return nil, err
}

timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
if err != nil {
return nil, err
Expand Down Expand Up @@ -1062,6 +1079,7 @@ func assembleModel(assert assertionBase) (Assertion, error) {
validationSets: valSets,
serialAuthority: serialAuthority,
sysUserAuthority: sysUserAuthority,
preseedAuthority: preseedAuthority,
timestamp: timestamp,
}, nil
}
26 changes: 25 additions & 1 deletion asserts/model_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-2020 Canonical Ltd
* Copyright (C) 2016-2023 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 @@ -49,6 +49,7 @@ const (
reqSnaps = "required-snaps:\n - foo\n - bar\n"
sysUserAuths = "system-user-authority: *\n"
serialAuths = "serial-authority:\n - generic\n"
preseedAuths = "preseed-authority:\n - preseed-delegate\n"
)

const (
Expand All @@ -66,6 +67,7 @@ const (
serialAuths +
sysUserAuths +
reqSnaps +
preseedAuths +
"TSLINE" +
"body-length: 0\n" +
"sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
Expand Down Expand Up @@ -276,6 +278,7 @@ func (mods *modelSuite) TestDecodeOK(c *C) {

c.Check(model.SystemUserAuthority(), HasLen, 0)
c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1", "generic"})
c.Check(model.PreseedAuthority(), DeepEquals, []string{"brand-id1", "preseed-delegate"})
}

func (mods *modelSuite) TestDecodeStoreIsOptional(c *C) {
Expand Down Expand Up @@ -458,6 +461,23 @@ func (mods *modelSuite) TestDecodeSystemUserAuthorityIsOptional(c *C) {
c.Check(model.SystemUserAuthority(), DeepEquals, []string{"brand-id1", "foo", "bar"})
}

func (mods *modelSuite) TestDecodePreseedAuthorityIsOptional(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, preseedAuths, "", 1)
a, err := asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model := a.(*asserts.Model)
// the default is just to accept the brand itself
c.Check(model.PreseedAuthority(), DeepEquals, []string{"brand-id1"})

encoded = strings.Replace(withTimestamp, preseedAuths, "preseed-authority:\n - foo\n - bar\n", 1)
a, err = asserts.Decode([]byte(encoded))
c.Assert(err, IsNil)
model = a.(*asserts.Model)
// the brand is always added implicitly
c.Check(model.PreseedAuthority(), DeepEquals, []string{"brand-id1", "foo", "bar"})
}

func (mods *modelSuite) TestDecodeKernelTrack(c *C) {
withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux=18\n", 1)
Expand Down Expand Up @@ -536,6 +556,9 @@ func (mods *modelSuite) TestDecodeInvalid(c *C) {
{serialAuths, "serial-authority:\n - 5_6\n", `"serial-authority" header must be a list of account ids`},
{sysUserAuths, "system-user-authority:\n a: 1\n", `"system-user-authority" header must be '\*' or a list of account ids`},
{sysUserAuths, "system-user-authority:\n - 5_6\n", `"system-user-authority" header must be '\*' or a list of account ids`},
{preseedAuths, "preseed-authority:\n a: 1\n", `"preseed-authority" header must be a list of account ids`},
{preseedAuths, "preseed-authority:\n - 5_6\n", `"preseed-authority" header must be a list of account ids`},
{preseedAuths, "preseed-authority: *\n", `"preseed-authority" header must be a list of account ids`},
{reqSnaps, "grade: dangerous\n", `cannot specify a grade for model without the extended snaps header`},
}

Expand Down Expand Up @@ -809,6 +832,7 @@ func (mods *modelSuite) testWithSnapsDecodeOK(c *C, modelRaw string, isClassic b

c.Check(model.SystemUserAuthority(), HasLen, 0)
c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1"})
c.Check(model.PreseedAuthority(), DeepEquals, []string{"brand-id1"})
}

func (mods *modelSuite) TestCore20ExplictBootBase(c *C) {
Expand Down
14 changes: 6 additions & 8 deletions asserts/preseed.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2022 Canonical Ltd
* Copyright (C) 2023 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 @@ -74,7 +74,7 @@ func (p *Preseed) Series() string {
return p.HeaderString("series")
}

// BrandID returns the brand identifier. Same as the authority id.
// BrandID returns the brand identifier.
func (p *Preseed) BrandID() string {
return p.HeaderString("brand-id")
}
Expand Down Expand Up @@ -186,13 +186,11 @@ func checkPreseedSnaps(snapList interface{}) ([]*PreseedSnap, error) {
}

func assemblePreseed(assert assertionBase) (Assertion, error) {
// authority must match the brand (signer is the brand)
err := checkAuthorityMatchesBrand(&assert)
if err != nil {
return nil, err
}
// because the authority-id and model-id can differ (as per the model),
// authority-id should be validated against allowed IDs when the preseed
// blob is being checked

_, err = checkModel(assert.headers)
_, err := checkModel(assert.headers)
if err != nil {
return nil, err
}
Expand Down
3 changes: 1 addition & 2 deletions asserts/preseed_test.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2022 Canonical Ltd
* Copyright (C) 2023 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 @@ -138,7 +138,6 @@ func (ps *preseedSuite) TestDecodeInvalid(c *C) {
{"model: baz-3000\n", "model: -\n", `"model" header contains invalid characters: "-"`},
{"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
{"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
{"brand-id: brand-id1\n", "brand-id: brand-id2\n", `authority-id and brand-id must match, preseed assertions are expected to be signed by the brand: "brand-id1" != "brand-id2"`},
{"system-label: 20220210\n", "system-label: \n", `"system-label" header should not be empty`},
{"system-label: 20220210\n", "system-label: -x\n", `"system-label" header contains invalid characters: "-x"`},
{ps.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
Expand Down
7 changes: 6 additions & 1 deletion seed/seed20.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-

/*
* Copyright (C) 2014-2022 Canonical Ltd
* Copyright (C) 2014-2023 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 @@ -837,6 +837,11 @@ func (s *seed20) LoadPreseedAssertion() (*asserts.Preseed, error) {
return nil, err
}
preseedAs := a.(*asserts.Preseed)

if !strutil.ListContains(model.PreseedAuthority(), preseedAs.AuthorityID()) {
return nil, fmt.Errorf("preseed authority-id %q is not allowed by the model", preseedAs.AuthorityID())
}

switch {
case preseedAs.SystemLabel() != sysLabel:
return nil, fmt.Errorf("preseed assertion system label %q doesn't match system label %q", preseedAs.SystemLabel(), sysLabel)
Expand Down
Loading

0 comments on commit 7c17cf2

Please sign in to comment.