Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 79
Support FIPS-compatible API signing#213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base:main
Are you sure you want to change the base?
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
File filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -21,6 +21,7 @@ | ||
| "bytes" | ||
| "crypto/hmac" | ||
| "crypto/sha1" | ||
| "crypto/sha512" | ||
| "encoding/base64" | ||
| "encoding/json" | ||
| "errors" | ||
| @@ -218,6 +219,202 @@ | ||
| return buf.String() | ||
| } | ||
| func cloneRequestParams(params url.Values) url.Values { | ||
| cloned := make(url.Values) | ||
| for key, values := range params { | ||
| for _, value := range values { | ||
| cloned.Add(key, value) | ||
| } | ||
| } | ||
| return cloned | ||
| } | ||
| func buildAPIRequestParams(r *Request, api string, args []string) url.Values { | ||
| params := make(url.Values) | ||
| params.Add("command", api) | ||
| apiData := r.Config.GetCache()[api] | ||
| for _, arg := range args { | ||
| if apiData != nil { | ||
| skip := false | ||
| for _, fakeArg := range apiData.FakeArgs { | ||
| if strings.HasPrefix(arg, fakeArg) { | ||
| skip = true | ||
| break | ||
| } | ||
| } | ||
| if skip { | ||
| continue | ||
| } | ||
| } | ||
| parts := strings.SplitN(arg, "=", 2) | ||
| if len(parts) == 2 { | ||
| key := parts[0] | ||
| value := parts[1] | ||
| if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") { | ||
| value = value[1 : len(value)-1] | ||
| } | ||
| if strings.HasPrefix(value, "@") { | ||
| possibleFileName := value[1:] | ||
| if fileInfo, err := os.Stat(possibleFileName); err == nil && !fileInfo.IsDir() { | ||
| bytes, err := ioutil.ReadFile(possibleFileName) | ||
| config.Debug() | ||
| if err == nil { | ||
| value = string(bytes) | ||
| config.Debug("Content for argument ", key, " read from file: ", possibleFileName, " is: ", value) | ||
| } | ||
| } | ||
| } | ||
| params.Add(key, value) | ||
| } | ||
| } | ||
| signatureversion := "3" | ||
| expiresKey := "expires" | ||
| params.Add("response", "json") | ||
| params.Add("signatureversion", signatureversion) | ||
| params.Add(expiresKey, time.Now().UTC().Add(15*time.Minute).Format(time.RFC3339)) | ||
| return params | ||
| } | ||
| func signRequest(unsignedRequest, secretKey, algorithm string) (string, error) { | ||
| signatureAlgorithm, err := config.NormalizeSignatureAlgorithm(algorithm) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| var signature []byte | ||
| switch signatureAlgorithm { | ||
| case config.SignatureAlgorithmHmacSHA1: | ||
| mac := hmac.New(sha1.New, []byte(secretKey)) | ||
| mac.Write([]byte(strings.ToLower(unsignedRequest))) | ||
| signature = mac.Sum(nil) | ||
| case config.SignatureAlgorithmHmacSHA512: | ||
| mac := hmac.New(sha512.New, []byte(secretKey)) | ||
| mac.Write([]byte(strings.ToLower(unsignedRequest))) | ||
| signature = mac.Sum(nil) | ||
| default: | ||
| return "", errors.New("signature algorithm must be concrete") | ||
| } | ||
| return base64.StdEncoding.EncodeToString(signature), nil | ||
| } | ||
| func executeSignedAPIRequest(r *Request, unsignedParams url.Values, algorithm string) (*http.Response, error) { | ||
| params := cloneRequestParams(unsignedParams) | ||
| encodedParams := encodeRequestParams(params) | ||
| signature, err := signRequest(encodedParams, r.Config.ActiveProfile.SecretKey, algorithm) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if r.Config.Core.PostRequest { | ||
| params.Add("signature", signature) | ||
| } else { | ||
| encodedParams = encodedParams + fmt.Sprintf("&signature=%s", url.QueryEscape(signature)) | ||
| params = nil | ||
| } | ||
| requestURL := fmt.Sprintf("%s?%s", r.Config.ActiveProfile.URL, encodedParams) | ||
| config.Debug("NewAPIRequest API request URL:", requestURL) | ||
| return executeRequest(r, requestURL, params) | ||
| } | ||
| func parseAPIResponse(body []byte) (map[string]interface{}, error) { | ||
| var data map[string]interface{} | ||
| if err := json.Unmarshal(body, &data); err != nil { | ||
| return nil, errors.New("failed to decode response") | ||
| } | ||
| if apiResponse := getResponseData(data); apiResponse != nil { | ||
| if _, ok := apiResponse["errorcode"]; ok { | ||
| return nil, fmt.Errorf("(HTTP %v, error code %v) %v", apiResponse["errorcode"], apiResponse["cserrorcode"], apiResponse["errortext"]) | ||
| } | ||
| return apiResponse, nil | ||
| } | ||
| return nil, errors.New("failed to decode response") | ||
| } | ||
| func isAuthenticationFailure(statusCode int, err error) bool { | ||
| if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden { | ||
| return true | ||
| } | ||
| if err == nil { | ||
| return false | ||
| } | ||
| errText := strings.ToLower(err.Error()) | ||
| for _, marker := range []string{"signature", "authenticate", "authentication", "credential", "unauthoriz", "api key", "apikey"} { | ||
| if strings.Contains(errText, marker) { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
| func persistDetectedSignatureAlgorithm(r *Request, algorithm string) { | ||
| r.Config.ActiveProfile.SignatureAlgorithm = algorithm | ||
| if r.CredentialsSupplied { | ||
| config.Debug("Credentials supplied on command-line, not persisting detected signature algorithm") | ||
| return | ||
| } | ||
| r.Config.UpdateConfig("signaturealgorithm", algorithm, true) | ||
| } | ||
| func detectSignatureAlgorithm(r *Request) (string, error) { | ||
| attempts := []string{config.SignatureAlgorithmHmacSHA512, config.SignatureAlgorithmHmacSHA1} | ||
| var lastErr error | ||
| for _, algorithm := range attempts { | ||
| config.Debug("Trying API signature algorithm probe:", algorithm) | ||
| params := buildAPIRequestParams(r, "listApis", []string{"listall=true"}) | ||
| params.Add("apiKey", r.Config.ActiveProfile.APIKey) | ||
| response, err := executeSignedAPIRequest(r, params, algorithm) | ||
| if err != nil { | ||
| config.Debug("API signature algorithm probe failed before response for ", algorithm, ": ", err) | ||
| lastErr = err | ||
| continue | ||
| } | ||
| defer response.Body.Close() | ||
| body, readErr := ioutil.ReadAll(response.Body) | ||
| if readErr != nil { | ||
| config.Debug("API signature algorithm probe failed reading response body for ", algorithm, ": ", readErr) | ||
| lastErr = readErr | ||
| continue | ||
| } | ||
| config.Debug("Signature algorithm probe response body:", string(body)) | ||
| if _, err := parseAPIResponse(body); err == nil { | ||
| config.Debug("Selected API signature algorithm:", algorithm) | ||
| persistDetectedSignatureAlgorithm(r, algorithm) | ||
| return algorithm, nil | ||
| } else { | ||
Check failure on line 389 in cmd/network.go
| ||
| lastErr = err | ||
| if isAuthenticationFailure(response.StatusCode, err) { | ||
| config.Debug("API signature algorithm probe failed authentication for ", algorithm, ": ", err) | ||
| } else { | ||
| config.Debug("API signature algorithm probe failed with non-authentication error for ", algorithm, ": ", err) | ||
| return "", err | ||
| } | ||
| } | ||
| } | ||
| config.Debug("Signature algorithm autodetection failed; attempted algorithms:", strings.Join(attempts, ", ")) | ||
| if lastErr != nil { | ||
| return "", lastErr | ||
| } | ||
| return "", errors.New("failed to detect signature algorithm") | ||
| } | ||
| func activeSignatureAlgorithm(r *Request) (string, error) { | ||
| signatureAlgorithm, err := config.NormalizeSignatureAlgorithm(r.Config.ActiveProfile.SignatureAlgorithm) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| if signatureAlgorithm == config.SignatureAlgorithmAuto { | ||
| return detectSignatureAlgorithm(r) | ||
| } | ||
| return signatureAlgorithm, nil | ||
| } | ||
| func getResponseData(data map[string]interface{}) map[string]interface{} { | ||
| for k := range data { | ||
| if strings.HasSuffix(k, "response") { | ||
| @@ -276,72 +473,28 @@ | ||
| // NewAPIRequest makes an API request to configured management server | ||
| func NewAPIRequest(r *Request, api string, args []string, isAsync bool) (map[string]interface{}, error) { | ||
| params := make(url.Values) | ||
| params.Add("command", api) | ||
| apiData := r.Config.GetCache()[api] | ||
| for _, arg := range args { | ||
| if apiData != nil { | ||
| skip := false | ||
| for _, fakeArg := range apiData.FakeArgs { | ||
| if strings.HasPrefix(arg, fakeArg) { | ||
| skip = true | ||
| break | ||
| } | ||
| } | ||
| if skip { | ||
| continue | ||
| } | ||
| } | ||
| parts := strings.SplitN(arg, "=", 2) | ||
| if len(parts) == 2 { | ||
| key := parts[0] | ||
| value := parts[1] | ||
| if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") { | ||
| value = value[1 : len(value)-1] | ||
| } | ||
| if strings.HasPrefix(value, "@") { | ||
| possibleFileName := value[1:] | ||
| if fileInfo, err := os.Stat(possibleFileName); err == nil && !fileInfo.IsDir() { | ||
| bytes, err := ioutil.ReadFile(possibleFileName) | ||
| config.Debug() | ||
| if err == nil { | ||
| value = string(bytes) | ||
| config.Debug("Content for argument ", key, " read from file: ", possibleFileName, " is: ", value) | ||
| } | ||
| } | ||
| } | ||
| params.Add(key, value) | ||
| } | ||
| } | ||
| signatureversion := "3" | ||
| expiresKey := "expires" | ||
| params.Add("response", "json") | ||
| params.Add("signatureversion", signatureversion) | ||
| params.Add(expiresKey, time.Now().UTC().Add(15*time.Minute).Format(time.RFC3339)) | ||
| params := buildAPIRequestParams(r, api, args) | ||
| var encodedParams string | ||
| var err error | ||
| usingSessionAuth := false | ||
| if len(r.Config.ActiveProfile.APIKey) > 0 && len(r.Config.ActiveProfile.SecretKey) > 0 { | ||
| apiKey := r.Config.ActiveProfile.APIKey | ||
| secretKey := r.Config.ActiveProfile.SecretKey | ||
| if len(apiKey) > 0 { | ||
| params.Add("apiKey", apiKey) | ||
| } | ||
| encodedParams = encodeRequestParams(params) | ||
| mac := hmac.New(sha1.New, []byte(secretKey)) | ||
| mac.Write([]byte(strings.ToLower(encodedParams))) | ||
| signature := base64.StdEncoding.EncodeToString(mac.Sum(nil)) | ||
| if r.Config.Core.PostRequest { | ||
| params.Add("signature", signature) | ||
| } else { | ||
| encodedParams = encodedParams + fmt.Sprintf("&signature=%s", url.QueryEscape(signature)) | ||
| params = nil | ||
| signatureAlgorithm, err := activeSignatureAlgorithm(r) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| response, err := executeSignedAPIRequest(r, params, signatureAlgorithm) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| return processAPIResponse(r, response, isAsync) | ||
| } else if len(r.Config.ActiveProfile.Username) > 0 && len(r.Config.ActiveProfile.Password) > 0 { | ||
| usingSessionAuth = true | ||
| sessionKey, err := Login(r) | ||
| if err != nil { | ||
| return nil, err | ||
| @@ -367,7 +520,7 @@ | ||
| config.Debug("Credentials supplied on command-line, not falling back to login") | ||
| } | ||
| if response.StatusCode == http.StatusUnauthorized && !r.CredentialsSupplied { | ||
| if usingSessionAuth && response.StatusCode == http.StatusUnauthorized && !r.CredentialsSupplied { | ||
| r.Client().Jar, _ = cookiejar.New(nil) | ||
| sessionKey, err := Login(r) | ||
| if err != nil { | ||
| @@ -384,27 +537,31 @@ | ||
| } | ||
| } | ||
| body, _ := ioutil.ReadAll(response.Body) | ||
| return processAPIResponse(r, response, isAsync) | ||
| } | ||
| func processAPIResponse(r *Request, response *http.Response, isAsync bool) (map[string]interface{}, error) { | ||
| defer response.Body.Close() | ||
| body, readErr := ioutil.ReadAll(response.Body) | ||
| if readErr != nil { | ||
| return nil, readErr | ||
| } | ||
| config.Debug("NewAPIRequest response body:", string(body)) | ||
Copilot marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| var data map[string]interface{} | ||
| _ = json.Unmarshal([]byte(body), &data) | ||
| apiResponse, err := parseAPIResponse(body) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| if isAsync && r.Config.Core.AsyncBlock { | ||
| if jobResponse := getResponseData(data); jobResponse != nil && jobResponse["jobid"] != nil { | ||
| jobID := jobResponse["jobid"].(string) | ||
| if apiResponse["jobid"] != nil { | ||
| jobID := apiResponse["jobid"].(string) | ||
| return pollAsyncJob(r, jobID) | ||
| } | ||
| } | ||
| if apiResponse := getResponseData(data); apiResponse != nil { | ||
| if _, ok := apiResponse["errorcode"]; ok { | ||
| return nil, fmt.Errorf("(HTTP %v, error code %v) %v", apiResponse["errorcode"], apiResponse["cserrorcode"], apiResponse["errortext"]) | ||
| } | ||
| return apiResponse, nil | ||
| } | ||
| return nil, errors.New("failed to decode response") | ||
| return apiResponse, nil | ||
| } | ||
| // we can implement further conditions to do POST or GET (or other http commands) here | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.