the unsupported authorization type error only echoed the algorithm back in ArgumentValue, while s3 returns the whole Authorization header there. the parse error now carries the raw header and the s3 error mapping uses it, which also lets the bats test skipped for #1705 run again. Fixes #2489
248 lines
6.7 KiB
Go
248 lines
6.7 KiB
Go
// Copyright 2026 Versity Software
|
|
// This file is 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 sigv4auth
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
)
|
|
|
|
const (
|
|
AlgorithmHMACSHA256 = "AWS4-HMAC-SHA256"
|
|
Terminal = "aws4_request"
|
|
ServiceS3 = "s3"
|
|
ServiceIAM = "iam"
|
|
ServiceSTS = "sts"
|
|
|
|
ISO8601Format = "20060102T150405Z"
|
|
YYYYMMDD = "20060102"
|
|
|
|
// HeaderSecurityToken is the header a temporary credential's
|
|
// SessionToken is presented in, matching AWS's X-Amz-Security-Token.
|
|
HeaderSecurityToken = "X-Amz-Security-Token"
|
|
|
|
// TempAccessKeyIDPrefix marks temporary credentials minted by
|
|
// AssumeRoleWithWebIdentity, matching AWS's ASIA… convention that
|
|
// distinguishes them from long-term AKIA… access keys.
|
|
TempAccessKeyIDPrefix = "ASIA"
|
|
)
|
|
|
|
// IsTempAccessKeyID reports whether accessKeyID is a temporary (session)
|
|
// credential rather than a long-term AKIA… access key.
|
|
func IsTempAccessKeyID(accessKeyID string) bool {
|
|
return strings.HasPrefix(accessKeyID, TempAccessKeyIDPrefix)
|
|
}
|
|
|
|
type ParseErrorKind string
|
|
|
|
const (
|
|
ErrInvalidAuthorizationHeader ParseErrorKind = "invalid_authorization_header"
|
|
ErrUnsupportedAuthorizationVersion ParseErrorKind = "unsupported_authorization_version"
|
|
ErrInvalidAuthorizationType ParseErrorKind = "invalid_authorization_type"
|
|
ErrMissingComponents ParseErrorKind = "missing_components"
|
|
ErrMissingCredential ParseErrorKind = "missing_credential"
|
|
ErrMissingSignedHeaders ParseErrorKind = "missing_signed_headers"
|
|
ErrMissingSignature ParseErrorKind = "missing_signature"
|
|
ErrMalformedComponent ParseErrorKind = "malformed_component"
|
|
ErrMalformedCredential ParseErrorKind = "malformed_credential"
|
|
ErrIncorrectService ParseErrorKind = "incorrect_service"
|
|
ErrIncorrectTerminal ParseErrorKind = "incorrect_terminal"
|
|
ErrInvalidDateFormat ParseErrorKind = "invalid_date_format"
|
|
)
|
|
|
|
type ParseError struct {
|
|
Kind ParseErrorKind
|
|
Input string
|
|
Value string
|
|
Expected string
|
|
Actual string
|
|
}
|
|
|
|
func (e *ParseError) Error() string {
|
|
if e == nil {
|
|
return ""
|
|
}
|
|
switch e.Kind {
|
|
case ErrIncorrectService, ErrIncorrectTerminal:
|
|
return fmt.Sprintf("sigv4 %s: expected %q, got %q", e.Kind, e.Expected, e.Actual)
|
|
case ErrInvalidAuthorizationType, ErrMalformedComponent, ErrInvalidDateFormat:
|
|
return fmt.Sprintf("sigv4 %s: %q", e.Kind, e.Value)
|
|
default:
|
|
return string(e.Kind)
|
|
}
|
|
}
|
|
|
|
// AuthData is the parsed authorization data from an AWS SigV4 Authorization header.
|
|
type AuthData struct {
|
|
Algorithm string
|
|
Access string
|
|
Region string
|
|
Service string
|
|
SignedHeaders string
|
|
Signature string
|
|
Date string
|
|
}
|
|
|
|
type CredentialsScope struct {
|
|
Access string
|
|
Date string
|
|
Region string
|
|
Service string
|
|
}
|
|
|
|
// HexBytes returns the hex byte representation used by AWS-style diagnostic
|
|
// signature mismatch errors.
|
|
func HexBytes(s string) string {
|
|
b := []byte(s)
|
|
|
|
parts := make([]string, len(b))
|
|
for i, v := range b {
|
|
parts[i] = fmt.Sprintf("%02x", v)
|
|
}
|
|
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
func PayloadSHA256Hex(payload []byte) string {
|
|
hashedPayload := sha256.Sum256(payload)
|
|
return hex.EncodeToString(hashedPayload[:])
|
|
}
|
|
|
|
// ParseAuthorization parses and validates an AWS SigV4 Authorization header.
|
|
// The credential scope service must match expectedService.
|
|
func ParseAuthorization(authorization, expectedService string) (AuthData, error) {
|
|
a := AuthData{}
|
|
|
|
authParts := strings.SplitN(authorization, " ", 2)
|
|
for i, el := range authParts {
|
|
if strings.Contains(el, " ") {
|
|
authParts[i] = removeSpace(el)
|
|
}
|
|
}
|
|
|
|
if len(authParts) < 2 {
|
|
return a, &ParseError{Kind: ErrInvalidAuthorizationHeader, Input: authorization}
|
|
}
|
|
|
|
algo := authParts[0]
|
|
if algo == "AWS" {
|
|
return a, &ParseError{Kind: ErrUnsupportedAuthorizationVersion, Value: algo}
|
|
}
|
|
if algo != AlgorithmHMACSHA256 {
|
|
return a, &ParseError{Kind: ErrInvalidAuthorizationType, Input: authorization, Value: algo}
|
|
}
|
|
|
|
kvPairs := strings.Split(authParts[1], ",")
|
|
if len(kvPairs) != 3 {
|
|
return a, &ParseError{Kind: ErrMissingComponents, Input: authorization}
|
|
}
|
|
|
|
var access, region, service, signedHeaders, signature, date string
|
|
for i, kv := range kvPairs {
|
|
keyValue := strings.Split(kv, "=")
|
|
if len(keyValue) != 2 {
|
|
return a, &ParseError{Kind: ErrMalformedComponent, Value: kv}
|
|
}
|
|
key, value := keyValue[0], keyValue[1]
|
|
switch i {
|
|
case 0:
|
|
if key != "Credential" {
|
|
return a, &ParseError{Kind: ErrMissingCredential}
|
|
}
|
|
case 1:
|
|
if key != "SignedHeaders" {
|
|
return a, &ParseError{Kind: ErrMissingSignedHeaders}
|
|
}
|
|
case 2:
|
|
if key != "Signature" {
|
|
return a, &ParseError{Kind: ErrMissingSignature}
|
|
}
|
|
}
|
|
|
|
switch key {
|
|
case "Credential":
|
|
creds, err := ParseCredentials(value, expectedService)
|
|
if err != nil {
|
|
return a, err
|
|
}
|
|
access = creds.Access
|
|
date = creds.Date
|
|
region = creds.Region
|
|
service = creds.Service
|
|
case "SignedHeaders":
|
|
signedHeaders = value
|
|
case "Signature":
|
|
signature = value
|
|
}
|
|
}
|
|
|
|
return AuthData{
|
|
Algorithm: algo,
|
|
Access: access,
|
|
Region: region,
|
|
Service: service,
|
|
SignedHeaders: signedHeaders,
|
|
Signature: signature,
|
|
Date: date,
|
|
}, nil
|
|
}
|
|
|
|
func ParseCredentials(input, expectedService string) (*CredentialsScope, error) {
|
|
creds := strings.Split(input, "/")
|
|
if len(creds) != 5 {
|
|
return nil, &ParseError{Kind: ErrMalformedCredential, Input: input}
|
|
}
|
|
if creds[3] != expectedService {
|
|
return nil, &ParseError{
|
|
Kind: ErrIncorrectService,
|
|
Input: input,
|
|
Expected: expectedService,
|
|
Actual: creds[3],
|
|
}
|
|
}
|
|
if creds[4] != Terminal {
|
|
return nil, &ParseError{
|
|
Kind: ErrIncorrectTerminal,
|
|
Input: input,
|
|
Expected: Terminal,
|
|
Actual: creds[4],
|
|
}
|
|
}
|
|
if _, err := time.Parse(YYYYMMDD, creds[1]); err != nil {
|
|
return nil, &ParseError{Kind: ErrInvalidDateFormat, Input: input, Value: creds[1]}
|
|
}
|
|
|
|
return &CredentialsScope{
|
|
Access: creds[0],
|
|
Date: creds[1],
|
|
Region: creds[2],
|
|
Service: creds[3],
|
|
}, nil
|
|
}
|
|
|
|
func removeSpace(str string) string {
|
|
var b strings.Builder
|
|
b.Grow(len(str))
|
|
for _, ch := range str {
|
|
if !unicode.IsSpace(ch) {
|
|
b.WriteRune(ch)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|