github.com/freetocompute/snapd@v0.0.0-20210618182524-2fb355d72fd9/asserts/system_user.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package asserts
    21  
    22  import (
    23  	"fmt"
    24  	"net/mail"
    25  	"regexp"
    26  	"strconv"
    27  	"strings"
    28  	"time"
    29  )
    30  
    31  var validSystemUserUsernames = regexp.MustCompile(`^[a-z0-9][-a-z0-9+.-_]*$`)
    32  
    33  // SystemUser holds a system-user assertion which allows creating local
    34  // system users.
    35  type SystemUser struct {
    36  	assertionBase
    37  	series  []string
    38  	models  []string
    39  	serials []string
    40  	sshKeys []string
    41  	since   time.Time
    42  	until   time.Time
    43  
    44  	forcePasswordChange bool
    45  }
    46  
    47  // BrandID returns the brand identifier that signed this assertion.
    48  func (su *SystemUser) BrandID() string {
    49  	return su.HeaderString("brand-id")
    50  }
    51  
    52  // Email returns the email address that this assertion is valid for.
    53  func (su *SystemUser) Email() string {
    54  	return su.HeaderString("email")
    55  }
    56  
    57  // Series returns the series that this assertion is valid for.
    58  func (su *SystemUser) Series() []string {
    59  	return su.series
    60  }
    61  
    62  // Models returns the models that this assertion is valid for.
    63  func (su *SystemUser) Models() []string {
    64  	return su.models
    65  }
    66  
    67  // Serials returns the serials that this assertion is valid for.
    68  func (su *SystemUser) Serials() []string {
    69  	return su.serials
    70  }
    71  
    72  // Name returns the full name of the user (e.g. Random Guy).
    73  func (su *SystemUser) Name() string {
    74  	return su.HeaderString("name")
    75  }
    76  
    77  // Username returns the system user name that should be created (e.g. "foo").
    78  func (su *SystemUser) Username() string {
    79  	return su.HeaderString("username")
    80  }
    81  
    82  // Password returns the crypt(3) compatible password for the user.
    83  // Note that only ID: $6$ or stronger is supported (sha512crypt).
    84  func (su *SystemUser) Password() string {
    85  	return su.HeaderString("password")
    86  }
    87  
    88  // ForcePasswordChange returns true if the user needs to change the password
    89  // after the first login.
    90  func (su *SystemUser) ForcePasswordChange() bool {
    91  	return su.forcePasswordChange
    92  }
    93  
    94  // SSHKeys returns the ssh keys for the user.
    95  func (su *SystemUser) SSHKeys() []string {
    96  	return su.sshKeys
    97  }
    98  
    99  // Since returns the time since the assertion is valid.
   100  func (su *SystemUser) Since() time.Time {
   101  	return su.since
   102  }
   103  
   104  // Until returns the time until the assertion is valid.
   105  func (su *SystemUser) Until() time.Time {
   106  	return su.until
   107  }
   108  
   109  // ValidAt returns whether the system-user is valid at 'when' time.
   110  func (su *SystemUser) ValidAt(when time.Time) bool {
   111  	valid := when.After(su.since) || when.Equal(su.since)
   112  	if valid {
   113  		valid = when.Before(su.until)
   114  	}
   115  	return valid
   116  }
   117  
   118  // Implement further consistency checks.
   119  func (su *SystemUser) checkConsistency(db RODatabase, acck *AccountKey) error {
   120  	// Do the cross-checks when this assertion is actually used,
   121  	// i.e. in the create-user code. See also Model.checkConsitency
   122  
   123  	return nil
   124  }
   125  
   126  // sanity
   127  var _ consistencyChecker = (*SystemUser)(nil)
   128  
   129  type shadow struct {
   130  	ID     string
   131  	Rounds string
   132  	Salt   string
   133  	Hash   string
   134  }
   135  
   136  // crypt(3) compatible hashes have the forms:
   137  // - $id$salt$hash
   138  // - $id$rounds=N$salt$hash
   139  func parseShadowLine(line string) (*shadow, error) {
   140  	l := strings.SplitN(line, "$", 5)
   141  	if len(l) != 4 && len(l) != 5 {
   142  		return nil, fmt.Errorf(`hashed password must be of the form "$integer-id$salt$hash", see crypt(3)`)
   143  	}
   144  
   145  	// if rounds is the second field, the line must consist of 4
   146  	if strings.HasPrefix(l[2], "rounds=") && len(l) == 4 {
   147  		return nil, fmt.Errorf(`missing hash field`)
   148  	}
   149  
   150  	// shadow line without $rounds=N$
   151  	if len(l) == 4 {
   152  		return &shadow{
   153  			ID:   l[1],
   154  			Salt: l[2],
   155  			Hash: l[3],
   156  		}, nil
   157  	}
   158  	// shadow line with rounds
   159  	return &shadow{
   160  		ID:     l[1],
   161  		Rounds: l[2],
   162  		Salt:   l[3],
   163  		Hash:   l[4],
   164  	}, nil
   165  }
   166  
   167  // see crypt(3) for the legal chars
   168  var isValidSaltAndHash = regexp.MustCompile(`^[a-zA-Z0-9./]+$`).MatchString
   169  
   170  func checkHashedPassword(headers map[string]interface{}, name string) (string, error) {
   171  	pw, err := checkOptionalString(headers, name)
   172  	if err != nil {
   173  		return "", err
   174  	}
   175  	// the pw string is optional, so just return if its empty
   176  	if pw == "" {
   177  		return "", nil
   178  	}
   179  
   180  	// parse the shadow line
   181  	shd, err := parseShadowLine(pw)
   182  	if err != nil {
   183  		return "", fmt.Errorf(`%q header invalid: %s`, name, err)
   184  	}
   185  
   186  	// and verify it
   187  
   188  	// see crypt(3), ID 6 means SHA-512 (since glibc 2.7)
   189  	ID, err := strconv.Atoi(shd.ID)
   190  	if err != nil {
   191  		return "", fmt.Errorf(`%q header must start with "$integer-id$", got %q`, name, shd.ID)
   192  	}
   193  	// double check that we only allow modern hashes
   194  	if ID < 6 {
   195  		return "", fmt.Errorf("%q header only supports $id$ values of 6 (sha512crypt) or higher", name)
   196  	}
   197  
   198  	// the $rounds=N$ part is optional
   199  	if strings.HasPrefix(shd.Rounds, "rounds=") {
   200  		rounds, err := strconv.Atoi(strings.SplitN(shd.Rounds, "=", 2)[1])
   201  		if err != nil {
   202  			return "", fmt.Errorf("%q header has invalid number of rounds: %s", name, err)
   203  		}
   204  		if rounds < 5000 || rounds > 999999999 {
   205  			return "", fmt.Errorf("%q header rounds parameter out of bounds: %d", name, rounds)
   206  		}
   207  	}
   208  
   209  	if !isValidSaltAndHash(shd.Salt) {
   210  		return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt)
   211  	}
   212  	if !isValidSaltAndHash(shd.Hash) {
   213  		return "", fmt.Errorf("%q header has invalid chars in hash %q", name, shd.Hash)
   214  	}
   215  
   216  	return pw, nil
   217  }
   218  
   219  func assembleSystemUser(assert assertionBase) (Assertion, error) {
   220  	// brand-id here can be different from authority-id,
   221  	// the code using the assertion must use the policy set
   222  	// by the model assertion system-user-authority header
   223  	email, err := checkNotEmptyString(assert.headers, "email")
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  	if _, err := mail.ParseAddress(email); err != nil {
   228  		return nil, fmt.Errorf(`"email" header must be a RFC 5322 compliant email address: %s`, err)
   229  	}
   230  
   231  	series, err := checkStringList(assert.headers, "series")
   232  	if err != nil {
   233  		return nil, err
   234  	}
   235  	models, err := checkStringList(assert.headers, "models")
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	serials, err := checkStringList(assert.headers, "serials")
   240  	if err != nil {
   241  		return nil, err
   242  	}
   243  	if len(serials) > 0 && assert.Format() < 1 {
   244  		return nil, fmt.Errorf(`the "serials" header is only supported for format 1 or greater`)
   245  	}
   246  	if len(serials) > 0 && len(models) != 1 {
   247  		return nil, fmt.Errorf(`in the presence of the "serials" header "models" must specify exactly one model`)
   248  	}
   249  
   250  	if _, err := checkOptionalString(assert.headers, "name"); err != nil {
   251  		return nil, err
   252  	}
   253  	if _, err := checkStringMatches(assert.headers, "username", validSystemUserUsernames); err != nil {
   254  		return nil, err
   255  	}
   256  	password, err := checkHashedPassword(assert.headers, "password")
   257  	if err != nil {
   258  		return nil, err
   259  	}
   260  	forcePasswordChange, err := checkOptionalBool(assert.headers, "force-password-change")
   261  	if err != nil {
   262  		return nil, err
   263  	}
   264  	if forcePasswordChange && password == "" {
   265  		return nil, fmt.Errorf(`cannot use "force-password-change" with an empty "password"`)
   266  	}
   267  
   268  	sshKeys, err := checkStringList(assert.headers, "ssh-keys")
   269  	if err != nil {
   270  		return nil, err
   271  	}
   272  	since, err := checkRFC3339Date(assert.headers, "since")
   273  	if err != nil {
   274  		return nil, err
   275  	}
   276  	until, err := checkRFC3339Date(assert.headers, "until")
   277  	if err != nil {
   278  		return nil, err
   279  	}
   280  	if until.Before(since) {
   281  		return nil, fmt.Errorf("'until' time cannot be before 'since' time")
   282  	}
   283  
   284  	// "global" system-user assertion can only be valid for 1y
   285  	if len(models) == 0 && until.After(since.AddDate(1, 0, 0)) {
   286  		return nil, fmt.Errorf("'until' time cannot be more than 365 days in the future when no models are specified")
   287  	}
   288  
   289  	return &SystemUser{
   290  		assertionBase:       assert,
   291  		series:              series,
   292  		models:              models,
   293  		serials:             serials,
   294  		sshKeys:             sshKeys,
   295  		since:               since,
   296  		until:               until,
   297  		forcePasswordChange: forcePasswordChange,
   298  	}, nil
   299  }
   300  
   301  func systemUserFormatAnalyze(headers map[string]interface{}, body []byte) (formatnum int, err error) {
   302  	formatnum = 0
   303  
   304  	serials, err := checkStringList(headers, "serials")
   305  	if err != nil {
   306  		return 0, err
   307  	}
   308  	if len(serials) > 0 {
   309  		formatnum = 1
   310  	}
   311  
   312  	return formatnum, nil
   313  }