awsig

package module
v0.0.0-...-3a31a81 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 15, 2026 License: MIT Imports: 27 Imported by: 0

README

awsig is a Go library implementing server-side verification of AWS Signature Version 4 (SigV4) and AWS Signature Version 2 (SigV2) meant for building AWS-compatible services.

compatibility

regular signed requests UNSIGNED-PAYLOAD STREAMING-UNSIGNED-PAYLOAD-TRAILER STREAMING-AWS4-HMAC-SHA256-PAYLOAD STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER presigned presigned (POST)
SigV2 n/a n/a n/a n/a n/a n/a
SigV4 unimplemented unimplemented

This package was written with S3 and certain security and performance characteristics in mind, but it should work for other service clones as well.

TODO

  • verify the returned errors (error codes) with the real AWS (preferably S3) or a close clone, like Ceph
  • do shallow test runs with all publicly available AWS SDKs
    • SDKs act differently with and without TLS and with different checksum options

example usage

package …

import (
	…
	"github.com/amwolff/awsig"
)

// (1) Implement awsig.CredentialsProvider:
type MyCredentialsProvider struct {
	secretAccessKeys map[string]string
}

func (p *MyCredentialsProvider) Provide(ctx context.Context, accessKeyID string) (secretAccessKey string, _ error) {
	secretAccessKey, ok := p.secretAccessKeys[accessKeyID]
	if !ok {
		return "", awsig.ErrInvalidAccessKeyID
	}
	return secretAccessKey, nil
}

func NewMyCredentialsProvider() *MyCredentialsProvider {
	return &MyCredentialsProvider{
		secretAccessKeys: map[string]string{
			"AKIAIOSFODNN7EXAMPLE": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
		},
	}
}

// (2) Create a combined V2/V4 verifier for S3 in us-east-1.
// You can also create a standalone V2-only or V4-only verifier:
v2v4 := awsig.NewV2V4(NewMyCredentialsProvider(), awsig.V4Config{
	Region:  "us-east-1",
	Service: "s3",
})

func …(w http.ResponseWriter, r *http.Request) {
	// (3) Verify the incoming request:
	vr, err := v2v4.Verify(r, "virtual-hosted-bucket-indication-for-v2")
	if err != nil {
		…
	}
	// (4) If the request is a multipart/form-data POST, you can access the parsed form values:
	form := vr.PostForm()
	//
	// Important: if you intend to read the body, use vr.Reader() instead of r.Body.
	//
	// (5) Declare which checksums you want to be verified/computed:
	sha1Req, err := awsig.NewChecksumRequest(awsig.AlgorithmSHA1, "ziEPrgmMDfQDTAAAQZuYfMjU4uc=")
	if err != nil {
		…
	}
	crc32Req, err := awsig.NewTrailingChecksumRequest(awsig.AlgorithmCRC32)
	if err != nil {
		…
	}
	// (6) Read the body. Notes:
	//
	// - requested checksums are verified automatically
	// - if the request includes a trailing checksum header, at least one checksum must be requested
	// - if not explicitly requested:
	//   - MD5 is always computed and available after reading
	//   - SHA256 is computed and available after reading, depending on the request type
	body, err := vr.Reader(sha1Req, crc32Req)
	if err != nil {
		…
	}

	_, err = io.Copy(…, body) // copy or do something else with the body
	if err != nil {
		…
	}

	// (7) Access computed/verified checksums as needed:
	checksums, err := body.Checksums()
	if err != nil {
		…
	}
	for algo, sum := range checksums {
		log.Printf("%s: %x", algo, sum)
	}

	// Perform additional application logic as needed…
}

A complete working example can be found in awsig_test.go (Example).

Documentation

Overview

Package awsig implements server-side verification of

- AWS Signature Version 4 (SigV4) - AWS Signature Version 2 (SigV2)

meant for building AWS-compatible services.

Example
package main

import (
	"context"
	"fmt"
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"time"
)

