Files
versity-versitygw/internal/sigv4auth/auth.go
T
dualfroz 8271bea5c1 fix: return full authorization header in unsupported authorization type error
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
2026-10-07 19:37:46 +02:00

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()
}