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 }