// (1) Implement awsig.CredentialsProvider:
type (
	MyAuthData struct {
		AccessKeyID string
	}
	MyCredentialsProvider struct {
		secretAccessKeys map[string]string
	}
)

func (p *MyCredentialsProvider) Provide(_ context.Context, accessKeyID string) (secretAccessKey string, _ MyAuthData, _ error) {
	var data MyAuthData

	secretAccessKey, ok := p.secretAccessKeys[accessKeyID]
	if !ok {
		return "", data, ErrInvalidAccessKeyID
	}

	data.AccessKeyID = accessKeyID

	return secretAccessKey, data, nil
}

func NewMyCredentialsProvider() *MyCredentialsProvider {
	return &MyCredentialsProvider{
		secretAccessKeys: map[string]string{
			"AKIAIOSFODNN7EXAMPLE": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
		},
	}
}

func main() {
	// (2) Create a combined V2/V4 verifier for S3 in us-east-1.
	// You can also create a standalone V2-only or V4-only verifier:
	v2v4 := NewV2V4(NewMyCredentialsProvider(), V4Config{
		Region:  "us-east-1",
		Service: "s3",
	})

	h := func(w http.ResponseWriter, r *http.Request) {
		// (3) Verify the incoming request:
		vr, err := v2v4.Verify(r, "virtual-hosted-bucket-indication-for-v2")
		if err != nil {
			errToHTTPError(w, err)
			return
		}
		fmt.Printf("verified the incoming request with Access Key ID=%s\n", vr.AuthData().AccessKeyID)
		// (4) If the request is a multipart/form-data POST, you can access the parsed form values:
		if vr.PostForm() != nil {
			fmt.Println("this request is a multipart/form-data POST")
		}
		//
		// Important: if you intend to read the body, use vr.Reader() instead of r.Body.
		//
		// (5) Declare which checksums you want to be verified/computed:
		var reqs []ChecksumRequest
		{
			req, err := NewChecksumRequest(AlgorithmSHA1, "CgqfKmdylCVXq1NV12r0Qvj2XgE=")
			if err != nil {
				errToHTTPError(w, err)
				return
			}
			reqs = append(reqs, req)
		}
		if r.Header.Get("x-amz-trailer") == "x-amz-checksum-crc32" {
			req, err := NewTrailingChecksumRequest(AlgorithmCRC32)
			if err != nil {
				errToHTTPError(w, err)
				return
			}
			reqs = append(reqs, req)
		}
		// (6) Read the body. Notes:
		//
		// - requested checksums are verified automatically
		// - if the request includes a trailing checksum header, at least one checksum must be requested
		// - if not explicitly requested:
		//   - MD5 is always computed and available after reading
		//   - SHA256 is computed and available after reading, depending on the request type
		body, err := vr.Reader(reqs...)
		if err != nil {
			errToHTTPError(w, err)
			return
		}

		_, err = io.Copy(w, body) // copy or do something else with the body
		if err != nil {
			errToHTTPError(w, err)
			return
		}

		// (7) Access computed/verified checksums as needed:
		checksums, err := body.Checksums()
		if err != nil {
			errToHTTPError(w, err)
			return
		}
		for algo, sum := range checksums {
			fmt.Printf("%s of the received content is %x\n", algo, sum)
		}

		// Perform additional application logic as needed…
	}

	rec := httptest.NewRecorder()
	req := httptest.NewRequest(http.MethodPut, "https://bucket.s3-compatible.provider.com/object.txt", strings.NewReader("Hello, World!"))
	req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20000101/us-east-1/s3/aws4_request, SignedHeaders=content-length;host;x-amz-content-sha256;x-amz-date, Signature=f7e9ff55dfc3b67c3ad92147a3056687e986e907e36f7971eaea693065bf999e")
	req.Header.Set("Content-Length", "13")
	req.Header.Set("X-Amz-Content-Sha256", "dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f")
	req.Header.Set("X-Amz-Date", "20000101T000000Z")

	serveHTTPAt(v2v4, h, rec, req, time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC))

	fmt.Println(rec.Body.String())
}

