oauth2-proxy/pkg/authentication/hmacauth/hmacauth.go

238 lines
6.3 KiB
Go

package hmacauth
import (
"bytes"
"crypto"
"crypto/hmac"
"encoding/base64"
"errors"
"fmt"
"io"
"net/http"
"strings"
)
// HmacAuth signs outbound requests and authenticates inbound requests.
type HmacAuth interface {
// Produces the string that will be prefixed to the request body and
// used to generate the signature.
StringToSign(req *http.Request) string
// Adds a signature header to the request.
SignRequest(req *http.Request)
// Generates a signature for the request.
RequestSignature(req *http.Request) string
// Retrieves the signature included in the request header.
SignatureFromHeader(req *http.Request) string
// Authenticates the request, returning the result code, the signature
// from the header, and the locally-computed signature.
AuthenticateRequest(request *http.Request) (
result AuthenticationResult,
headerSignature, computedSignature string)
}
var supportedAlgorithms = map[string]crypto.Hash{
"md4": crypto.MD4,
"md5": crypto.MD5,
"sha1": crypto.SHA1,
"sha224": crypto.SHA224,
"sha256": crypto.SHA256,
"sha384": crypto.SHA384,
"sha512": crypto.SHA512,
"ripemd160": crypto.RIPEMD160,
}
var algorithmName map[crypto.Hash]string
func init() {
algorithmName = make(map[crypto.Hash]string)
for name, algorithm := range supportedAlgorithms {
algorithmName[algorithm] = name
// Make sure the algorithm is linked into the binary, per
// https://golang.org/pkg/crypto/#Hash.Available
//
// Note that both sides of the client/server connection must
// have an algorithm available in order to successfully
// authenticate using that algorithm
if !algorithm.Available() {
delete(supportedAlgorithms, name)
}
}
}
// DigestNameToCryptoHash returns the crypto.Hash value corresponding to the
// algorithm name, or an error if the algorithm is not supported.
func DigestNameToCryptoHash(name string) (result crypto.Hash, err error) {
var supported bool
if result, supported = supportedAlgorithms[name]; !supported {
err = errors.New("hmacauth: hash algorithm not supported: " +
name)
}
return
}
// CryptoHashToDigestName returns the algorithm name corresponding to the
// crypto.Hash ID, or an error if the algorithm is not supported.
func CryptoHashToDigestName(id crypto.Hash) (result string, err error) {
var supported bool
if result, supported = algorithmName[id]; !supported {
err = fmt.Errorf("hmacauth: unsupported crypto.Hash #%d", id)
}
return
}
type hmacAuth struct {
hash crypto.Hash
key []byte
header string
headers []string
}
// NewHmacAuth returns an HmacAuth object that can be used to sign or
// authenticate HTTP requests based on the supplied parameters.
func NewHmacAuth(hash crypto.Hash, key []byte, header string,
headers []string) HmacAuth {
if !hash.Available() {
var name string
var supported bool
if name, supported = algorithmName[hash]; !supported {
name = fmt.Sprintf("#%d", hash)
}
panic("hmacauth: hash algorithm " + name + " is unavailable")
}
canonicalHeaders := make([]string, len(headers))
for i, h := range headers {
canonicalHeaders[i] = http.CanonicalHeaderKey(h)
}
return &hmacAuth{hash, key, header, canonicalHeaders}
}
func (auth *hmacAuth) StringToSign(req *http.Request) string {
var buffer bytes.Buffer
_, _ = buffer.WriteString(req.Method)
_, _ = buffer.WriteString("\n")
for _, header := range auth.headers {
values := req.Header[header]
lastIndex := len(values) - 1
for i, value := range values {
_, _ = buffer.WriteString(value)
if i != lastIndex {
_, _ = buffer.WriteString(",")
}
}
_, _ = buffer.WriteString("\n")
}
_, _ = buffer.WriteString(req.URL.Path)
if req.URL.RawQuery != "" {
_, _ = buffer.WriteString("?")
_, _ = buffer.WriteString(req.URL.RawQuery)
}
if req.URL.Fragment != "" {
_, _ = buffer.WriteString("#")
_, _ = buffer.WriteString(req.URL.Fragment)
}
_, _ = buffer.WriteString("\n")
return buffer.String()
}
func (auth *hmacAuth) SignRequest(req *http.Request) {
req.Header.Set(auth.header, auth.RequestSignature(req))
}
func (auth *hmacAuth) RequestSignature(req *http.Request) string {
return requestSignature(auth, req, auth.hash)
}
func requestSignature(auth *hmacAuth, req *http.Request,
hashAlgorithm crypto.Hash) string {
h := hmac.New(hashAlgorithm.New, auth.key)
_, _ = h.Write([]byte(auth.StringToSign(req)))
if req.Body != nil {
reqBody, _ := io.ReadAll(req.Body)
req.Body = io.NopCloser(bytes.NewBuffer(reqBody))
_, _ = h.Write(reqBody)
}
var sig []byte
sig = h.Sum(sig)
return algorithmName[hashAlgorithm] + " " +
base64.StdEncoding.EncodeToString(sig)
}
func (auth *hmacAuth) SignatureFromHeader(req *http.Request) string {
return req.Header.Get(auth.header)
}
// AuthenticationResult is a code used to identify the outcome of
// HmacAuth.AuthenticateRequest().
type AuthenticationResult int
const (
// ResultNoSignature - the incoming result did not have a signature
// header.
ResultNoSignature AuthenticationResult = iota
// ResultInvalidFormat - the signature header was not parseable.
ResultInvalidFormat
// ResultUnsupportedAlgorithm - the signature header specified an
// unsupported algorithm.
ResultUnsupportedAlgorithm
// ResultMatch - the signature from the request header matched the
// locally-computed signature.
ResultMatch
// ResultMismatch - the signature from the request header did not match
// the locally-computed signature.
ResultMismatch
)
var validationResultStrings = []string{
"",
"ResultNoSignature",
"ResultInvalidFormat",
"ResultUnsupportedAlgorithm",
"ResultMatch",
"ResultMismatch",
}
func (result AuthenticationResult) String() string {
return validationResultStrings[result]
}
func (auth *hmacAuth) AuthenticateRequest(request *http.Request) (
result AuthenticationResult, headerSignature,
computedSignature string) {
headerSignature = auth.SignatureFromHeader(request)
if headerSignature == "" {
result = ResultNoSignature
return
}
components := strings.Split(headerSignature, " ")
if len(components) != 2 {
result = ResultInvalidFormat
return
}
algorithm, err := DigestNameToCryptoHash(components[0])
if err != nil {
result = ResultUnsupportedAlgorithm
return
}
computedSignature = requestSignature(auth, request, algorithm)
if hmac.Equal([]byte(headerSignature), []byte(computedSignature)) {
result = ResultMatch
} else {
result = ResultMismatch
}
return
}