mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
to have the information available when we seal capture classic in modeenv and bootchains as we do for other mode characteristics as for now we assume we don't want so support classic/core remodels some things need to be tested but it's best if the tests are added when we are actually looking at the full picture of installing classic systems with modes
876 lines
26 KiB
Go
876 lines
26 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2016-2022 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
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package asserts
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/snapcore/snapd/snap/channel"
|
|
"github.com/snapcore/snapd/snap/naming"
|
|
"github.com/snapcore/snapd/strutil"
|
|
)
|
|
|
|
// TODO: for ModelSnap
|
|
// * consider moving snap.Type out of snap and using it in ModelSnap
|
|
// but remember assertions use "core" (never "os") for TypeOS
|
|
// * consider having a first-class Presence type
|
|
|
|
// ModelSnap holds the details about a snap specified by a model assertion.
|
|
type ModelSnap struct {
|
|
Name string
|
|
SnapID string
|
|
// SnapType is one of: app|base|gadget|kernel|core, default is app
|
|
SnapType string
|
|
// Modes in which the snap must be made available
|
|
Modes []string
|
|
// DefaultChannel is the initial tracking channel,
|
|
// default is latest/stable in an extended model
|
|
DefaultChannel string
|
|
// PinnedTrack is a pinned track for the snap, if set DefaultChannel
|
|
// cannot be set at the same time (Core 18 models feature)
|
|
PinnedTrack string
|
|
// Presence is one of: required|optional
|
|
Presence string
|
|
}
|
|
|
|
// SnapName implements naming.SnapRef.
|
|
func (s *ModelSnap) SnapName() string {
|
|
return s.Name
|
|
}
|
|
|
|
// ID implements naming.SnapRef.
|
|
func (s *ModelSnap) ID() string {
|
|
return s.SnapID
|
|
}
|
|
|
|
type modelSnaps struct {
|
|
snapd *ModelSnap
|
|
base *ModelSnap
|
|
gadget *ModelSnap
|
|
kernel *ModelSnap
|
|
snapsNoEssential []*ModelSnap
|
|
}
|
|
|
|
func (ms *modelSnaps) list() (allSnaps []*ModelSnap, requiredWithEssentialSnaps []naming.SnapRef, numEssentialSnaps int) {
|
|
addSnap := func(snap *ModelSnap, essentialSnap int) {
|
|
if snap == nil {
|
|
return
|
|
}
|
|
numEssentialSnaps += essentialSnap
|
|
allSnaps = append(allSnaps, snap)
|
|
if snap.Presence == "required" {
|
|
requiredWithEssentialSnaps = append(requiredWithEssentialSnaps, snap)
|
|
}
|
|
}
|
|
|
|
addSnap(ms.snapd, 1)
|
|
addSnap(ms.kernel, 1)
|
|
addSnap(ms.base, 1)
|
|
addSnap(ms.gadget, 1)
|
|
for _, snap := range ms.snapsNoEssential {
|
|
addSnap(snap, 0)
|
|
}
|
|
return allSnaps, requiredWithEssentialSnaps, numEssentialSnaps
|
|
}
|
|
|
|
var (
|
|
essentialSnapModes = []string{"run", "ephemeral"}
|
|
defaultModes = []string{"run"}
|
|
)
|
|
|
|
func checkExtendedSnaps(extendedSnaps interface{}, base string, grade ModelGrade) (*modelSnaps, error) {
|
|
const wrongHeaderType = `"snaps" header must be a list of maps`
|
|
|
|
entries, ok := extendedSnaps.([]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf(wrongHeaderType)
|
|
}
|
|
|
|
var modelSnaps modelSnaps
|
|
seen := make(map[string]bool, len(entries))
|
|
seenIDs := make(map[string]string, len(entries))
|
|
|
|
for _, entry := range entries {
|
|
snap, ok := entry.(map[string]interface{})
|
|
if !ok {
|
|
return nil, fmt.Errorf(wrongHeaderType)
|
|
}
|
|
modelSnap, err := checkModelSnap(snap, grade)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if seen[modelSnap.Name] {
|
|
return nil, fmt.Errorf("cannot list the same snap %q multiple times", modelSnap.Name)
|
|
}
|
|
seen[modelSnap.Name] = true
|
|
// at this time we do not support parallel installing
|
|
// from model/seed
|
|
if snapID := modelSnap.SnapID; snapID != "" {
|
|
if underName := seenIDs[snapID]; underName != "" {
|
|
return nil, fmt.Errorf("cannot specify the same snap id %q multiple times, specified for snaps %q and %q", snapID, underName, modelSnap.Name)
|
|
}
|
|
seenIDs[snapID] = modelSnap.Name
|
|
}
|
|
|
|
essential := false
|
|
switch {
|
|
case modelSnap.SnapType == "snapd":
|
|
// TODO: allow to be explicit only in grade: dangerous?
|
|
essential = true
|
|
if modelSnaps.snapd != nil {
|
|
return nil, fmt.Errorf("cannot specify multiple snapd snaps: %q and %q", modelSnaps.snapd.Name, modelSnap.Name)
|
|
}
|
|
modelSnaps.snapd = modelSnap
|
|
case modelSnap.SnapType == "kernel":
|
|
essential = true
|
|
if modelSnaps.kernel != nil {
|
|
return nil, fmt.Errorf("cannot specify multiple kernel snaps: %q and %q", modelSnaps.kernel.Name, modelSnap.Name)
|
|
}
|
|
modelSnaps.kernel = modelSnap
|
|
case modelSnap.SnapType == "gadget":
|
|
essential = true
|
|
if modelSnaps.gadget != nil {
|
|
return nil, fmt.Errorf("cannot specify multiple gadget snaps: %q and %q", modelSnaps.gadget.Name, modelSnap.Name)
|
|
}
|
|
modelSnaps.gadget = modelSnap
|
|
case modelSnap.Name == base:
|
|
essential = true
|
|
if modelSnap.SnapType != "base" {
|
|
return nil, fmt.Errorf(`boot base %q must specify type "base", not %q`, base, modelSnap.SnapType)
|
|
}
|
|
modelSnaps.base = modelSnap
|
|
}
|
|
|
|
if essential {
|
|
if len(modelSnap.Modes) != 0 || modelSnap.Presence != "" {
|
|
return nil, fmt.Errorf("essential snaps are always available, cannot specify modes or presence for snap %q", modelSnap.Name)
|
|
}
|
|
modelSnap.Modes = essentialSnapModes
|
|
}
|
|
|
|
if len(modelSnap.Modes) == 0 {
|
|
modelSnap.Modes = defaultModes
|
|
}
|
|
if modelSnap.Presence == "" {
|
|
modelSnap.Presence = "required"
|
|
}
|
|
|
|
if !essential {
|
|
modelSnaps.snapsNoEssential = append(modelSnaps.snapsNoEssential, modelSnap)
|
|
}
|
|
}
|
|
|
|
return &modelSnaps, nil
|
|
}
|
|
|
|
var (
|
|
validSnapTypes = []string{"app", "base", "gadget", "kernel", "core", "snapd"}
|
|
validSnapMode = regexp.MustCompile("^[a-z][-a-z]+$")
|
|
validSnapPresences = []string{"required", "optional"}
|
|
)
|
|
|
|
func checkModelSnap(snap map[string]interface{}, grade ModelGrade) (*ModelSnap, error) {
|
|
name, err := checkNotEmptyStringWhat(snap, "name", "of snap")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := naming.ValidateSnap(name); err != nil {
|
|
return nil, fmt.Errorf("invalid snap name %q", name)
|
|
}
|
|
|
|
what := fmt.Sprintf("of snap %q", name)
|
|
|
|
var snapID string
|
|
_, ok := snap["id"]
|
|
if ok {
|
|
var err error
|
|
snapID, err = checkStringMatchesWhat(snap, "id", what, naming.ValidSnapID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
// snap ids are optional with grade dangerous to allow working
|
|
// with local/not pushed yet to the store snaps
|
|
if grade != ModelDangerous {
|
|
return nil, fmt.Errorf(`"id" %s is mandatory for %s grade model`, what, grade)
|
|
}
|
|
}
|
|
|
|
typ, err := checkOptionalStringWhat(snap, "type", what)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if typ == "" {
|
|
typ = "app"
|
|
}
|
|
if !strutil.ListContains(validSnapTypes, typ) {
|
|
return nil, fmt.Errorf("type of snap %q must be one of %s", name, strings.Join(validSnapTypes, "|"))
|
|
}
|
|
|
|
modes, err := checkStringListInMap(snap, "modes", fmt.Sprintf("%q %s", "modes", what), validSnapMode)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
defaultChannel, err := checkOptionalStringWhat(snap, "default-channel", what)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if defaultChannel == "" {
|
|
defaultChannel = "latest/stable"
|
|
}
|
|
defCh, err := channel.ParseVerbatim(defaultChannel, "-")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid default channel for snap %q: %v", name, err)
|
|
}
|
|
if defCh.Track == "" {
|
|
return nil, fmt.Errorf("default channel for snap %q must specify a track", name)
|
|
}
|
|
|
|
presence, err := checkOptionalStringWhat(snap, "presence", what)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if presence != "" && !strutil.ListContains(validSnapPresences, presence) {
|
|
return nil, fmt.Errorf("presence of snap %q must be one of required|optional", name)
|
|
}
|
|
|
|
return &ModelSnap{
|
|
Name: name,
|
|
SnapID: snapID,
|
|
SnapType: typ,
|
|
Modes: modes, // can be empty
|
|
DefaultChannel: defaultChannel,
|
|
Presence: presence, // can be empty
|
|
}, nil
|
|
}
|
|
|
|
// unextended case support
|
|
|
|
func checkSnapWithTrack(headers map[string]interface{}, which string) (*ModelSnap, error) {
|
|
_, ok := headers[which]
|
|
if !ok {
|
|
return nil, nil
|
|
}
|
|
value, ok := headers[which].(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf(`%q header must be a string`, which)
|
|
}
|
|
l := strings.SplitN(value, "=", 2)
|
|
|
|
name := l[0]
|
|
track := ""
|
|
if err := validateSnapName(name, which); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(l) > 1 {
|
|
track = l[1]
|
|
if strings.Count(track, "/") != 0 {
|
|
return nil, fmt.Errorf(`%q channel selector must be a track name only`, which)
|
|
}
|
|
channelRisks := []string{"stable", "candidate", "beta", "edge"}
|
|
if strutil.ListContains(channelRisks, track) {
|
|
return nil, fmt.Errorf(`%q channel selector must be a track name`, which)
|
|
}
|
|
}
|
|
|
|
return &ModelSnap{
|
|
Name: name,
|
|
SnapType: which,
|
|
Modes: defaultModes,
|
|
PinnedTrack: track,
|
|
Presence: "required",
|
|
}, nil
|
|
}
|
|
|
|
func validateSnapName(name string, headerName string) error {
|
|
if err := naming.ValidateSnap(name); err != nil {
|
|
return fmt.Errorf("invalid snap name in %q header: %s", headerName, name)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func checkRequiredSnap(name string, headerName string, snapType string) (*ModelSnap, error) {
|
|
if err := validateSnapName(name, headerName); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &ModelSnap{
|
|
Name: name,
|
|
SnapType: snapType,
|
|
Modes: defaultModes,
|
|
Presence: "required",
|
|
}, nil
|
|
}
|
|
|
|
// ModelGrade characterizes the security of the model which then
|
|
// controls related policy.
|
|
type ModelGrade string
|
|
|
|
const (
|
|
ModelGradeUnset ModelGrade = "unset"
|
|
// ModelSecured implies mandatory full disk encryption and secure boot.
|
|
ModelSecured ModelGrade = "secured"
|
|
// ModelSigned implies all seed snaps are signed and mentioned
|
|
// in the model, i.e. no unasserted or extra snaps.
|
|
ModelSigned ModelGrade = "signed"
|
|
// ModelDangerous allows unasserted snaps and extra snaps.
|
|
ModelDangerous ModelGrade = "dangerous"
|
|
)
|
|
|
|
// StorageSafety characterizes the requested storage safety of
|
|
// the model which then controls what encryption is used
|
|
type StorageSafety string
|
|
|
|
const (
|
|
StorageSafetyUnset StorageSafety = "unset"
|
|
// StorageSafetyEncrypted implies mandatory full disk encryption.
|
|
StorageSafetyEncrypted StorageSafety = "encrypted"
|
|
// StorageSafetyPreferEncrypted implies full disk
|
|
// encryption when the system supports it.
|
|
StorageSafetyPreferEncrypted StorageSafety = "prefer-encrypted"
|
|
// StorageSafetyPreferUnencrypted implies no full disk
|
|
// encryption by default even if the system supports
|
|
// encryption.
|
|
StorageSafetyPreferUnencrypted StorageSafety = "prefer-unencrypted"
|
|
)
|
|
|
|
var validStorageSafeties = []string{string(StorageSafetyEncrypted), string(StorageSafetyPreferEncrypted), string(StorageSafetyPreferUnencrypted)}
|
|
|
|
var validModelGrades = []string{string(ModelSecured), string(ModelSigned), string(ModelDangerous)}
|
|
|
|
// gradeToCode encodes grades into 32 bits, trying to be slightly future-proof:
|
|
// * lower 16 bits are reserved
|
|
// * in the higher bits use the sequence 1, 8, 16 to have some space
|
|
// to possibly add new grades in between
|
|
var gradeToCode = map[ModelGrade]uint32{
|
|
ModelGradeUnset: 0,
|
|
ModelDangerous: 0x10000,
|
|
ModelSigned: 0x80000,
|
|
ModelSecured: 0x100000,
|
|
// reserved by secboot to measure classic models
|
|
// "ClassicModelGradeMask": 0x80000000
|
|
}
|
|
|
|
// Code returns a bit representation of the grade, for example for
|
|
// measuring it in a full disk encryption implementation.
|
|
func (mg ModelGrade) Code() uint32 {
|
|
code, ok := gradeToCode[mg]
|
|
if !ok {
|
|
panic(fmt.Sprintf("unknown model grade: %s", mg))
|
|
}
|
|
return code
|
|
}
|
|
|
|
// Model holds a model assertion, which is a statement by a brand
|
|
// about the properties of a device model.
|
|
type Model struct {
|
|
assertionBase
|
|
classic bool
|
|
|
|
baseSnap *ModelSnap
|
|
gadgetSnap *ModelSnap
|
|
kernelSnap *ModelSnap
|
|
|
|
grade ModelGrade
|
|
|
|
storageSafety StorageSafety
|
|
|
|
allSnaps []*ModelSnap
|
|
// consumers of this info should care only about snap identity =>
|
|
// snapRef
|
|
requiredWithEssentialSnaps []naming.SnapRef
|
|
numEssentialSnaps int
|
|
|
|
serialAuthority []string
|
|
sysUserAuthority []string
|
|
timestamp time.Time
|
|
}
|
|
|
|
// BrandID returns the brand identifier. Same as the authority id.
|
|
func (mod *Model) BrandID() string {
|
|
return mod.HeaderString("brand-id")
|
|
}
|
|
|
|
// Model returns the model name identifier.
|
|
func (mod *Model) Model() string {
|
|
return mod.HeaderString("model")
|
|
}
|
|
|
|
// DisplayName returns the human-friendly name of the model or
|
|
// falls back to Model if this was not set.
|
|
func (mod *Model) DisplayName() string {
|
|
display := mod.HeaderString("display-name")
|
|
if display == "" {
|
|
return mod.Model()
|
|
}
|
|
return display
|
|
}
|
|
|
|
// Series returns the series of the core software the model uses.
|
|
func (mod *Model) Series() string {
|
|
return mod.HeaderString("series")
|
|
}
|
|
|
|
// Classic returns whether the model is a classic system.
|
|
func (mod *Model) Classic() bool {
|
|
return mod.classic
|
|
}
|
|
|
|
// Distribution returns the linux distro specified in the model.
|
|
func (mod *Model) Distribution() string {
|
|
return mod.HeaderString("distribution")
|
|
}
|
|
|
|
// Architecture returns the architecture the model is based on.
|
|
func (mod *Model) Architecture() string {
|
|
return mod.HeaderString("architecture")
|
|
}
|
|
|
|
// Grade returns the stability grade of the model. Will be ModelGradeUnset
|
|
// for Core 16/18 models.
|
|
func (mod *Model) Grade() ModelGrade {
|
|
return mod.grade
|
|
}
|
|
|
|
// StorageSafety returns the storage safety for the model. Will be
|
|
// StorageSafetyUnset for Core 16/18 models.
|
|
func (mod *Model) StorageSafety() StorageSafety {
|
|
return mod.storageSafety
|
|
}
|
|
|
|
// GadgetSnap returns the details of the gadget snap the model uses.
|
|
func (mod *Model) GadgetSnap() *ModelSnap {
|
|
return mod.gadgetSnap
|
|
}
|
|
|
|
// Gadget returns the gadget snap the model uses.
|
|
func (mod *Model) Gadget() string {
|
|
if mod.gadgetSnap == nil {
|
|
return ""
|
|
}
|
|
return mod.gadgetSnap.Name
|
|
}
|
|
|
|
// GadgetTrack returns the gadget track the model uses.
|
|
// XXX this should go away
|
|
func (mod *Model) GadgetTrack() string {
|
|
if mod.gadgetSnap == nil {
|
|
return ""
|
|
}
|
|
return mod.gadgetSnap.PinnedTrack
|
|
}
|
|
|
|
// KernelSnap returns the details of the kernel snap the model uses.
|
|
func (mod *Model) KernelSnap() *ModelSnap {
|
|
return mod.kernelSnap
|
|
}
|
|
|
|
// Kernel returns the kernel snap the model uses.
|
|
// XXX this should go away
|
|
func (mod *Model) Kernel() string {
|
|
if mod.kernelSnap == nil {
|
|
return ""
|
|
}
|
|
return mod.kernelSnap.Name
|
|
}
|
|
|
|
// KernelTrack returns the kernel track the model uses.
|
|
// XXX this should go away
|
|
func (mod *Model) KernelTrack() string {
|
|
if mod.kernelSnap == nil {
|
|
return ""
|
|
}
|
|
return mod.kernelSnap.PinnedTrack
|
|
}
|
|
|
|
// Base returns the base snap the model uses.
|
|
func (mod *Model) Base() string {
|
|
return mod.HeaderString("base")
|
|
}
|
|
|
|
// BaseSnap returns the details of the base snap the model uses.
|
|
func (mod *Model) BaseSnap() *ModelSnap {
|
|
return mod.baseSnap
|
|
}
|
|
|
|
// Store returns the snap store the model uses.
|
|
func (mod *Model) Store() string {
|
|
return mod.HeaderString("store")
|
|
}
|
|
|
|
// RequiredNoEssentialSnaps returns the snaps that must be installed at all times and cannot be removed for this model, excluding the essential snaps (gadget, kernel, boot base, snapd).
|
|
func (mod *Model) RequiredNoEssentialSnaps() []naming.SnapRef {
|
|
return mod.requiredWithEssentialSnaps[mod.numEssentialSnaps:]
|
|
}
|
|
|
|
// RequiredWithEssentialSnaps returns the snaps that must be installed at all times and cannot be removed for this model, including any essential snaps (gadget, kernel, boot base, snapd).
|
|
func (mod *Model) RequiredWithEssentialSnaps() []naming.SnapRef {
|
|
return mod.requiredWithEssentialSnaps
|
|
}
|
|
|
|
// EssentialSnaps returns all essential snaps explicitly mentioned by
|
|
// the model.
|
|
// They are always returned according to this order with some skipped
|
|
// if not mentioned: snapd, kernel, boot base, gadget.
|
|
func (mod *Model) EssentialSnaps() []*ModelSnap {
|
|
return mod.allSnaps[:mod.numEssentialSnaps]
|
|
}
|
|
|
|
// SnapsWithoutEssential returns all the snaps listed by the model
|
|
// without any of the essential snaps (as returned by EssentialSnaps).
|
|
// They are returned in the order of mention by the model.
|
|
func (mod *Model) SnapsWithoutEssential() []*ModelSnap {
|
|
return mod.allSnaps[mod.numEssentialSnaps:]
|
|
}
|
|
|
|
// SerialAuthority returns the authority ids that are accepted as
|
|
// signers for serial assertions for this model. It always includes the
|
|
// brand of the model.
|
|
func (mod *Model) SerialAuthority() []string {
|
|
return mod.serialAuthority
|
|
}
|
|
|
|
// SystemUserAuthority returns the authority ids that are accepted as
|
|
// signers of system-user assertions for this model. Empty list means
|
|
// any, otherwise it always includes the brand of the model.
|
|
func (mod *Model) SystemUserAuthority() []string {
|
|
return mod.sysUserAuthority
|
|
}
|
|
|
|
// Timestamp returns the time when the model assertion was issued.
|
|
func (mod *Model) Timestamp() time.Time {
|
|
return mod.timestamp
|
|
}
|
|
|
|
// Implement further consistency checks.
|
|
func (mod *Model) checkConsistency(db RODatabase, acck *AccountKey) error {
|
|
// TODO: double check trust level of authority depending on class and possibly allowed-modes
|
|
return nil
|
|
}
|
|
|
|
// expected interface is implemented
|
|
var _ consistencyChecker = (*Model)(nil)
|
|
|
|
// limit model to only lowercase for now
|
|
var validModel = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$")
|
|
|
|
func checkModel(headers map[string]interface{}) (string, error) {
|
|
s, err := checkStringMatches(headers, "model", validModel)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// TODO: support the concept of case insensitive/preserving string headers
|
|
if strings.ToLower(s) != s {
|
|
return "", fmt.Errorf(`"model" header cannot contain uppercase letters`)
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func checkAuthorityMatchesBrand(a Assertion) error {
|
|
typeName := a.Type().Name
|
|
authorityID := a.AuthorityID()
|
|
brand := a.HeaderString("brand-id")
|
|
if brand != authorityID {
|
|
return fmt.Errorf("authority-id and brand-id must match, %s assertions are expected to be signed by the brand: %q != %q", typeName, authorityID, brand)
|
|
}
|
|
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) {
|
|
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 == "*" {
|
|
return nil, nil
|
|
}
|
|
case []interface{}:
|
|
lst, err := checkStringListMatches(headers, name, validAccountID)
|
|
if err == nil {
|
|
if !strutil.ListContains(lst, brandID) {
|
|
lst = append(ids, lst...)
|
|
}
|
|
return lst, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("%q header must be '*' or a list of account ids", name)
|
|
}
|
|
|
|
var (
|
|
modelMandatory = []string{"architecture", "gadget", "kernel"}
|
|
extendedMandatory = []string{"architecture", "base"}
|
|
extendedSnapsConflicting = []string{"gadget", "kernel", "required-snaps"}
|
|
classicModelOptional = []string{"architecture", "gadget"}
|
|
|
|
// The distribution header must be a valid ID according to
|
|
// https://www.freedesktop.org/software/systemd/man/os-release.html#ID=
|
|
validDistribution = regexp.MustCompile(`^[a-z0-9._-]*$`)
|
|
)
|
|
|
|
func assembleModel(assert assertionBase) (Assertion, error) {
|
|
err := checkAuthorityMatchesBrand(&assert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = checkModel(assert.headers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
classic, err := checkOptionalBool(assert.headers, "classic")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Core 20 extended snaps header
|
|
extendedSnaps, extended := assert.headers["snaps"]
|
|
if extended {
|
|
for _, conflicting := range extendedSnapsConflicting {
|
|
if _, ok := assert.headers[conflicting]; ok {
|
|
return nil, fmt.Errorf("cannot specify separate %q header once using the extended snaps header", conflicting)
|
|
}
|
|
}
|
|
} else {
|
|
if _, ok := assert.headers["grade"]; ok {
|
|
return nil, fmt.Errorf("cannot specify a grade for model without the extended snaps header")
|
|
}
|
|
if _, ok := assert.headers["storage-safety"]; ok {
|
|
return nil, fmt.Errorf("cannot specify storage-safety for model without the extended snaps header")
|
|
}
|
|
}
|
|
|
|
if classic && !extended {
|
|
if _, ok := assert.headers["kernel"]; ok {
|
|
return nil, fmt.Errorf("cannot specify a kernel with a non-extended classic model")
|
|
}
|
|
if _, ok := assert.headers["base"]; ok {
|
|
return nil, fmt.Errorf("cannot specify a base with a non-extended classic model")
|
|
}
|
|
}
|
|
|
|
// distribution mandatory for classic with extended snaps, not
|
|
// allowed otherwise.
|
|
if classic && extended {
|
|
_, err := checkStringMatches(assert.headers, "distribution", validDistribution)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%v, see distribution ID in os-release spec", err)
|
|
}
|
|
} else if _, ok := assert.headers["distribution"]; ok {
|
|
return nil, fmt.Errorf("cannot specify distribution for model unless it is classic and has an extended snaps header")
|
|
}
|
|
|
|
checker := checkNotEmptyString
|
|
toCheck := modelMandatory
|
|
if extended {
|
|
toCheck = extendedMandatory
|
|
} else if classic {
|
|
checker = checkOptionalString
|
|
toCheck = classicModelOptional
|
|
}
|
|
|
|
for _, h := range toCheck {
|
|
if _, err := checker(assert.headers, h); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// base, if provided, must be a valid snap name too
|
|
var baseSnap *ModelSnap
|
|
base, err := checkOptionalString(assert.headers, "base")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if base != "" {
|
|
baseSnap, err = checkRequiredSnap(base, "base", "base")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// store is optional but must be a string, defaults to the ubuntu store
|
|
if _, err = checkOptionalString(assert.headers, "store"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// display-name is optional but must be a string
|
|
if _, err = checkOptionalString(assert.headers, "display-name"); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var modSnaps *modelSnaps
|
|
grade := ModelGradeUnset
|
|
storageSafety := StorageSafetyUnset
|
|
if extended {
|
|
gradeStr, err := checkOptionalString(assert.headers, "grade")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if gradeStr != "" && !strutil.ListContains(validModelGrades, gradeStr) {
|
|
return nil, fmt.Errorf("grade for model must be %s, not %q", strings.Join(validModelGrades, "|"), gradeStr)
|
|
}
|
|
grade = ModelSigned
|
|
if gradeStr != "" {
|
|
grade = ModelGrade(gradeStr)
|
|
}
|
|
|
|
storageSafetyStr, err := checkOptionalString(assert.headers, "storage-safety")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if storageSafetyStr != "" && !strutil.ListContains(validStorageSafeties, storageSafetyStr) {
|
|
return nil, fmt.Errorf("storage-safety for model must be %s, not %q", strings.Join(validStorageSafeties, "|"), storageSafetyStr)
|
|
}
|
|
if storageSafetyStr != "" {
|
|
storageSafety = StorageSafety(storageSafetyStr)
|
|
} else {
|
|
if grade == ModelSecured {
|
|
storageSafety = StorageSafetyEncrypted
|
|
} else {
|
|
storageSafety = StorageSafetyPreferEncrypted
|
|
}
|
|
}
|
|
|
|
if grade == ModelSecured && storageSafety != StorageSafetyEncrypted {
|
|
return nil, fmt.Errorf(`secured grade model must not have storage-safety overridden, only "encrypted" is valid`)
|
|
}
|
|
|
|
modSnaps, err = checkExtendedSnaps(extendedSnaps, base, grade)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if modSnaps.gadget == nil {
|
|
return nil, fmt.Errorf(`one "snaps" header entry must specify the model gadget`)
|
|
}
|
|
if modSnaps.kernel == nil {
|
|
return nil, fmt.Errorf(`one "snaps" header entry must specify the model kernel`)
|
|
}
|
|
|
|
if modSnaps.base == nil {
|
|
// complete with defaults,
|
|
// the assumption is that base names are very stable
|
|
// essentially fixed
|
|
modSnaps.base = baseSnap
|
|
snapID := naming.WellKnownSnapID(modSnaps.base.Name)
|
|
if snapID == "" && grade != ModelDangerous {
|
|
return nil, fmt.Errorf(`cannot specify not well-known base %q without a corresponding "snaps" header entry`, modSnaps.base.Name)
|
|
}
|
|
modSnaps.base.SnapID = snapID
|
|
modSnaps.base.Modes = essentialSnapModes
|
|
modSnaps.base.DefaultChannel = "latest/stable"
|
|
}
|
|
} else {
|
|
modSnaps = &modelSnaps{
|
|
base: baseSnap,
|
|
}
|
|
// kernel/gadget must be valid snap names and can have (optional) tracks
|
|
// - validate those
|
|
modSnaps.kernel, err = checkSnapWithTrack(assert.headers, "kernel")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
modSnaps.gadget, err = checkSnapWithTrack(assert.headers, "gadget")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// required snap must be valid snap names
|
|
reqSnaps, err := checkStringList(assert.headers, "required-snaps")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, name := range reqSnaps {
|
|
reqSnap, err := checkRequiredSnap(name, "required-snaps", "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
modSnaps.snapsNoEssential = append(modSnaps.snapsNoEssential, reqSnap)
|
|
}
|
|
}
|
|
|
|
brandID := assert.HeaderString("brand-id")
|
|
|
|
serialAuthority, err := checkOptionalSerialAuthority(assert.headers, brandID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sysUserAuthority, err := checkOptionalSystemUserAuthority(assert.headers, brandID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allSnaps, requiredWithEssentialSnaps, numEssentialSnaps := modSnaps.list()
|
|
|
|
// NB:
|
|
// * core is not supported at this time, it defaults to ubuntu-core
|
|
// in prepare-image until rename and/or introduction of the header.
|
|
// * some form of allowed-modes, class are postponed,
|
|
//
|
|
// prepare-image takes care of not allowing them for now
|
|
|
|
// ignore extra headers and non-empty body for future compatibility
|
|
return &Model{
|
|
assertionBase: assert,
|
|
classic: classic,
|
|
baseSnap: modSnaps.base,
|
|
gadgetSnap: modSnaps.gadget,
|
|
kernelSnap: modSnaps.kernel,
|
|
grade: grade,
|
|
storageSafety: storageSafety,
|
|
allSnaps: allSnaps,
|
|
requiredWithEssentialSnaps: requiredWithEssentialSnaps,
|
|
numEssentialSnaps: numEssentialSnaps,
|
|
serialAuthority: serialAuthority,
|
|
sysUserAuthority: sysUserAuthority,
|
|
timestamp: timestamp,
|
|
}, nil
|
|
}
|