func errToHTTPError(w http.ResponseWriter, _ error) {
	defaultCode := http.StatusInternalServerError
	// TODO: match awsig errors to HTTP codes
	http.Error(w, http.StatusText(defaultCode), defaultCode)
}

func serveHTTPAt[T any](v *V2V4[T], h http.HandlerFunc, w http.ResponseWriter, r *http.Request, t time.Time) {
	prev2 := v.v2.now
	prev4 := v.v4.now

	now := func() time.Time { return t }

	v.v2.now = now
	v.v4.now = now

	h(w, r)

	v.v2.now = prev2
	v.v4.now = prev4
}
Output:

verified the incoming request with Access Key ID=AKIAIOSFODNN7EXAMPLE
md5 of the received content is 65a8e27d8879283831b664bd8b7f0ad4
sha1 of the received content is 0a0a9f2a6772942557ab5355d76af442f8f65e01
sha256 of the received content is dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f
Hello, World!

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrAccessDenied indicates the AccessDenied error code.
	ErrAccessDenied = errors.New("access denied")
	// ErrAuthorizationHeaderMalformed indicates the AuthorizationHeaderMalformed error code.
	ErrAuthorizationHeaderMalformed = errors.New("the authorization header that you provided is not valid")
	// ErrBadDigest indicates the BadDigest error code.
	ErrBadDigest = errors.New("the Content-MD5 or checksum value that you specified did not match what the server received")
	// ErrContentLengthWithTransferEncoding indicates that both the Content-Length and Transfer-Encoding headers were provided.
	ErrContentLengthWithTransferEncoding = errors.New("the Content-Length and Transfer-Encoding headers must not both be provided")
	// ErrEntityTooLarge indicates the EntityTooLarge error code.
	ErrEntityTooLarge = errors.New("your proposed upload exceeds the maximum allowed object size")
	// ErrEntityTooSmall indicates the EntityTooSmall error code.
	ErrEntityTooSmall = errors.New("your proposed upload is smaller than the minimum allowed object size")
	// ErrInvalidAccessKeyID indicates the InvalidAccessKeyID error code.
	ErrInvalidAccessKeyID = errors.New("the AWS access key ID that you provided does not exist in our records")
	// ErrInvalidArgument indicates the InvalidArgument error code.
	ErrInvalidArgument = errors.New("invalid argument")
	// ErrInvalidDateHeader indicates that the Date or X-Amz-Date header is not valid.
	ErrInvalidDateHeader = errors.New("AWS authentication requires a valid Date or x-amz-date header")
	// ErrInvalidDigest indicates the InvalidDigest error code.
	ErrInvalidDigest = errors.New("the Content-MD5 or checksum value that you specified is not valid")
	// ErrInvalidPOSTDate indicates that the X-Amz-Date form field is malformed.
	ErrInvalidPOSTDate = errors.New("the X-Amz-Date form field does not contain a valid date")
	// ErrInvalidPresignedDate indicates that the date provided in a presigned URL is malformed.
	ErrInvalidPresignedDate = errors.New("the X-Amz-Date query parameter does not contain a valid date")
	// ErrInvalidPresignedExpiration indicates that the expiration provided in a presigned URL is not a valid integer.
	ErrInvalidPresignedExpiration = errors.New("the X-Amz-Expires query parameter does not contain a valid integer")
	// ErrInvalidPresignedXAmzContentSHA256 is returned when attempting to read from the body of a presigned request
	// that provided an invalid value for the X-Amz-Content-Sha256 header.
	ErrInvalidPresignedXAmzContentSHA256 = errors.New("the provided 'x-amz-content-sha256' header does not match what was computed")
	// ErrInvalidRequest indicates the InvalidRequest error code.
	ErrInvalidRequest = errors.New("invalid request")
	// ErrInvalidSignature indicates the InvalidSignature error code.
	ErrInvalidSignature = errors.New("the request signature that the server calculated does not match the signature that you provided")
	// ErrInvalidXAmzContentSHA256 indicates that the X-Amz-Content-Sha256 header has an invalid value.
	ErrInvalidXAmzContentSHA256 = errors.New("the x-amz-content-sha256 header does not contain a valid value")
	// ErrInvalidXAmzDecodedContentSHA256 indicates that the X-Amz-Decoded-Content-Length header has an invalid value.
	ErrInvalidXAmzDecodedContentSHA256 = errors.New("the x-amz-decoded-content-length header does not contain a valid integer")
	// ErrMalformedPOSTRequest indicates that a POST request is malformed.
	ErrMalformedPOSTRequest = errors.New("unable to parse multipart form data")
	// ErrMissingContentLength indicates the MissingContentLength error code.
	ErrMissingContentLength = errors.New("you must provide the Content-Length HTTP header")
	// ErrMissingPOSTPolicy indicates that the POST policy was not provided.
	ErrMissingPOSTPolicy = errors.New("the Policy form field is missing")
	// ErrMissingSecurityHeader indicates the MissingSecurityHeader error code.
	ErrMissingSecurityHeader = errors.New("your request is missing a required header")
	// ErrNegativePresignedExpiration indicates that the expiration provided in a presigned URL is negative integer.
	ErrNegativePresignedExpiration = errors.New("the X-Amz-Expires query parameter is negative")
	// ErrNotImplemented indicates the NotImplemented error code.
	ErrNotImplemented = errors.New("a header that you provided implies functionality that is not implemented")
	// ErrPresignedExpirationTooLarge indicates that the expiration provided in a presigned URL is too large.
	ErrPresignedExpirationTooLarge = errors.New("the X-Amz-Expires query parameter exceeds the maximum of 604800 seconds (7 days)")
	// ErrRequestExpired indicates that the request's expiration date has passed.
	ErrRequestExpired = errors.New("the request has expired")
	// ErrRequestNotYetValid indicates that the request's date is in the future.
	ErrRequestNotYetValid = errors.New("the request is not yet valid")
	// ErrRequestTimeTooSkewed indicates the RequestTimeTooSkewed error code.
	ErrRequestTimeTooSkewed = errors.New("the difference between the request time and the server's time is too large")
	// ErrSignatureDoesNotMatch indicates the SignatureDoesNotMatch error code.
	ErrSignatureDoesNotMatch = errors.New("the request signature that the server calculated does not match the signature that you provided")
	// ErrUnsupportedSignature indicates the UnsupportedSignature error code.
	ErrUnsupportedSignature = errors.New("the provided request is signed with an unsupported STS Token version or the signature version is not supported")
)

