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  }