// -*- Mode: Go; indent-tabs-mode: t -*- /* * 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 * 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 . * */ package asserts import ( "fmt" "regexp" "strings" "time" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap/channel" "github.com/snapcore/snapd/snap/naming" "github.com/snapcore/snapd/strutil" ) // ModelComponent holds details for components specified by a model assertion. type ModelComponent struct { // Presence can be optional or required Presence string // Modes is an optional list of modes, which must be a subset // of the ones for the snap Modes []string } // 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 // Classic indicates that this classic snap is intentionally // included in a classic model Classic bool // Components is a map of component names to ModelComponent Components map[string]ModelComponent } // 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, modelIsClassic bool) (*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, base, grade, modelIsClassic) 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 } switch { case modelSnap.SnapType == "snapd": // TODO: allow to be explicit only in grade: dangerous? 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": 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": 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: if modelSnap.SnapType != "base" { return nil, fmt.Errorf(`boot base %q must specify type "base", not %q`, base, modelSnap.SnapType) } modelSnaps.base = modelSnap } if !isEssentialSnap(modelSnap.Name, modelSnap.SnapType, base) { 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 isEssentialSnap(snapName, snapType, modelBase string) bool { switch snapType { case "snapd", "kernel", "gadget": return true } if snapName == modelBase { return true } return false } func checkModesForSnap(snap map[string]interface{}, isEssential bool, what string) ([]string, error) { modes, err := checkStringListInMap(snap, "modes", fmt.Sprintf("%q %s", "modes", what), validSnapMode) if err != nil { return nil, err } if isEssential { if len(modes) != 0 { return nil, fmt.Errorf("essential snaps are always available, cannot specify modes %s", what) } modes = essentialSnapModes } if len(modes) == 0 { modes = defaultModes } return modes, nil } func checkModelSnap(snap map[string]interface{}, modelBase string, grade ModelGrade, modelIsClassic bool) (*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, "|")) } 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) } essential := isEssentialSnap(name, typ, modelBase) if essential && presence != "" { return nil, fmt.Errorf("essential snaps are always available, cannot specify presence for snap %q", name) } if presence == "" { presence = "required" } modes, err := checkModesForSnap(snap, essential, what) 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) } isClassic, err := checkOptionalBoolWhat(snap, "classic", what) if err != nil { return nil, err } if isClassic && !modelIsClassic { return nil, fmt.Errorf("snap %q cannot be classic in non-classic model", name) } if isClassic && typ != "app" { return nil, fmt.Errorf("snap %q cannot be classic with type %q instead of app", name, typ) } if isClassic && (len(modes) != 1 || modes[0] != "run") { return nil, fmt.Errorf("classic snap %q not allowed outside of run mode: %v", name, modes) } components, err := checkComponentsForMaps(snap, modes, what) if err != nil { return nil, err } return &ModelSnap{ Name: name, SnapID: snapID, SnapType: typ, Modes: modes, // can be empty DefaultChannel: defaultChannel, Presence: presence, // can be empty Classic: isClassic, Components: components, // can be empty }, nil } // This is what we expect for components: /** snaps: - name: ... presence: "optional"|"required" # optional, defaults to "required" modes: [] # list of modes components: # optional : presence: "optional"|"required" modes: [] # list of modes, optional # must be a subset of snap modes # defaults to the same modes # as the snap : "required"|"optional" # presence, shortcut syntax **/ func checkComponentsForMaps(m map[string]interface{}, validModes []string, what string) (map[string]ModelComponent, error) { const compsField = "components" value, ok := m[compsField] if !ok { return nil, nil } comps, ok := value.(map[string]interface{}) if !ok { return nil, fmt.Errorf("%q %s must be a map from strings to components", compsField, what) } res := make(map[string]ModelComponent, len(comps)) for name, comp := range comps { // Name of component follows the same rules as snap components if err := naming.ValidateSnap(name); err != nil { return nil, fmt.Errorf("invalid component name %s", name) } // "comp: required|optional" case compWhat := fmt.Sprintf("of component %q %s", name, what) presence, ok := comp.(string) if ok { if !strutil.ListContains(validSnapPresences, presence) { return nil, fmt.Errorf("presence %s must be one of required|optional", compWhat) } res[name] = ModelComponent{Presence: presence, Modes: append([]string(nil), validModes...)} continue } // try map otherwise compFields, ok := comp.(map[string]interface{}) if !ok { return nil, fmt.Errorf("%s must be a map of strings to components or one of required|optional", compWhat) } // Error out if unexpected entry for key := range compFields { if !strutil.ListContains([]string{"presence", "modes"}, key) { return nil, fmt.Errorf("entry %q %s is unknown", key, compWhat) } } presence, err := checkNotEmptyStringWhat(compFields, "presence", compWhat) if err != nil { return nil, err } if !strutil.ListContains(validSnapPresences, presence) { return nil, fmt.Errorf("presence %s must be one of required|optional", compWhat) } modes, err := checkStringListInMap(compFields, "modes", fmt.Sprintf("modes %s", compWhat), validSnapMode) if err != nil { return nil, err } if len(modes) == 0 { modes = append([]string(nil), validModes...) } else { for _, m := range modes { if !strutil.ListContains(validModes, m) { return nil, fmt.Errorf("mode %q %s is incompatible with the snap modes", m, compWhat) } } } res[name] = ModelComponent{Presence: presence, Modes: modes} } return res, 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 } type ModelValidationSetMode string const ( ModelValidationSetModePreferEnforced ModelValidationSetMode = "prefer-enforce" ModelValidationSetModeEnforced ModelValidationSetMode = "enforce" ) var validModelValidationSetModes = []string{ string(ModelValidationSetModePreferEnforced), string(ModelValidationSetModeEnforced), } // ModelValidationSet represents a reference to a validation set assertion. // The structure also describes how the validation set will be applied // to the device, and whether the validation set should be pinned to // a specific sequence. type ModelValidationSet struct { // AccountID is the account ID the validation set originates from. // If this was not explicitly set in the stanza, this will instead // be set to the brand ID. AccountID string // Name is the name of the validation set from the account ID. Name string // Sequence, if non-zero, specifies that the validation set should be // pinned at this sequence number. Sequence int // Mode is the enforcement mode the validation set should be applied with. Mode ModelValidationSetMode } // SequenceKey returns the sequence key for this validation set. func (mvs *ModelValidationSet) SequenceKey() string { return vsSequenceKey(release.Series, mvs.AccountID, mvs.Name) } func (mvs *ModelValidationSet) AtSequence() *AtSequence { return &AtSequence{ Type: ValidationSetType, SequenceKey: []string{release.Series, mvs.AccountID, mvs.Name}, Sequence: mvs.Sequence, Pinned: mvs.Sequence > 0, Revision: RevisionNotKnown, } } // 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 validationSets []*ModelValidationSet serialAuthority []string sysUserAuthority []string preseedAuthority []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:] } // AllSnaps returns all the snaps listed by the model, across all modes. // Essential snaps are at the front of the slice, followed by the non-essential // snaps. The essential snaps follow the same order as returned by // EssentialSnaps. The non-essential snaps are returned in the order they are // mentioned in the model. func (mod *Model) AllSnaps() []*ModelSnap { return mod.allSnaps } // ValidationSets returns all the validation-sets listed by the model. func (mod *Model) ValidationSets() []*ModelValidationSet { return mod.validationSets } // 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 } // 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 } // 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 checkOptionalAuthority(headers map[string]interface{}, name string, brandID string, acceptsWildcard bool) ([]string, error) { ids := []string{brandID} v, ok := headers[name] if !ok { return ids, nil } switch x := v.(type) { case string: if acceptsWildcard && 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 } } 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) { accountID, err := checkOptionalStringWhat(headers, "account-id", what) if err != nil { return "", err } // default to brand ID if account ID is not provided if accountID == "" { return brandID, nil } return accountID, nil } // checkOptionalModelValidationSetSequence reads the optional 'sequence' member, if // not set, returns 0 as this means unpinned. Unfortunately we are not able // to reuse `checkSequence` as it operates inside different parameters. func checkOptionalModelValidationSetSequence(headers map[string]interface{}, what string) (int, error) { // Default to 0 when the sequence header is not present if _, ok := headers["sequence"]; !ok { return 0, nil } seq, err := checkIntWhat(headers, "sequence", what) if err != nil { return 0, err } // If sequence is provided, only accept positive values above 0 if seq <= 0 { return 0, fmt.Errorf("\"sequence\" %s must be larger than 0 or left unspecified (meaning tracking latest)", what) } return seq, nil } func checkModelValidationSetMode(headers map[string]interface{}, what string) (ModelValidationSetMode, error) { modeStr, err := checkNotEmptyStringWhat(headers, "mode", what) if err != nil { return "", err } if modeStr != "" && !strutil.ListContains(validModelValidationSetModes, modeStr) { return "", fmt.Errorf("\"mode\" %s must be %s, not %q", what, strings.Join(validModelValidationSetModes, "|"), modeStr) } return ModelValidationSetMode(modeStr), nil } func checkModelValidationSet(headers map[string]interface{}, brandID string) (*ModelValidationSet, error) { name, err := checkStringMatchesWhat(headers, "name", "of validation-set", validValidationSetName) if err != nil { return nil, err } what := fmt.Sprintf("of validation-set %q", name) accountID, err := checkModelValidationSetAccountID(headers, what, brandID) if err != nil { return nil, err } what = fmt.Sprintf("of validation-set \"%s/%s\"", accountID, name) seq, err := checkOptionalModelValidationSetSequence(headers, what) if err != nil { return nil, err } mode, err := checkModelValidationSetMode(headers, what) if err != nil { return nil, err } return &ModelValidationSet{ AccountID: accountID, Name: name, Sequence: seq, Mode: mode, }, nil } func checkOptionalModelValidationSets(headers map[string]interface{}, brandID string) ([]*ModelValidationSet, error) { valSets, ok := headers["validation-sets"] if !ok { return nil, nil } entries, ok := valSets.([]interface{}) if !ok { return nil, fmt.Errorf(`"validation-sets" must be a list of validation sets`) } vss := make([]*ModelValidationSet, len(entries)) seen := make(map[string]bool, len(entries)) for i, entry := range entries { data, ok := entry.(map[string]interface{}) if !ok { return nil, fmt.Errorf(`entry in "validation-sets" is not a valid validation-set`) } vs, err := checkModelValidationSet(data, brandID) if err != nil { return nil, err } vsKey := fmt.Sprintf("%s/%s", vs.AccountID, vs.Name) if seen[vsKey] { return nil, fmt.Errorf("cannot add validation set %q twice", vsKey) } vss[i] = vs seen[vsKey] = true } return vss, nil } 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, classic) if err != nil { return nil, err } hasKernel := modSnaps.kernel != nil hasGadget := modSnaps.gadget != nil if !classic { if !hasGadget { return nil, fmt.Errorf(`one "snaps" header entry must specify the model gadget`) } if !hasKernel { return nil, fmt.Errorf(`one "snaps" header entry must specify the model kernel`) } } else { if hasKernel && !hasGadget { return nil, fmt.Errorf("cannot specify a kernel in an extended classic model without a model gadget") } } 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 } preseedAuthority, err := checkOptionalPreseedAuthority(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() valSets, err := checkOptionalModelValidationSets(assert.headers, brandID) if err != nil { return nil, err } // 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, validationSets: valSets, serialAuthority: serialAuthority, sysUserAuthority: sysUserAuthority, preseedAuthority: preseedAuthority, timestamp: timestamp, }, nil }