Functions

This section is empty.

Types

type ChecksumAlgorithm

type ChecksumAlgorithm int

ChecksumAlgorithm represents different checksum algorithms supported by this package.

const (
	// AlgorithmCRC32 represents the CRC-32 checksum algorithm.
	AlgorithmCRC32 ChecksumAlgorithm = iota
	// AlgorithmCRC32C represents the CRC-32C checksum algorithm.
	AlgorithmCRC32C
	// AlgorithmCRC64NVME represents the CRC-64/NVME checksum algorithm.
	AlgorithmCRC64NVME
	// AlgorithmMD5 represents the MD5 checksum algorithm.
	AlgorithmMD5
	// AlgorithmSHA1 represents the SHA-1 checksum algorithm.
	AlgorithmSHA1
	// AlgorithmSHA256 represents the SHA-256 checksum algorithm.
	AlgorithmSHA256
)

func (ChecksumAlgorithm) String

func (a ChecksumAlgorithm) String() string

type ChecksumMismatch

type ChecksumMismatch struct {
	Algorithm          ChecksumAlgorithm
	ClientChecksum     []byte
	CalculatedChecksum []byte

	// IsContentSHA256 indicates whether the expected checksum was specified in the X-Amz-Content-Sha256 header.
	// This can only be true if ChecksumAlgorithm is AlgorithmSHA256.
	IsContentSHA256 bool
}

