github.com/greenpau/go-authcrunch@v1.1.4/pkg/kms/config.go (about) 1 // Copyright 2022 Paul Greenberg greenpau@outlook.com 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package kms 16 17 import ( 18 "encoding/csv" 19 "fmt" 20 "github.com/greenpau/go-authcrunch/pkg/errors" 21 cfgutil "github.com/greenpau/go-authcrunch/pkg/util/cfg" 22 23 "os" 24 "sort" 25 "strconv" 26 "strings" 27 ) 28 29 const ( 30 defaultKeyID = "0" 31 defaultTokenName = "access_token" 32 defaultTokenLifetime int = 900 33 ) 34 35 var ( 36 reservedKeyConfigKeywords = map[string]bool{ 37 "crypto": true, 38 "key": true, 39 "sign": true, 40 "verify": true, 41 "sign-verify": true, 42 "auto": true, 43 "and": true, 44 "token": true, 45 "lifetime": true, 46 "from": true, 47 "env": true, 48 "as": true, 49 } 50 reservedUsageKeywords = map[string]bool{ 51 "sign": true, 52 "verify": true, 53 "sign-verify": true, 54 "auto": true, 55 } 56 ) 57 58 // CryptoKeyConfig is common token-related configuration settings. 59 type CryptoKeyConfig struct { 60 // Seq is the order in which a key would be processed. 61 Seq int `json:"seq,omitempty" xml:"seq,omitempty" yaml:"seq,omitempty"` 62 // ID is the key ID, aka kid. 63 ID string `json:"id,omitempty" xml:"id,omitempty" yaml:"id,omitempty"` 64 // Usage is the intended key usage. The values are: sign, verify, both, 65 // or auto. 66 Usage string `json:"usage,omitempty" xml:"usage,omitempty" yaml:"usage,omitempty"` 67 // TokenName is the token name associated with the key. 68 TokenName string `json:"token_name,omitempty" xml:"token_name,omitempty" yaml:"token_name,omitempty"` 69 // Source is either config or env. 70 Source string `json:"source,omitempty" xml:"source,omitempty" yaml:"source,omitempty"` 71 // Algorithm is either hmac, rsa, or ecdsa. 72 Algorithm string `json:"algorithm,omitempty" xml:"algorithm,omitempty" yaml:"algorithm,omitempty"` 73 // EnvVarName is the name of environment variables holding either the value of 74 // a key or the path a directory or file containing a key. 75 EnvVarName string `json:"env_var_name,omitempty" xml:"env_var_name,omitempty" yaml:"env_var_name,omitempty"` 76 // EnvVarType indicates how to interpret the value found in the EnvVarName. If 77 // it is blank, then the assumption is the environment variable value 78 // contains either public or private key. 79 EnvVarType string `json:"env_var_type,omitempty" xml:"env_var_type,omitempty" yaml:"env_var_type,omitempty"` 80 // EnvVarValue is the value associated with the environment variable set by EnvVarName. 81 EnvVarValue string `json:"env_var_value,omitempty" xml:"env_var_value,omitempty" yaml:"env_var_value,omitempty"` 82 // FilePath is the path of a file containing either private or public key. 83 FilePath string `json:"file_path,omitempty" xml:"file_path,omitempty" yaml:"file_path,omitempty"` 84 // DirPath is the path to a directory containing crypto keys. 85 DirPath string `json:"dir_path,omitempty" xml:"dir_path,omitempty" yaml:"dir_path,omitempty"` 86 // TokenLifetime is the expected token grant lifetime in seconds. 87 TokenLifetime int `json:"token_lifetime,omitempty" xml:"token_lifetime,omitempty" yaml:"token_lifetime,omitempty"` 88 // Secret is the shared key used with HMAC algorithm. 89 Secret string `json:"token_secret,omitempty" xml:"token_secret" yaml:"token_secret"` 90 // PreferredSignMethod is the preferred method to sign tokens, e.g. 91 // all HMAC keys could use HS256, HS384, and HS512 methods. By default, 92 // the preferred method is HS512. However, one may prefer using HS256. 93 PreferredSignMethod string `json:"token_sign_method,omitempty" xml:"token_sign_method,omitempty" yaml:"token_sign_method,omitempty"` 94 // EvalExpr is a list of expressions evaluated whether a specific key 95 // should be used for signing and verification. 96 EvalExpr []string `json:"token_eval_expr,omitempty" xml:"token_eval_expr" yaml:"token_eval_expr"` 97 // parsed indicated whether the key was parsed via config. 98 parsed bool 99 // validated indicated whether the key config was validated. 100 validated bool 101 } 102 103 // ToString returns string representation of a crypto key config. 104 func (k *CryptoKeyConfig) ToString() string { 105 var sb strings.Builder 106 sb.WriteString("key config for kid: " + k.ID) 107 if k.Usage != "" { 108 sb.WriteString(", usage: " + k.Usage) 109 } 110 if k.Source != "" { 111 sb.WriteString(", source: " + k.Source) 112 } 113 if k.Secret != "" { 114 sb.WriteString(", secret: " + k.Secret) 115 } 116 if k.Algorithm != "" { 117 sb.WriteString(", algo: " + k.Algorithm) 118 } 119 if k.EnvVarName != "" { 120 sb.WriteString(", env var as " + k.EnvVarType + ": " + k.EnvVarName) 121 } 122 if k.FilePath != "" { 123 sb.WriteString(", file path: " + k.FilePath) 124 } 125 if k.DirPath != "" { 126 sb.WriteString(", dir path: " + k.DirPath) 127 } 128 if k.validated || k.parsed { 129 sb.WriteString(", flags:") 130 if k.parsed { 131 sb.WriteString(" parsed") 132 } 133 if k.validated { 134 sb.WriteString(" validated") 135 } 136 } 137 if k.TokenName != "" { 138 sb.WriteString(", token name=" + k.TokenName) 139 } 140 if k.TokenLifetime != 0 { 141 sb.WriteString(fmt.Sprintf(" lifetime=%d", k.TokenLifetime)) 142 } 143 return sb.String() 144 } 145 146 func (k *CryptoKeyConfig) loadEnvVar() error { 147 v := os.Getenv(k.EnvVarName) 148 v = strings.TrimSpace(v) 149 if v == "" { 150 return errors.ErrCryptoKeyConfigEmptyEnvVar.WithArgs(k.EnvVarName) 151 } 152 k.EnvVarValue = v 153 return nil 154 } 155 156 func (k *CryptoKeyConfig) validate() error { 157 switch k.Usage { 158 case "verify", "sign", "sign-verify", "auto": 159 case "": 160 return fmt.Errorf("key usage is not set") 161 default: 162 return fmt.Errorf("key usage %q is invalid", k.Usage) 163 } 164 165 switch k.Source { 166 case "": 167 return fmt.Errorf("key source not found") 168 case "config": 169 case "env": 170 switch k.EnvVarType { 171 case "key", "file", "directory": 172 case "": 173 return fmt.Errorf("key source type for env not set") 174 default: 175 return fmt.Errorf("key source type %q for env is invalid", k.EnvVarType) 176 } 177 default: 178 return fmt.Errorf("key source %q is invalid", k.Source) 179 } 180 181 switch k.Algorithm { 182 case "hmac", "rsa", "ecdsa", "": 183 default: 184 return fmt.Errorf("key algorithm %q is invalid", k.Algorithm) 185 } 186 k.validated = true 187 return nil 188 } 189 190 // ParseCryptoKeyStoreConfig parses crypto key store default configuration, 191 // e.g. default token name and configuration. 192 func ParseCryptoKeyStoreConfig(cfg string) (map[string]interface{}, error) { 193 m := make(map[string]interface{}) 194 for _, line := range strings.Split(cfg, "\n") { 195 args, err := cfgutil.DecodeArgs(line) 196 if err != nil { 197 return nil, err 198 } 199 if len(args) < 4 { 200 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "too few arguments") 201 } 202 if args[0] != "default" { 203 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "must be prefixed with 'crypto default' keywords") 204 } 205 switch args[1] { 206 case "token": 207 switch args[2] { 208 case "name": 209 m["token_name"] = args[3] 210 case "lifetime": 211 lifetime, err := strconv.Atoi(args[3]) 212 if err != nil { 213 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, err) 214 } 215 m["token_lifetime"] = lifetime 216 default: 217 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "contains unsupported 'crypto default token' parameter: %s", args[2]) 218 } 219 default: 220 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, fmt.Sprintf("contains unsupported 'crypto default' keyword: %s", args[1])) 221 } 222 } 223 return m, nil 224 } 225 226 // ParseCryptoKeyConfigs parses crypto key configurations. 227 func ParseCryptoKeyConfigs(cfg string) ([]*CryptoKeyConfig, error) { 228 var cursor int 229 var keys []*CryptoKeyConfig 230 defaultConfig := make(map[string]interface{}) 231 // m := make(map[string]*CryptoKeyConfig) 232 for _, s := range strings.Split(cfg, "\n") { 233 var key *CryptoKeyConfig 234 var keyUsage string 235 kid := defaultKeyID 236 s = strings.TrimSpace(s) 237 if s == "" { 238 continue 239 } 240 241 r := csv.NewReader(strings.NewReader(s)) 242 r.Comma = ' ' 243 args, err := r.Read() 244 if err != nil { 245 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(s, err) 246 } 247 248 line := strings.Join(args, " ") 249 if len(args) < 3 { 250 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "entry is too short") 251 } 252 253 // First, identify key id. 254 j := 0 255 if args[0] == "crypto" { 256 j = 1 257 } 258 259 nextEntry := false 260 switch args[j] { 261 case "default": 262 nextEntry = true 263 p := args[j+1:] 264 switch p[0] { 265 case "token": 266 if len(p) != 3 { 267 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "default token setting too short") 268 } 269 switch p[1] { 270 case "name": 271 defaultConfig["token_name"] = p[2] 272 case "lifetime": 273 lifetime, err := strconv.Atoi(p[2]) 274 if err != nil { 275 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, err) 276 } 277 defaultConfig["token_lifetime"] = lifetime 278 case "kid": 279 defaultConfig["token_kid"] = p[2] 280 default: 281 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "unknown default token setting") 282 } 283 default: 284 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "unknown default setting") 285 } 286 case "key": 287 if exists := reservedKeyConfigKeywords[args[j+1]]; !exists { 288 kid = args[j+1] 289 } 290 default: 291 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "bad syntax") 292 } 293 294 if nextEntry { 295 continue 296 } 297 298 for _, arg := range args { 299 if _, exists := reservedUsageKeywords[arg]; exists { 300 keyUsage = arg 301 break 302 } 303 } 304 305 // Next, register the key. 306 var curKey *CryptoKeyConfig 307 if len(keys) > 0 { 308 curKey = keys[cursor] 309 } 310 switch { 311 case len(keys) == 0: 312 k := &CryptoKeyConfig{} 313 k.Seq = len(keys) 314 k.ID = kid 315 keys = append(keys, k) 316 key = k 317 cursor = len(keys) - 1 318 case curKey.ID != kid: 319 k := &CryptoKeyConfig{} 320 k.Seq = len(keys) 321 k.ID = kid 322 keys = append(keys, k) 323 key = k 324 cursor = len(keys) - 1 325 case curKey.Usage != "" && keyUsage != "": 326 if (curKey.Usage == "verify" && keyUsage == "sign") || (curKey.Usage == "sign" && keyUsage == "verify") || 327 (curKey.Usage == "auto" && keyUsage == "auto") || (curKey.Usage == "sign-verify" && keyUsage == "sign-verify") { 328 nk := &CryptoKeyConfig{} 329 nk.Seq = len(keys) 330 nk.ID = kid 331 nk.TokenName = curKey.TokenName 332 nk.TokenLifetime = curKey.TokenLifetime 333 key = nk 334 keys = append(keys, nk) 335 cursor = len(keys) - 1 336 } else { 337 key = curKey 338 } 339 default: 340 key = curKey 341 } 342 343 // Iterate over the provided configuration line. 344 max := len(args) - 1 345 i := 0 346 // for i < max { 347 for i < len(args) { 348 remainder := max - i 349 if exists := reservedKeyConfigKeywords[args[i]]; exists && (remainder == 0) { 350 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "reserved keyword must not be last") 351 } 352 353 switch args[i] { 354 case "crypto": 355 case "key": 356 if exists := reservedKeyConfigKeywords[args[i+1]]; !exists { 357 i++ 358 } 359 case "token": 360 if remainder < 2 { 361 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "token must be followed by its attributes") 362 } 363 switch args[i+1] { 364 case "name": 365 key.TokenName = args[i+2] 366 case "lifetime": 367 i, err := strconv.Atoi(args[i+2]) 368 if err != nil { 369 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, err) 370 } 371 key.TokenLifetime = i 372 default: 373 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "unknown key token setting") 374 } 375 i += 2 376 case "verify", "sign", "sign-verify", "auto": 377 if key.Usage != "" { 378 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "duplicate key id") 379 } 380 key.Usage = args[i] 381 if args[i+1] != "from" { 382 if remainder > 1 { 383 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "bad syntax") 384 } 385 key.Secret = args[i+1] 386 key.Source = "config" 387 key.Algorithm = "hmac" 388 i++ 389 break 390 } 391 switch remainder { 392 case 3: 393 switch args[i+2] { 394 case "file": 395 key.Source = "config" 396 key.FilePath = args[i+3] 397 case "directory": 398 key.Source = "config" 399 key.DirPath = args[i+3] 400 case "env": 401 key.Source = "env" 402 key.EnvVarName = args[i+3] 403 key.EnvVarType = "key" 404 if err := key.loadEnvVar(); err != nil { 405 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, err) 406 } 407 default: 408 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "bad syntax") 409 } 410 i += 3 411 case 5: 412 if args[i+2] != "env" || args[i+4] != "as" { 413 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "bad syntax") 414 } 415 key.EnvVarName = args[i+3] 416 switch args[i+5] { 417 case "file", "directory", "key": 418 key.Source = "env" 419 key.EnvVarType = args[i+5] 420 if err := key.loadEnvVar(); err != nil { 421 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, err) 422 } 423 default: 424 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "bad syntax") 425 } 426 i += 5 427 default: 428 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "bad syntax") 429 } 430 default: 431 return nil, errors.ErrCryptoKeyConfigEntryInvalid.WithArgs(line, "invalid argument") 432 } 433 i++ 434 } 435 } 436 437 if len(keys) == 0 { 438 return nil, errors.ErrCryptoKeyConfigNoConfigFound 439 } 440 441 sort.Slice(keys, func(i, j int) bool { 442 return keys[i].Seq < keys[j].Seq 443 }) 444 445 for _, kcfg := range keys { 446 if kcfg.TokenName == "" { 447 if _, exists := defaultConfig["token_name"]; exists { 448 kcfg.TokenName = defaultConfig["token_name"].(string) 449 } else { 450 kcfg.TokenName = defaultTokenName 451 } 452 } 453 if kcfg.TokenLifetime == 0 { 454 if _, exists := defaultConfig["token_lifetime"]; exists { 455 kcfg.TokenLifetime = defaultConfig["token_lifetime"].(int) 456 } else { 457 kcfg.TokenLifetime = defaultTokenLifetime 458 } 459 } 460 if kcfg.ID == defaultKeyID { 461 if _, exists := defaultConfig["token_kid"]; exists { 462 kcfg.ID = defaultConfig["token_kid"].(string) 463 } 464 } 465 if err := kcfg.validate(); err != nil { 466 return nil, errors.ErrCryptoKeyConfigKeyInvalid.WithArgs(kcfg.Seq, err) 467 } 468 kcfg.parsed = true 469 } 470 return keys, nil 471 }