github.com/rigado/snapd@v2.42.5-go-mod+incompatible/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 sshKeys []string 40 since time.Time 41 until time.Time 42 43 forcePasswordChange bool 44 } 45 46 // BrandID returns the brand identifier that signed this assertion. 47 func (su *SystemUser) BrandID() string { 48 return su.HeaderString("brand-id") 49 } 50 51 // Email returns the email address that this assertion is valid for. 52 func (su *SystemUser) Email() string { 53 return su.HeaderString("email") 54 } 55 56 // Series returns the series that this assertion is valid for. 57 func (su *SystemUser) Series() []string { 58 return su.series 59 } 60 61 // Models returns the models that this assertion is valid for. 62 func (su *SystemUser) Models() []string { 63 return su.models 64 } 65 66 // Name returns the full name of the user (e.g. Random Guy). 67 func (su *SystemUser) Name() string { 68 return su.HeaderString("name") 69 } 70 71 // Username returns the system user name that should be created (e.g. "foo"). 72 func (su *SystemUser) Username() string { 73 return su.HeaderString("username") 74 } 75 76 // Password returns the crypt(3) compatible password for the user. 77 // Note that only ID: $6$ or stronger is supported (sha512crypt). 78 func (su *SystemUser) Password() string { 79 return su.HeaderString("password") 80 } 81 82 // ForcePasswordChange returns true if the user needs to change the password 83 // after the first login. 84 func (su *SystemUser) ForcePasswordChange() bool { 85 return su.forcePasswordChange 86 } 87 88 // SSHKeys returns the ssh keys for the user. 89 func (su *SystemUser) SSHKeys() []string { 90 return su.sshKeys 91 } 92 93 // Since returns the time since the assertion is valid. 94 func (su *SystemUser) Since() time.Time { 95 return su.since 96 } 97 98 // Until returns the time until the assertion is valid. 99 func (su *SystemUser) Until() time.Time { 100 return su.until 101 } 102 103 // ValidAt returns whether the system-user is valid at 'when' time. 104 func (su *SystemUser) ValidAt(when time.Time) bool { 105 valid := when.After(su.since) || when.Equal(su.since) 106 if valid { 107 valid = when.Before(su.until) 108 } 109 return valid 110 } 111 112 // Implement further consistency checks. 113 func (su *SystemUser) checkConsistency(db RODatabase, acck *AccountKey) error { 114 // Do the cross-checks when this assertion is actually used, 115 // i.e. in the create-user code. See also Model.checkConsitency 116 117 return nil 118 } 119 120 // sanity 121 var _ consistencyChecker = (*SystemUser)(nil) 122 123 type shadow struct { 124 ID string 125 Rounds string 126 Salt string 127 Hash string 128 } 129 130 // crypt(3) compatible hashes have the forms: 131 // - $id$salt$hash 132 // - $id$rounds=N$salt$hash 133 func parseShadowLine(line string) (*shadow, error) { 134 l := strings.SplitN(line, "$", 5) 135 if len(l) != 4 && len(l) != 5 { 136 return nil, fmt.Errorf(`hashed password must be of the form "$integer-id$salt$hash", see crypt(3)`) 137 } 138 139 // if rounds is the second field, the line must consist of 4 140 if strings.HasPrefix(l[2], "rounds=") && len(l) == 4 { 141 return nil, fmt.Errorf(`missing hash field`) 142 } 143 144 // shadow line without $rounds=N$ 145 if len(l) == 4 { 146 return &shadow{ 147 ID: l[1], 148 Salt: l[2], 149 Hash: l[3], 150 }, nil 151 } 152 // shadow line with rounds 153 return &shadow{ 154 ID: l[1], 155 Rounds: l[2], 156 Salt: l[3], 157 Hash: l[4], 158 }, nil 159 } 160 161 // see crypt(3) for the legal chars 162 var isValidSaltAndHash = regexp.MustCompile(`^[a-zA-Z0-9./]+$`).MatchString 163 164 func checkHashedPassword(headers map[string]interface{}, name string) (string, error) { 165 pw, err := checkOptionalString(headers, name) 166 if err != nil { 167 return "", err 168 } 169 // the pw string is optional, so just return if its empty 170 if pw == "" { 171 return "", nil 172 } 173 174 // parse the shadow line 175 shd, err := parseShadowLine(pw) 176 if err != nil { 177 return "", fmt.Errorf(`%q header invalid: %s`, name, err) 178 } 179 180 // and verify it 181 182 // see crypt(3), ID 6 means SHA-512 (since glibc 2.7) 183 ID, err := strconv.Atoi(shd.ID) 184 if err != nil { 185 return "", fmt.Errorf(`%q header must start with "$integer-id$", got %q`, name, shd.ID) 186 } 187 // double check that we only allow modern hashes 188 if ID < 6 { 189 return "", fmt.Errorf("%q header only supports $id$ values of 6 (sha512crypt) or higher", name) 190 } 191 192 // the $rounds=N$ part is optional 193 if strings.HasPrefix(shd.Rounds, "rounds=") { 194 rounds, err := strconv.Atoi(strings.SplitN(shd.Rounds, "=", 2)[1]) 195 if err != nil { 196 return "", fmt.Errorf("%q header has invalid number of rounds: %s", name, err) 197 } 198 if rounds < 5000 || rounds > 999999999 { 199 return "", fmt.Errorf("%q header rounds parameter out of bounds: %d", name, rounds) 200 } 201 } 202 203 if !isValidSaltAndHash(shd.Salt) { 204 return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt) 205 } 206 if !isValidSaltAndHash(shd.Hash) { 207 return "", fmt.Errorf("%q header has invalid chars in hash %q", name, shd.Hash) 208 } 209 210 return pw, nil 211 } 212 213 func assembleSystemUser(assert assertionBase) (Assertion, error) { 214 // brand-id here can be different from authority-id, 215 // the code using the assertion must use the policy set 216 // by the model assertion system-user-authority header 217 email, err := checkNotEmptyString(assert.headers, "email") 218 if err != nil { 219 return nil, err 220 } 221 if _, err := mail.ParseAddress(email); err != nil { 222 return nil, fmt.Errorf(`"email" header must be a RFC 5322 compliant email address: %s`, err) 223 } 224 225 series, err := checkStringList(assert.headers, "series") 226 if err != nil { 227 return nil, err 228 } 229 models, err := checkStringList(assert.headers, "models") 230 if err != nil { 231 return nil, err 232 } 233 if _, err := checkOptionalString(assert.headers, "name"); err != nil { 234 return nil, err 235 } 236 if _, err := checkStringMatches(assert.headers, "username", validSystemUserUsernames); err != nil { 237 return nil, err 238 } 239 password, err := checkHashedPassword(assert.headers, "password") 240 if err != nil { 241 return nil, err 242 } 243 forcePasswordChange, err := checkOptionalBool(assert.headers, "force-password-change") 244 if err != nil { 245 return nil, err 246 } 247 if forcePasswordChange && password == "" { 248 return nil, fmt.Errorf(`cannot use "force-password-change" with an empty "password"`) 249 } 250 251 sshKeys, err := checkStringList(assert.headers, "ssh-keys") 252 if err != nil { 253 return nil, err 254 } 255 since, err := checkRFC3339Date(assert.headers, "since") 256 if err != nil { 257 return nil, err 258 } 259 until, err := checkRFC3339Date(assert.headers, "until") 260 if err != nil { 261 return nil, err 262 } 263 if until.Before(since) { 264 return nil, fmt.Errorf("'until' time cannot be before 'since' time") 265 } 266 267 // "global" system-user assertion can only be valid for 1y 268 if len(models) == 0 && until.After(since.AddDate(1, 0, 0)) { 269 return nil, fmt.Errorf("'until' time cannot be more than 365 days in the future when no models are specified") 270 } 271 272 return &SystemUser{ 273 assertionBase: assert, 274 series: series, 275 models: models, 276 sshKeys: sshKeys, 277 since: since, 278 until: until, 279 forcePasswordChange: forcePasswordChange, 280 }, nil 281 }