// // Copyright 2021 The Sigstore Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package app import ( "encoding/base64" "errors" "fmt" "log" "os" "path/filepath" "strconv" "strings" "time" "github.com/sigstore/rekor/pkg/pki" "github.com/sigstore/rekor/pkg/sharding" "github.com/spf13/pflag" validator "github.com/asaskevich/govalidator" ) type FlagType string const ( uuidFlag FlagType = "uuid" shaFlag FlagType = "sha" emailFlag FlagType = "email" operatorFlag FlagType = "operator" logIndexFlag FlagType = "logIndex" pkiFormatFlag FlagType = "pkiFormat" typeFlag FlagType = "type" fileFlag FlagType = "file" urlFlag FlagType = "url" fileOrURLFlag FlagType = "fileOrURL" multiFileOrURLFlag FlagType = "multiFileOrURL" oidFlag FlagType = "oid" formatFlag FlagType = "format" timeoutFlag FlagType = "timeout" base64Flag FlagType = "base64" uintFlag FlagType = "uint" ) type newPFlagValueFunc func() pflag.Value var pflagValueFuncMap map[FlagType]newPFlagValueFunc // TODO: unit tests for all of this func initializePFlagMap() { pflagValueFuncMap = map[FlagType]newPFlagValueFunc{ uuidFlag: func() pflag.Value { // this validates a UUID with or without a prepended TreeID; // the UUID corresponds to the merkle leaf hash of entries, // which is represented by a 64 character hexadecimal string return valueFactory(uuidFlag, validateID, "") }, shaFlag: func() pflag.Value { // this validates a valid sha256 checksum which is optionally prefixed with 'sha256:' return valueFactory(shaFlag, validateSHAValue, "") }, operatorFlag: func() pflag.Value { // this validates a valid operator name operatorFlagValidator := func(val string) error { o := struct { Value string `valid:"in(and|or)"` }{val} _, err := validator.ValidateStruct(o) return err } return valueFactory(operatorFlag, operatorFlagValidator, "") }, emailFlag: func() pflag.Value { // this validates an email address emailValidator := func(val string) error { if !validator.IsEmail(val) { return fmt.Errorf("'%v' is not a valid email address", val) } return nil } return valueFactory(emailFlag, emailValidator, "") }, logIndexFlag: func() pflag.Value { // this checks for a valid integer >= 0 return valueFactory(logIndexFlag, validateUint, "") }, pkiFormatFlag: func() pflag.Value { // this ensures a PKI implementation exists for the requested format pkiFormatValidator := func(val string) error { if !validator.IsIn(val, pki.SupportedFormats()...) { return fmt.Errorf("'%v' is not a valid pki format", val) } return nil } return valueFactory(pkiFormatFlag, pkiFormatValidator, "pgp") }, typeFlag: func() pflag.Value { // this ensures the type of the log entry matches a type supported in the CLI return valueFactory(typeFlag, validateTypeFlag, "rekord") }, fileFlag: func() pflag.Value { // this validates that the file exists and can be opened by the current uid return valueFactory(fileFlag, validateFile, "") }, urlFlag: func() pflag.Value { // this validates that the string is a valid http/https URL httpHTTPSValidator := func(val string) error { if !validator.IsURL(val) { return fmt.Errorf("'%v' is not a valid url", val) } if !(strings.HasPrefix(val, "http") || strings.HasPrefix(val, "https")) { return errors.New("URL must be for http or https scheme") } return nil } return valueFactory(urlFlag, httpHTTPSValidator, "") }, fileOrURLFlag: func() pflag.Value { // applies logic of fileFlag OR urlFlag validators from above return valueFactory(fileOrURLFlag, validateFileOrURL, "") }, multiFileOrURLFlag: func() pflag.Value { // applies logic of fileFlag OR urlFlag validators from above for multi file and URL return multiValueFactory(multiFileOrURLFlag, validateFileOrURL, []string{}) }, oidFlag: func() pflag.Value { // this validates for an OID, which is a sequence of positive integers separated by periods return valueFactory(oidFlag, validateOID, "") }, formatFlag: func() pflag.Value { // this validates the output format requested formatValidator := func(val string) error { if !validator.IsIn(val, "json", "default", "tle") { return fmt.Errorf("'%v' is not a valid output format", val) } return nil } return valueFactory(formatFlag, formatValidator, "") }, timeoutFlag: func() pflag.Value { // this validates the timeout is >= 0 return valueFactory(formatFlag, validateTimeout, "") }, base64Flag: func() pflag.Value { // This validates the string is in base64 format return valueFactory(base64Flag, validateBase64, "") }, uintFlag: func() pflag.Value { // This validates the string is in base64 format return valueFactory(uintFlag, validateUint, "") }, } } // NewFlagValue creates a new pflag.Value for the specified type with the specified default value. // If a default value is not desired, pass "" for defaultVal. func NewFlagValue(flagType FlagType, defaultVal string) pflag.Value { valFunc := pflagValueFuncMap[flagType] val := valFunc() if defaultVal != "" { if err := val.Set(defaultVal); err != nil { log.Fatal(fmt.Errorf("initializing flag: %w", err)) } } return val } type validationFunc func(string) error func valueFactory(flagType FlagType, v validationFunc, defaultVal string) pflag.Value { return &baseValue{ flagType: flagType, validationFunc: v, value: defaultVal, } } func multiValueFactory(flagType FlagType, v validationFunc, defaultVal []string) pflag.Value { return &multiBaseValue{ flagType: flagType, validationFunc: v, value: defaultVal, } } // multiBaseValue implements pflag.Value type multiBaseValue struct { flagType FlagType value []string validationFunc validationFunc } func (b *multiBaseValue) String() string { return strings.Join(b.value, ",") } // Type returns the type of this Value func (b multiBaseValue) Type() string { return string(b.flagType) } func (b *multiBaseValue) Set(value string) error { if err := b.validationFunc(value); err != nil { return err } b.value = append(b.value, value) return nil } // baseValue implements pflag.Value type baseValue struct { flagType FlagType value string validationFunc validationFunc } // Type returns the type of this Value func (b baseValue) Type() string { return string(b.flagType) } // String returns the string representation of this Value func (b baseValue) String() string { return b.value } // Set validates the provided string against the appropriate validation rule // for b.flagType; if the string validates, it is stored in the Value and nil is returned. // Otherwise the validation error is returned but the state of the Value is not changed. func (b *baseValue) Set(s string) error { if err := b.validationFunc(s); err != nil { return err } b.value = s return nil } // isURL returns true if the supplied value is a valid URL and false otherwise func isURL(v string) bool { valGen := pflagValueFuncMap[urlFlag] return valGen().Set(v) == nil } // validateSHAValue ensures that the supplied string matches the following formats: // [sha512:]<128 hexadecimal characters> // [sha256:]<64 hexadecimal characters> // [sha1:]<40 hexadecimal characters> // where [sha256:] and [sha1:] are optional func validateSHAValue(v string) error { err := validateSHA1Value(v) if err == nil { return nil } err = validateSHA256Value(v) if err == nil { return nil } err = validateSHA512Value(v) if err == nil { return nil } return fmt.Errorf("error parsing %v flag: %w", shaFlag, err) } // validateFileOrURL ensures the provided string is either a valid file path that can be opened or a valid URL func validateFileOrURL(v string) error { valGen := pflagValueFuncMap[fileFlag] if valGen().Set(v) == nil { return nil } valGen = pflagValueFuncMap[urlFlag] return valGen().Set(v) } // validateID ensures the ID is either an EntryID (TreeID + UUID) or a UUID func validateID(v string) error { if len(v) != sharding.EntryIDHexStringLen && len(v) != sharding.UUIDHexStringLen { return fmt.Errorf("ID len error, expected %v (EntryID) or %v (UUID) but got len %v for ID %v", sharding.EntryIDHexStringLen, sharding.UUIDHexStringLen, len(v), v) } if !validator.IsHexadecimal(v) { return fmt.Errorf("invalid uuid: %v", v) } return nil } // validateOID ensures that the supplied string is a valid ASN.1 object identifier func validateOID(v string) error { values := strings.Split(v, ".") for _, value := range values { if !validator.IsNumeric(value) { return fmt.Errorf("field '%v' is not a valid number", value) } } return nil } // validateTimeout ensures that the supplied string is a valid time.Duration value >= 0 func validateTimeout(v string) error { duration, err := time.ParseDuration(v) if err != nil { return err } if duration < 0 { return errors.New("timeout must be a positive value") } return nil } // validateBase64 ensures that the supplied string is valid base64 encoded data func validateBase64(v string) error { _, err := base64.StdEncoding.DecodeString(v) return err } // validateTypeFlag ensures that the string is in the format type(\.version)? and // that one of the types requested is implemented func validateTypeFlag(v string) error { _, _, err := ParseTypeFlag(v) return err } // validateUint ensures that the supplied string is a valid unsigned integer >= 0 func validateUint(v string) error { i, err := strconv.Atoi(v) if err != nil { return err } if i < 0 { return fmt.Errorf("invalid unsigned int: %v", v) } return nil } // validateFile ensures that the supplied string is a valid path to a file that exists func validateFile(v string) error { fileInfo, err := os.Stat(filepath.Clean(v)) if err != nil { return err } if fileInfo.IsDir() { return errors.New("path to a directory was provided") } return nil }