Skip to content

Commit f9231bf

Browse files
Support FIPS-compatible API signing
Signed-off-by: andrijapanicsb <andrija.panic@gmail.com>
1 parent 76df16c commit f9231bf

6 files changed

Lines changed: 681 additions & 98 deletions

File tree

cmd/network.go

Lines changed: 213 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"bytes"
2222
"crypto/hmac"
2323
"crypto/sha1"
24+
"crypto/sha512"
2425
"encoding/base64"
2526
"encoding/json"
2627
"errors"
@@ -218,6 +219,195 @@ func encodeRequestParams(params url.Values) string {
218219
return buf.String()
219220
}
220221

222+
func cloneRequestParams(params url.Values) url.Values {
223+
cloned := make(url.Values)
224+
for key, values := range params {
225+
for _, value := range values {
226+
cloned.Add(key, value)
227+
}
228+
}
229+
return cloned
230+
}
231+
232+
func buildAPIRequestParams(r *Request, api string, args []string) url.Values {
233+
params := make(url.Values)
234+
params.Add("command", api)
235+
apiData := r.Config.GetCache()[api]
236+
for _, arg := range args {
237+
if apiData != nil {
238+
skip := false
239+
for _, fakeArg := range apiData.FakeArgs {
240+
if strings.HasPrefix(arg, fakeArg) {
241+
skip = true
242+
break
243+
}
244+
}
245+
if skip {
246+
continue
247+
}
248+
249+
}
250+
parts := strings.SplitN(arg, "=", 2)
251+
if len(parts) == 2 {
252+
key := parts[0]
253+
value := parts[1]
254+
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
255+
value = value[1 : len(value)-1]
256+
}
257+
if strings.HasPrefix(value, "@") {
258+
possibleFileName := value[1:]
259+
if fileInfo, err := os.Stat(possibleFileName); err == nil && !fileInfo.IsDir() {
260+
bytes, err := ioutil.ReadFile(possibleFileName)
261+
config.Debug()
262+
if err == nil {
263+
value = string(bytes)
264+
config.Debug("Content for argument ", key, " read from file: ", possibleFileName, " is: ", value)
265+
}
266+
}
267+
}
268+
params.Add(key, value)
269+
}
270+
}
271+
signatureversion := "3"
272+
expiresKey := "expires"
273+
params.Add("response", "json")
274+
params.Add("signatureversion", signatureversion)
275+
params.Add(expiresKey, time.Now().UTC().Add(15*time.Minute).Format(time.RFC3339))
276+
return params
277+
}
278+
279+
func signRequest(unsignedRequest, secretKey, algorithm string) (string, error) {
280+
signatureAlgorithm, err := config.NormalizeSignatureAlgorithm(algorithm)
281+
if err != nil {
282+
return "", err
283+
}
284+
285+
var signature []byte
286+
switch signatureAlgorithm {
287+
case config.SignatureAlgorithmHmacSHA1:
288+
mac := hmac.New(sha1.New, []byte(secretKey))
289+
mac.Write([]byte(strings.ToLower(unsignedRequest)))
290+
signature = mac.Sum(nil)
291+
case config.SignatureAlgorithmHmacSHA512:
292+
mac := hmac.New(sha512.New, []byte(secretKey))
293+
mac.Write([]byte(strings.ToLower(unsignedRequest)))
294+
signature = mac.Sum(nil)
295+
default:
296+
return "", errors.New("signature algorithm must be concrete")
297+
}
298+
return base64.StdEncoding.EncodeToString(signature), nil
299+
}
300+
301+
func executeSignedAPIRequest(r *Request, unsignedParams url.Values, algorithm string) (*http.Response, error) {
302+
params := cloneRequestParams(unsignedParams)
303+
encodedParams := encodeRequestParams(params)
304+
305+
signature, err := signRequest(encodedParams, r.Config.ActiveProfile.SecretKey, algorithm)
306+
if err != nil {
307+
return nil, err
308+
}
309+
if r.Config.Core.PostRequest {
310+
params.Add("signature", signature)
311+
} else {
312+
encodedParams = encodedParams + fmt.Sprintf("&signature=%s", url.QueryEscape(signature))
313+
params = nil
314+
}
315+
316+
requestURL := fmt.Sprintf("%s?%s", r.Config.ActiveProfile.URL, encodedParams)
317+
config.Debug("NewAPIRequest API request URL:", requestURL)
318+
return executeRequest(r, requestURL, params)
319+
}
320+
321+
func parseAPIResponse(body []byte) (map[string]interface{}, error) {
322+
var data map[string]interface{}
323+
if err := json.Unmarshal(body, &data); err != nil {
324+
return nil, errors.New("failed to decode response")
325+
}
326+
327+
if apiResponse := getResponseData(data); apiResponse != nil {
328+
if _, ok := apiResponse["errorcode"]; ok {
329+
return nil, fmt.Errorf("(HTTP %v, error code %v) %v", apiResponse["errorcode"], apiResponse["cserrorcode"], apiResponse["errortext"])
330+
}
331+
return apiResponse, nil
332+
}
333+
334+
return nil, errors.New("failed to decode response")
335+
}
336+
337+
func isAuthenticationFailure(statusCode int, err error) bool {
338+
if statusCode == http.StatusUnauthorized || statusCode == http.StatusForbidden {
339+
return true
340+
}
341+
if err == nil {
342+
return false
343+
}
344+
errText := strings.ToLower(err.Error())
345+
for _, marker := range []string{"signature", "authenticate", "authentication", "credential", "unauthoriz", "api key", "apikey"} {
346+
if strings.Contains(errText, marker) {
347+
return true
348+
}
349+
}
350+
return false
351+
}
352+
353+
func persistDetectedSignatureAlgorithm(r *Request, algorithm string) {
354+
r.Config.ActiveProfile.SignatureAlgorithm = algorithm
355+
if r.CredentialsSupplied {
356+
config.Debug("Credentials supplied on command-line, not persisting detected signature algorithm")
357+
return
358+
}
359+
r.Config.UpdateConfig("signaturealgorithm", algorithm, true)
360+
}
361+
362+
func detectSignatureAlgorithm(r *Request) (string, error) {
363+
attempts := []string{config.SignatureAlgorithmHmacSHA512, config.SignatureAlgorithmHmacSHA1}
364+
var lastErr error
365+
366+
for _, algorithm := range attempts {
367+
config.Debug("Trying API signature algorithm probe:", algorithm)
368+
params := buildAPIRequestParams(r, "listApis", []string{"listall=true"})
369+
params.Add("apiKey", r.Config.ActiveProfile.APIKey)
370+
response, err := executeSignedAPIRequest(r, params, algorithm)
371+
if err != nil {
372+
config.Debug("API signature algorithm probe failed before response for ", algorithm, ": ", err)
373+
lastErr = err
374+
continue
375+
}
376+
377+
body, _ := ioutil.ReadAll(response.Body)
378+
config.Debug("Signature algorithm probe response body:", string(body))
379+
if _, err := parseAPIResponse(body); err == nil {
380+
config.Debug("Selected API signature algorithm:", algorithm)
381+
persistDetectedSignatureAlgorithm(r, algorithm)
382+
return algorithm, nil
383+
} else {
384+
lastErr = err
385+
if isAuthenticationFailure(response.StatusCode, err) {
386+
config.Debug("API signature algorithm probe failed authentication for ", algorithm, ": ", err)
387+
} else {
388+
config.Debug("API signature algorithm probe failed with non-authentication error for ", algorithm, ": ", err)
389+
}
390+
}
391+
}
392+
393+
config.Debug("Signature algorithm autodetection failed; attempted algorithms:", strings.Join(attempts, ", "))
394+
if lastErr != nil {
395+
return "", lastErr
396+
}
397+
return "", errors.New("failed to detect signature algorithm")
398+
}
399+
400+
func activeSignatureAlgorithm(r *Request) (string, error) {
401+
signatureAlgorithm, err := config.NormalizeSignatureAlgorithm(r.Config.ActiveProfile.SignatureAlgorithm)
402+
if err != nil {
403+
return "", err
404+
}
405+
if signatureAlgorithm == config.SignatureAlgorithmAuto {
406+
return detectSignatureAlgorithm(r)
407+
}
408+
return signatureAlgorithm, nil
409+
}
410+
221411
func getResponseData(data map[string]interface{}) map[string]interface{} {
222412
for k := range data {
223413
if strings.HasSuffix(k, "response") {
@@ -276,72 +466,28 @@ func pollAsyncJob(r *Request, jobID string) (map[string]interface{}, error) {
276466

277467
// NewAPIRequest makes an API request to configured management server
278468
func NewAPIRequest(r *Request, api string, args []string, isAsync bool) (map[string]interface{}, error) {
279-
params := make(url.Values)
280-
params.Add("command", api)
281-
apiData := r.Config.GetCache()[api]
282-
for _, arg := range args {
283-
if apiData != nil {
284-
skip := false
285-
for _, fakeArg := range apiData.FakeArgs {
286-
if strings.HasPrefix(arg, fakeArg) {
287-
skip = true
288-
break
289-
}
290-
}
291-
if skip {
292-
continue
293-
}
294-
295-
}
296-
parts := strings.SplitN(arg, "=", 2)
297-
if len(parts) == 2 {
298-
key := parts[0]
299-
value := parts[1]
300-
if strings.HasPrefix(value, "\"") && strings.HasSuffix(value, "\"") {
301-
value = value[1 : len(value)-1]
302-
}
303-
if strings.HasPrefix(value, "@") {
304-
possibleFileName := value[1:]
305-
if fileInfo, err := os.Stat(possibleFileName); err == nil && !fileInfo.IsDir() {
306-
bytes, err := ioutil.ReadFile(possibleFileName)
307-
config.Debug()
308-
if err == nil {
309-
value = string(bytes)
310-
config.Debug("Content for argument ", key, " read from file: ", possibleFileName, " is: ", value)
311-
}
312-
}
313-
}
314-
params.Add(key, value)
315-
}
316-
}
317-
signatureversion := "3"
318-
expiresKey := "expires"
319-
params.Add("response", "json")
320-
params.Add("signatureversion", signatureversion)
321-
params.Add(expiresKey, time.Now().UTC().Add(15*time.Minute).Format(time.RFC3339))
469+
params := buildAPIRequestParams(r, api, args)
322470

323471
var encodedParams string
324472
var err error
473+
usingSessionAuth := false
325474

326475
if len(r.Config.ActiveProfile.APIKey) > 0 && len(r.Config.ActiveProfile.SecretKey) > 0 {
327476
apiKey := r.Config.ActiveProfile.APIKey
328-
secretKey := r.Config.ActiveProfile.SecretKey
329-
330477
if len(apiKey) > 0 {
331478
params.Add("apiKey", apiKey)
332479
}
333-
encodedParams = encodeRequestParams(params)
334-
335-
mac := hmac.New(sha1.New, []byte(secretKey))
336-
mac.Write([]byte(strings.ToLower(encodedParams)))
337-
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
338-
if r.Config.Core.PostRequest {
339-
params.Add("signature", signature)
340-
} else {
341-
encodedParams = encodedParams + fmt.Sprintf("&signature=%s", url.QueryEscape(signature))
342-
params = nil
480+
signatureAlgorithm, err := activeSignatureAlgorithm(r)
481+
if err != nil {
482+
return nil, err
343483
}
484+
response, err := executeSignedAPIRequest(r, params, signatureAlgorithm)
485+
if err != nil {
486+
return nil, err
487+
}
488+
return processAPIResponse(r, response, isAsync)
344489
} else if len(r.Config.ActiveProfile.Username) > 0 && len(r.Config.ActiveProfile.Password) > 0 {
490+
usingSessionAuth = true
345491
sessionKey, err := Login(r)
346492
if err != nil {
347493
return nil, err
@@ -367,7 +513,7 @@ func NewAPIRequest(r *Request, api string, args []string, isAsync bool) (map[str
367513
config.Debug("Credentials supplied on command-line, not falling back to login")
368514
}
369515

370-
if response.StatusCode == http.StatusUnauthorized && !r.CredentialsSupplied {
516+
if usingSessionAuth && response.StatusCode == http.StatusUnauthorized && !r.CredentialsSupplied {
371517
r.Client().Jar, _ = cookiejar.New(nil)
372518
sessionKey, err := Login(r)
373519
if err != nil {
@@ -384,27 +530,26 @@ func NewAPIRequest(r *Request, api string, args []string, isAsync bool) (map[str
384530
}
385531
}
386532

533+
return processAPIResponse(r, response, isAsync)
534+
}
535+
536+
func processAPIResponse(r *Request, response *http.Response, isAsync bool) (map[string]interface{}, error) {
387537
body, _ := ioutil.ReadAll(response.Body)
388538
config.Debug("NewAPIRequest response body:", string(body))
389539

390-
var data map[string]interface{}
391-
_ = json.Unmarshal([]byte(body), &data)
540+
apiResponse, err := parseAPIResponse(body)
541+
if err != nil {
542+
return nil, err
543+
}
392544

393545
if isAsync && r.Config.Core.AsyncBlock {
394-
if jobResponse := getResponseData(data); jobResponse != nil && jobResponse["jobid"] != nil {
395-
jobID := jobResponse["jobid"].(string)
546+
if apiResponse["jobid"] != nil {
547+
jobID := apiResponse["jobid"].(string)
396548
return pollAsyncJob(r, jobID)
397549
}
398550
}
399551

400-
if apiResponse := getResponseData(data); apiResponse != nil {
401-
if _, ok := apiResponse["errorcode"]; ok {
402-
return nil, fmt.Errorf("(HTTP %v, error code %v) %v", apiResponse["errorcode"], apiResponse["cserrorcode"], apiResponse["errortext"])
403-
}
404-
return apiResponse, nil
405-
}
406-
407-
return nil, errors.New("failed to decode response")
552+
return apiResponse, nil
408553
}
409554

410555
// we can implement further conditions to do POST or GET (or other http commands) here

0 commit comments

Comments
 (0)