ChecksumMismatch contains information about a mismatch between a computed checksum and client-provided checksum.

type ChecksumMismatchError

type ChecksumMismatchError struct {
	Mismatches []ChecksumMismatch
}

ChecksumMismatchError is the error used when a set of computed checksums don't match those provided by the client.

func (ChecksumMismatchError) Error

func (err ChecksumMismatchError) Error() string

Error implements the error interface.

type ChecksumRequest

type ChecksumRequest struct {
	// contains filtered or unexported fields
}

ChecksumRequest represents a request to compute or verify a specific checksum.

func NewChecksumRequest

func NewChecksumRequest(algorithm ChecksumAlgorithm, encodedValue string) (ChecksumRequest, error)

NewChecksumRequest creates a new ChecksumRequest with the specified algorithm and encoded value.

func NewTrailingChecksumRequest

func NewTrailingChecksumRequest(algorithm ChecksumAlgorithm) (ChecksumRequest, error)

NewTrailingChecksumRequest creates a new ChecksumRequest for trailing checksum verification.

type CredentialsProvider

type CredentialsProvider[T any] interface {
	Provide(ctx context.Context, accessKeyID string) (secretAccessKey string, data T, _ error)
}

CredentialsProvider is the interface that all users of this package must implement. Provide is called by signature verifiers. If the given accessKeyID is unknown to the implementation, it should return zero values alongside the ErrInvalidAccessKeyID error.

type PostForm

type PostForm map[string][]PostFormElement

PostForm maps a string key to a list of values. It represents a parsed multipart form data. Unlike in url.Values, the keys in PostForm are case-insensitive.

func (PostForm) Add

func (f PostForm) Add(key string, value PostFormElement)

Add adds the value to key. It appends to any existing values associated with key.

func (PostForm) Clone

func (f PostForm) Clone() PostForm

Clone returns a copy of f or nil if f is nil.

func (PostForm) Del

func (f PostForm) Del(key string)

Del deletes the values associated with key.

func (PostForm) FileName

func (f PostForm) FileName() string

FileName returns the filename from the "file" field of the form.

func (PostForm) Get

func (f PostForm) Get(key string) PostFormElement

Get gets the first value associated with the given key. If there are no values associated with the key, Get returns the empty PostFormElement.

func (PostForm) Has

func (f PostForm) Has(key string) bool

Has checks whether a given key is set.

func (PostForm) Set

func (f PostForm) Set(key string, value PostFormElement)

Set sets the key to value. It replaces any existing values.

func (PostForm) Values

func (f PostForm) Values(key string) []PostFormElement

Values returns all values associated with the given key. The returned slice is not a copy.

type PostFormElement

type PostFormElement struct {
	Headers textproto.MIMEHeader
	Value   string
}

PostFormElement represents a single element in a multipart form.

type Reader

type Reader interface {
	io.Reader
	// Checksums returns computed checksums of the read data.
	// Checksums are only available after reaching EOF.
	// If called before reaching EOF, it returns an error.
	//
	// If not previously requested:
	//
	// - MD5 is always computed
	// - SHA-256 is computed if the request has a hashed payload
	Checksums() (map[ChecksumAlgorithm][]byte, error)
}

Reader is an io.Reader to be used to read the body of a verified request with optional checksum computation and auto-verification.

type V2

type V2[T any] struct {
	// contains filtered or unexported fields
}

V2 implements AWS Signature Version 2 verification.

func NewV2

func NewV2[T any](provider CredentialsProvider[T]) *V2[T]

NewV2 creates a new V2 with the given provider.

func (*V2[T]) Verify

func (v2 *V2[T]) Verify(r *http.Request, virtualHostedBucket string) (*V2VerifiedRequest[T], error)

Verify verifies the AWS Signature Version 2 for the given request and returns a verified request.

type V2V4

