mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
* asserts: update username regex allowed by system-user assertion * asserts: drop the second '-' from the regex
357 lines
9.7 KiB
Go
357 lines
9.7 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2016 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"
|
|
"net/mail"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// validSystemUserUsernames matches the regex we allow by osutil/user.go:IsValidUsername
|
|
var validSystemUserUsernames = regexp.MustCompile(`^[a-z0-9][-a-z0-9._]*$`)
|
|
|
|
// SystemUser holds a system-user assertion which allows creating local
|
|
// system users.
|
|
type SystemUser struct {
|
|
assertionBase
|
|
series []string
|
|
models []string
|
|
serials []string
|
|
sshKeys []string
|
|
since time.Time
|
|
until time.Time
|
|
expiration string
|
|
|
|
forcePasswordChange bool
|
|
}
|
|
|
|
// BrandID returns the brand identifier that signed this assertion.
|
|
func (su *SystemUser) BrandID() string {
|
|
return su.HeaderString("brand-id")
|
|
}
|
|
|
|
// Email returns the email address that this assertion is valid for.
|
|
func (su *SystemUser) Email() string {
|
|
return su.HeaderString("email")
|
|
}
|
|
|
|
// Series returns the series that this assertion is valid for.
|
|
func (su *SystemUser) Series() []string {
|
|
return su.series
|
|
}
|
|
|
|
// Models returns the models that this assertion is valid for.
|
|
func (su *SystemUser) Models() []string {
|
|
return su.models
|
|
}
|
|
|
|
// Serials returns the serials that this assertion is valid for.
|
|
func (su *SystemUser) Serials() []string {
|
|
return su.serials
|
|
}
|
|
|
|
// Name returns the full name of the user (e.g. Random Guy).
|
|
func (su *SystemUser) Name() string {
|
|
return su.HeaderString("name")
|
|
}
|
|
|
|
// Username returns the system user name that should be created (e.g. "foo").
|
|
func (su *SystemUser) Username() string {
|
|
return su.HeaderString("username")
|
|
}
|
|
|
|
// Password returns the crypt(3) compatible password for the user.
|
|
// Note that only ID: $6$ or stronger is supported (sha512crypt).
|
|
func (su *SystemUser) Password() string {
|
|
return su.HeaderString("password")
|
|
}
|
|
|
|
// ForcePasswordChange returns true if the user needs to change the password
|
|
// after the first login.
|
|
func (su *SystemUser) ForcePasswordChange() bool {
|
|
return su.forcePasswordChange
|
|
}
|
|
|
|
// SSHKeys returns the ssh keys for the user.
|
|
func (su *SystemUser) SSHKeys() []string {
|
|
return su.sshKeys
|
|
}
|
|
|
|
// Since returns the time since the assertion is valid.
|
|
func (su *SystemUser) Since() time.Time {
|
|
return su.since
|
|
}
|
|
|
|
// Until returns the time until the assertion is valid.
|
|
func (su *SystemUser) Until() time.Time {
|
|
return su.until
|
|
}
|
|
|
|
// UserExpiration returns the expiration or validity duration of the user created.
|
|
//
|
|
// If no expiration was specified, this will return an zero time.Time structure.
|
|
//
|
|
// If expiration was set to 'until-expiration' then the .Until() time will be
|
|
// returned.
|
|
func (su *SystemUser) UserExpiration() time.Time {
|
|
if su.expiration == "until-expiration" {
|
|
return su.until
|
|
}
|
|
return time.Time{}
|
|
}
|
|
|
|
// ValidAt returns whether the system-user is valid at 'when' time.
|
|
func (su *SystemUser) ValidAt(when time.Time) bool {
|
|
valid := when.After(su.since) || when.Equal(su.since)
|
|
if valid {
|
|
valid = when.Before(su.until)
|
|
}
|
|
return valid
|
|
}
|
|
|
|
// Implement further consistency checks.
|
|
func (su *SystemUser) checkConsistency(db RODatabase, acck *AccountKey) error {
|
|
// Do the cross-checks when this assertion is actually used,
|
|
// i.e. in the create-user code. See also Model.checkConsitency
|
|
|
|
return nil
|
|
}
|
|
|
|
// expected interface is implemented
|
|
var _ consistencyChecker = (*SystemUser)(nil)
|
|
|
|
type shadow struct {
|
|
ID string
|
|
Rounds string
|
|
Salt string
|
|
Hash string
|
|
}
|
|
|
|
// crypt(3) compatible hashes have the forms:
|
|
// - $id$salt$hash
|
|
// - $id$rounds=N$salt$hash
|
|
func parseShadowLine(line string) (*shadow, error) {
|
|
l := strings.SplitN(line, "$", 5)
|
|
if len(l) != 4 && len(l) != 5 {
|
|
return nil, fmt.Errorf(`hashed password must be of the form "$integer-id$salt$hash", see crypt(3)`)
|
|
}
|
|
|
|
// if rounds is the second field, the line must consist of 4
|
|
if strings.HasPrefix(l[2], "rounds=") && len(l) == 4 {
|
|
return nil, fmt.Errorf(`missing hash field`)
|
|
}
|
|
|
|
// shadow line without $rounds=N$
|
|
if len(l) == 4 {
|
|
return &shadow{
|
|
ID: l[1],
|
|
Salt: l[2],
|
|
Hash: l[3],
|
|
}, nil
|
|
}
|
|
// shadow line with rounds
|
|
return &shadow{
|
|
ID: l[1],
|
|
Rounds: l[2],
|
|
Salt: l[3],
|
|
Hash: l[4],
|
|
}, nil
|
|
}
|
|
|
|
// see crypt(3) for the legal chars
|
|
var isValidSaltAndHash = regexp.MustCompile(`^[a-zA-Z0-9./]+$`).MatchString
|
|
|
|
func checkHashedPassword(headers map[string]interface{}, name string) (string, error) {
|
|
pw, err := checkOptionalString(headers, name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
// the pw string is optional, so just return if its empty
|
|
if pw == "" {
|
|
return "", nil
|
|
}
|
|
|
|
// parse the shadow line
|
|
shd, err := parseShadowLine(pw)
|
|
if err != nil {
|
|
return "", fmt.Errorf(`%q header invalid: %s`, name, err)
|
|
}
|
|
|
|
// and verify it
|
|
|
|
// see crypt(3), ID 6 means SHA-512 (since glibc 2.7)
|
|
ID, err := strconv.Atoi(shd.ID)
|
|
if err != nil {
|
|
return "", fmt.Errorf(`%q header must start with "$integer-id$", got %q`, name, shd.ID)
|
|
}
|
|
// double check that we only allow modern hashes
|
|
if ID < 6 {
|
|
return "", fmt.Errorf("%q header only supports $id$ values of 6 (sha512crypt) or higher", name)
|
|
}
|
|
|
|
// the $rounds=N$ part is optional
|
|
if strings.HasPrefix(shd.Rounds, "rounds=") {
|
|
rounds, err := strconv.Atoi(strings.SplitN(shd.Rounds, "=", 2)[1])
|
|
if err != nil {
|
|
return "", fmt.Errorf("%q header has invalid number of rounds: %s", name, err)
|
|
}
|
|
if rounds < 5000 || rounds > 999999999 {
|
|
return "", fmt.Errorf("%q header rounds parameter out of bounds: %d", name, rounds)
|
|
}
|
|
}
|
|
|
|
if !isValidSaltAndHash(shd.Salt) {
|
|
return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt)
|
|
}
|
|
if !isValidSaltAndHash(shd.Hash) {
|
|
return "", fmt.Errorf("%q header has invalid chars in hash %q", name, shd.Hash)
|
|
}
|
|
|
|
return pw, nil
|
|
}
|
|
|
|
func checkSystemUserPresence(assert assertionBase) (string, error) {
|
|
str, err := checkOptionalString(assert.headers, "user-presence")
|
|
if err != nil || str == "" {
|
|
return "", err
|
|
}
|
|
if assert.Format() < 2 {
|
|
return "", fmt.Errorf(`the "user-presence" header is only supported for format 2 or greater`)
|
|
}
|
|
|
|
if str != "until-expiration" {
|
|
return "", fmt.Errorf(`invalid "user-presence" header, only explicit valid value is "until-expiration": %q`, str)
|
|
}
|
|
return str, nil
|
|
}
|
|
|
|
func assembleSystemUser(assert assertionBase) (Assertion, error) {
|
|
// brand-id here can be different from authority-id,
|
|
// the code using the assertion must use the policy set
|
|
// by the model assertion system-user-authority header
|
|
email, err := checkNotEmptyString(assert.headers, "email")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := mail.ParseAddress(email); err != nil {
|
|
return nil, fmt.Errorf(`"email" header must be a RFC 5322 compliant email address: %s`, err)
|
|
}
|
|
|
|
series, err := checkStringList(assert.headers, "series")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
models, err := checkStringList(assert.headers, "models")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
serials, err := checkStringList(assert.headers, "serials")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(serials) > 0 && assert.Format() < 1 {
|
|
return nil, fmt.Errorf(`the "serials" header is only supported for format 1 or greater`)
|
|
}
|
|
if len(serials) > 0 && len(models) != 1 {
|
|
return nil, fmt.Errorf(`in the presence of the "serials" header "models" must specify exactly one model`)
|
|
}
|
|
|
|
if _, err := checkOptionalString(assert.headers, "name"); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := checkStringMatches(assert.headers, "username", validSystemUserUsernames); err != nil {
|
|
return nil, err
|
|
}
|
|
password, err := checkHashedPassword(assert.headers, "password")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
forcePasswordChange, err := checkOptionalBool(assert.headers, "force-password-change")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if forcePasswordChange && password == "" {
|
|
return nil, fmt.Errorf(`cannot use "force-password-change" with an empty "password"`)
|
|
}
|
|
|
|
sshKeys, err := checkStringList(assert.headers, "ssh-keys")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
since, err := checkRFC3339Date(assert.headers, "since")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
until, err := checkRFC3339Date(assert.headers, "until")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if until.Before(since) {
|
|
return nil, fmt.Errorf("'until' time cannot be before 'since' time")
|
|
}
|
|
expiration, err := checkSystemUserPresence(assert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// "global" system-user assertion can only be valid for 1y
|
|
if len(models) == 0 && until.After(since.AddDate(1, 0, 0)) {
|
|
return nil, fmt.Errorf("'until' time cannot be more than 365 days in the future when no models are specified")
|
|
}
|
|
|
|
return &SystemUser{
|
|
assertionBase: assert,
|
|
series: series,
|
|
models: models,
|
|
serials: serials,
|
|
sshKeys: sshKeys,
|
|
since: since,
|
|
until: until,
|
|
expiration: expiration,
|
|
forcePasswordChange: forcePasswordChange,
|
|
}, nil
|
|
}
|
|
|
|
func systemUserFormatAnalyze(headers map[string]interface{}, body []byte) (formatnum int, err error) {
|
|
formatnum = 0
|
|
|
|
serials, err := checkStringList(headers, "serials")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if len(serials) > 0 {
|
|
formatnum = 1
|
|
}
|
|
|
|
presence, err := checkOptionalString(headers, "user-presence")
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
if presence != "" {
|
|
formatnum = 2
|
|
}
|
|
|
|
return formatnum, nil
|
|
}
|