agent/plugins/downloadcontent/httpresource/handler/auth/digest/authorization.go (205 lines of code) (raw):
/*
* Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License"). You may not
* use this file except in compliance with the License. A copy of the
* License is located at
*
* http://aws.amazon.com/apache2.0/
*
* or in the "license" file accompanying this file. This file 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 digest defines functionality required to support digest authorization
package digest
import (
"bytes"
"crypto/md5"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"hash"
"io"
"net/http"
"reflect"
"strconv"
"strings"
)
var (
md5Writer = md5.New()
sha256Writer = sha256.New()
)
// Possible values of the qop parameter
const (
QOP_AUTH = "auth"
QOP_AUTH_INT = "auth-int"
)
// Supported encryption algorithms
const (
MD5 = "MD5"
MD5_SESS = "MD5-SESS"
SHA256 = "SHA-256"
SHA256_SESS = "SHA-256-SESS"
)
var createDigestAuthorization = newDigestAuthorization
// Authorization defines all required attributes for the "Authorization" request header when performing digest authz
// https://tools.ietf.org/html/rfc7616#section-3.4
type Authorization struct {
username string `quoted:"true"`
realm string `quoted:"true"`
nonce string `quoted:"true"`
uri string `quoted:"true"`
response string `quoted:"false"`
algorithm string `quoted:"false"`
cnonce string `quoted:"true"`
opaque string `quoted:"true"`
qop string `quoted:"false"`
nc int `quoted:"false"`
userhash bool `quoted:"false"`
}
// newDigestAuthorization creates a new Authorization instance and computes all required fields
func newDigestAuthorization(
username string,
password string,
req *http.Request,
authProps *WwwAuthenticateProps,
) (*Authorization, error) {
authz := &Authorization{
realm: authProps.realm,
nonce: authProps.nonce,
uri: req.URL.Path,
algorithm: authProps.algorithm,
opaque: authProps.opaque,
qop: authProps.qop[0],
nc: 1,
userhash: authProps.userhash,
}
return authz.computeFields(username, password, req)
}
// computeFields computes and sets the userhash, cnonce and response attributes
func (authz *Authorization) computeFields(
username string,
password string,
req *http.Request,
) (*Authorization, error) {
if authz.userhash {
hashedUsername, err := authz.computeUserhash(username)
if err != nil {
return nil, err
}
authz.username = hashedUsername
} else {
authz.username = username
}
cnonce, err := authz.generateCNonce()
if err != nil {
return nil, err
}
authz.cnonce = cnonce
response, err := authz.computeResponse(username, password, req)
if err != nil {
return nil, err
}
authz.response = response
return authz, nil
}
// generateCNonce generates the client nonce
func (authz *Authorization) generateCNonce() (string, error) {
b := make([]byte, 8)
_, err := io.ReadFull(rand.Reader, b)
if err != nil {
return "", err
}
return fmt.Sprintf("%x", b)[:16], nil
}
// computeUserhash hashes the username: https://tools.ietf.org/html/rfc7616#section-3.4.4
func (authz *Authorization) computeUserhash(username string) (string, error) {
return computeHash(fmt.Sprintf("%s:%s", username, authz.realm), authz.algorithm)
}
// computeHA1 computes the A1 hash: https://tools.ietf.org/html/rfc7616#section-3.4.2
func (authz *Authorization) computeHA1(username string, password string) (string, error) {
ha1, err := computeHash(fmt.Sprintf("%s:%s:%s", username, authz.realm, password), authz.algorithm)
if err != nil {
return "", err
}
if strings.Contains(strings.ToUpper(authz.algorithm), "SESS") {
return computeHash(fmt.Sprintf("%s:%s:%s", ha1, authz.nonce, authz.cnonce), authz.algorithm)
}
return ha1, nil
}
// computeHA2 computes the A2 hash: https://tools.ietf.org/html/rfc7616#section-3.4.3
func (authz *Authorization) computeHA2(method string, uri string, body string) (string, error) {
if authz.qop == QOP_AUTH_INT {
bodyHash, err := computeHash(body, authz.algorithm)
if err != nil {
return "", nil
}
return computeHash(fmt.Sprintf("%s:%s:%s", method, uri, bodyHash), authz.algorithm)
}
return computeHash(fmt.Sprintf("%s:%s", method, uri), authz.algorithm)
}
// computeResponse computes the response hash: https://tools.ietf.org/html/rfc7616#section-3.4.1
func (authz *Authorization) computeResponse(username string, password string, req *http.Request) (string, error) {
ha1, err := authz.computeHA1(username, password)
if err != nil {
return "", err
}
buf := new(strings.Builder)
if req.Body != nil {
_, err = io.Copy(buf, req.Body)
if err != nil {
return "", err
}
}
ha2, err := authz.computeHA2(req.Method, req.URL.Path, buf.String())
if err != nil {
return "", err
}
return computeHash(fmt.Sprintf(
"%s:%s:%08x:%s:%s:%s",
ha1,
authz.nonce,
authz.nc,
authz.cnonce,
authz.qop,
ha2,
), authz.algorithm)
}
// String generates the "Authorization" request header field value: https://tools.ietf.org/html/rfc7616#section-3.4
func (authz *Authorization) String() string {
var buffer bytes.Buffer
buffer.WriteString("Digest ")
value := reflect.ValueOf(*authz)
typeOfS := value.Type()
for i := 0; i < value.NumField(); i++ {
fieldName := typeOfS.Field(i).Name
switch fieldName {
case "nc":
buffer.WriteString(fmt.Sprintf("%s=%08x, ", fieldName, value.Field(i).Int()))
case "userhash":
buffer.WriteString(fmt.Sprintf("%s=%s, ", fieldName, strconv.FormatBool(value.Field(i).Bool())))
default:
if v, ok := typeOfS.Field(i).Tag.Lookup("quoted"); ok && v == "true" {
buffer.WriteString(fmt.Sprintf("%s=\"%s\", ", fieldName, value.Field(i).String()))
} else {
buffer.WriteString(fmt.Sprintf("%s=%s, ", fieldName, value.Field(i).String()))
}
}
}
return strings.TrimSuffix(buffer.String(), ", ")
}
// computeHash hashes the given str using the specified algorithm
func computeHash(str string, algorithm string) (string, error) {
algorithm = strings.ToUpper(algorithm)
var hashMethod hash.Hash
switch algorithm {
case MD5, MD5_SESS:
hashMethod = md5Writer
case SHA256, SHA256_SESS:
hashMethod = sha256Writer
default:
return "", fmt.Errorf("Algorithm %s not supported", algorithm)
}
hashMethod.Reset()
_, err := io.WriteString(hashMethod, str)
if err != nil {
return "", err
}
return hex.EncodeToString(hashMethod.Sum(nil)), nil
}
// Authorize orchestrates the digest authorization process. If authz is not required, an empty string is returned,
// otherwise the "Authorization" header field value is returned
func Authorize(username, password string, req *http.Request, client *http.Client) (string, error) {
resp, err := client.Do(req)
if err != nil {
return "", err
}
if resp.Body != nil {
defer resp.Body.Close()
}
if resp.StatusCode != http.StatusUnauthorized {
return "", fmt.Errorf("Unexpected HTTP response code received %d instead of 401 Unauthorized. The requested resources might not require authorization", resp.StatusCode)
}
if len(resp.Header["Www-Authenticate"]) > 0 {
authProps := newWwwAuthenticateProps(resp.Header["Www-Authenticate"][0])
digest, err := createDigestAuthorization(username, password, req, authProps)
if err != nil {
return "", err
}
return digest.String(), nil
}
return "", nil
}