type V2V4[T any] struct {
	// contains filtered or unexported fields
}

V2V4 implements AWS Signature Version 2 and AWS Signature Version 4 verification.

func NewV2V4

func NewV2V4[T any](provider CredentialsProvider[T], v4Config V4Config) *V2V4[T]

NewV2V4 creates a new V2V4 with the given provider and v4Config.

func (*V2V4[T]) Verify

func (v2v4 *V2V4[T]) Verify(r *http.Request, virtualHostedBucket string) (VerifiedRequest[T], error)

Verify automatically detects and verifies either AWS Signature Version 2 or AWS Signature Version 4 for the given request and returns a verified request.

type V2VerifiedRequest

type V2VerifiedRequest[T any] struct {
	// contains filtered or unexported fields
}

V2VerifiedRequest implements VerifiedRequest for AWS Signature Version 2.

func (*V2VerifiedRequest[T]) AuthData

func (vr *V2VerifiedRequest[T]) AuthData() T

AuthData implements VerifiedRequest.

func (*V2VerifiedRequest[T]) PostForm

func (vr *V2VerifiedRequest[T]) PostForm() PostForm

PostForm implements VerifiedRequest.

func (*V2VerifiedRequest[T]) Reader

func (vr *V2VerifiedRequest[T]) Reader(reqs ...ChecksumRequest) (Reader, error)

Reader implements VerifiedRequest.

type V4

type V4[T any] struct {
	// contains filtered or unexported fields
}

V4 implements AWS Signature Version 4 verification.

func NewV4

func NewV4[T any](provider CredentialsProvider[T], config V4Config) *V4[T]

NewV4 creates a new V4 with the given provider and config.

func (*V4[T]) Verify

func (v4 *V4[T]) Verify(r *http.Request) (*V4VerifiedRequest[T], error)

Verify verifies the AWS Signature Version 4 for the given request and returns a verified request.

type V4Config

type V4Config struct {
	Region                 string
	Service                string
	SkipRegionVerification bool
}

V4Config contains configuration for V4.

type V4VerifiedRequest

type V4VerifiedRequest[T any] struct {
	// contains filtered or unexported fields
}

V4VerifiedRequest implements VerifiedRequest for AWS Signature Version 4.

func (*V4VerifiedRequest[T]) AuthData

func (vr *V4VerifiedRequest[T]) AuthData() T

AuthData implements VerifiedRequest.

func (*V4VerifiedRequest[T]) PostForm

func (vr *V4VerifiedRequest[T]) PostForm() PostForm

PostForm implements VerifiedRequest.

func (*V4VerifiedRequest[T]) Reader

func (vr *V4VerifiedRequest[T]) Reader(reqs ...ChecksumRequest) (Reader, error)

Reader implements VerifiedRequest.

type VerifiedRequest

type VerifiedRequest[T any] interface {
	// AuthData returns data collected while providing credentials
	// via CredentialsProvider. AuthData's type is determined by the
	// generic type parameter T to allow flexibility. For example, a
	// caller that would need to access Access Key ID could call one
	// of the verifiers with T reserving space for Access Key ID,
	// which would be filled by the CredentialsProvider
	// implementation.
	AuthData() T
	// PostForm returns the parsed multipart form data if the
	// request is a POST with "multipart/form-data" Content-Type.
	PostForm() PostForm
	// Reader returns a Reader to read the body of the verified
	// request. Reader can be called multiple times, but only the
	// first call can request checksums. Checksum requests must have
	// distinct algorithms. If the request includes a trailing
	// checksum header, at least one checksum must be requested.
	Reader(...ChecksumRequest) (Reader, error)
}

VerifiedRequest represents a successfully verified AWS Signature Version 4 or AWS Signature Version 2 request.

Directories

Path Synopsis
cmd
s3tests command
s3tests is a mock S3 server using awsig that helps running a subset of ceph/s3-tests against it.
s3tests is a mock S3 server using awsig that helps running a subset of ceph/s3-tests against it.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL