Feat: Add support for AWS Signing Version 4A (#5489)

AWS is rolling out an extension to SigV4 called Signature Version 4A (SigV4A)
which enables signatures that are valid in more than one AWS Region. This is
required for signing multi-region API requests, for example with Amazon S3
Multi-Region Access Points (MRAP). This commit lets OPA use an S3 MRAP as
a bundle source.

The SigV4A implementation used in this commit is a modified version of
internal code from the `aws-sdk-go-v2` project:
https://github.com/aws/aws-sdk-go-v2/tree/93c3f18/internal/v4a

This commit also refactors the existing V4 signing code into a shared
`internal/providers/aws` package that contains both the existing V4 signing
code as well as the V4A signing code added by this PR.

Fixes #5429

Signed-off-by: Jay Wineinger <jawineinger@spscommerce.com>
This commit is contained in:
Jay Wineinger
2022-12-19 15:11:50 -06:00
committed by GitHub
parent 30356a7fc2
commit ea39bf35b9
18 changed files with 1520 additions and 97 deletions
+9 -6
View File
@@ -449,15 +449,18 @@ Consider requiring authentication in order to prevent unauthorized read access t
#### AWS Signature
OPA will authenticate with an [AWS4 HMAC](https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html) signature. Several methods of obtaining the
necessary credentials are available; exactly one must be specified to use the AWS signature
authentication method.
OPA will authenticate with an [AWS Version 4](https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html) or version 4A signature. While version 4 is the default, version 4A must be used when making requests that might be handled by more than one region, such as an [S3 Multi-Region Access Point](https://docs.aws.amazon.com/AmazonS3/latest/userguide/MultiRegionAccessPoints.html). You must use version 4A for this or requests will fail when routed to a different region than the one indicated in a version 4 signature. Furthermore, using version 4a also requires that temporary credentials are retrieved from a [regional AWS STS endpoint](https://docs.aws.amazon.com/sdkref/latest/guide/feature-sts-regionalized-endpoints.html), rather than the global STS endpoint.
Several methods of obtaining the necessary credentials are available; exactly one must be specified to use the AWS signature authentication method.
The AWS service for which to sign the request can be specified in the `service` field. If omitted, the default is `s3`.
| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `services[_].credentials.s3_signing.service` | `string` | No | The AWS service to sign requests with, eg `execute-api` or `s3`. Default: `s3` |
The AWS signature version to sign the request with can be specified in the `signature_version` field. If omitted, the default is `4`. The only other valid value is `4a`.
| Field | Type | Required | Description |
|--------------------------------------------------------| --- | --- |--------------------------------------------------------------------------------|
| `services[_].credentials.s3_signing.service` | `string` | No | The AWS service to sign requests with, eg `execute-api` or `s3`. Default: `s3` |
| `services[_].credentials.s3_signing.signature_version` | `string` | No | The AWS signature version to sign requests with, eg `4` or `4a`. Default: `4` |
##### Using Static Environment Credentials
If specifying `environment_credentials`, OPA will expect to find environment variables
+3
View File
@@ -0,0 +1,3 @@
AWS SDK for Go
Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Copyright 2014-2015 Stripe, Inc.
+30
View File
@@ -0,0 +1,30 @@
package crypto
import "fmt"
// ConstantTimeByteCompare is a constant-time byte comparison of x and y. This function performs an absolute comparison
// if the two byte slices assuming they represent a big-endian number.
//
// error if len(x) != len(y)
// -1 if x < y
// 0 if x == y
// +1 if x > y
func ConstantTimeByteCompare(x, y []byte) (int, error) {
if len(x) != len(y) {
return 0, fmt.Errorf("slice lengths do not match")
}
xLarger, yLarger := 0, 0
for i := 0; i < len(x); i++ {
xByte, yByte := int(x[i]), int(y[i])
x := ((yByte - xByte) >> 8) & 1
y := ((xByte - yByte) >> 8) & 1
xLarger |= x &^ yLarger
yLarger |= y &^ xLarger
}
return xLarger - yLarger, nil
}
@@ -0,0 +1,52 @@
package crypto
import (
"bytes"
"math/big"
"testing"
)
func TestConstantTimeByteCompare(t *testing.T) {
cases := []struct {
x, y []byte
r int
expectErr bool
}{
{x: []byte{}, y: []byte{}, r: 0},
{x: []byte{40}, y: []byte{30}, r: 1},
{x: []byte{30}, y: []byte{40}, r: -1},
{x: []byte{60, 40, 30, 10, 20}, y: []byte{50, 30, 20, 0, 10}, r: 1},
{x: []byte{50, 30, 20, 0, 10}, y: []byte{60, 40, 30, 10, 20}, r: -1},
{x: nil, y: []byte{}, r: 0},
{x: []byte{}, y: nil, r: 0},
{x: []byte{}, y: []byte{10}, expectErr: true},
{x: []byte{10}, y: []byte{}, expectErr: true},
{x: []byte{10, 20}, y: []byte{10}, expectErr: true},
}
for _, tt := range cases {
compare, err := ConstantTimeByteCompare(tt.x, tt.y)
if (err != nil) != tt.expectErr {
t.Fatalf("expectErr=%v, got %v", tt.expectErr, err)
}
if e, a := tt.r, compare; e != a {
t.Errorf("expect %v, got %v", e, a)
}
}
}
func BenchmarkConstantTimeCompare(b *testing.B) {
x, y := big.NewInt(1023), big.NewInt(1024)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = ConstantTimeByteCompare(x.Bytes(), y.Bytes())
}
}
func BenchmarkCompare(b *testing.B) {
x, y := big.NewInt(1023).Bytes(), big.NewInt(1024).Bytes()
b.ResetTimer()
for i := 0; i < b.N; i++ {
bytes.Compare(x, y)
}
}
+113
View File
@@ -0,0 +1,113 @@
package crypto
import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/hmac"
"encoding/asn1"
"encoding/binary"
"fmt"
"hash"
"math"
"math/big"
)
type ecdsaSignature struct {
R, S *big.Int
}
// ECDSAKey takes the given elliptic curve, and private key (d) byte slice
// and returns the private ECDSA key.
func ECDSAKey(curve elliptic.Curve, d []byte) *ecdsa.PrivateKey {
return ECDSAKeyFromPoint(curve, (&big.Int{}).SetBytes(d))
}
// ECDSAKeyFromPoint takes the given elliptic curve and point and returns the
// private and public keypair
func ECDSAKeyFromPoint(curve elliptic.Curve, d *big.Int) *ecdsa.PrivateKey {
pX, pY := curve.ScalarBaseMult(d.Bytes())
privKey := &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{
Curve: curve,
X: pX,
Y: pY,
},
D: d,
}
return privKey
}
// ECDSAPublicKey takes the provide curve and (x, y) coordinates and returns
// *ecdsa.PublicKey. Returns an error if the given points are not on the curve.
func ECDSAPublicKey(curve elliptic.Curve, x, y []byte) (*ecdsa.PublicKey, error) {
xPoint := (&big.Int{}).SetBytes(x)
yPoint := (&big.Int{}).SetBytes(y)
if !curve.IsOnCurve(xPoint, yPoint) {
return nil, fmt.Errorf("point(%v, %v) is not on the given curve", xPoint.String(), yPoint.String())
}
return &ecdsa.PublicKey{
Curve: curve,
X: xPoint,
Y: yPoint,
}, nil
}
// VerifySignature takes the provided public key, hash, and asn1 encoded signature and returns
// whether the given signature is valid.
func VerifySignature(key *ecdsa.PublicKey, hash []byte, signature []byte) (bool, error) {
var ecdsaSignature ecdsaSignature
_, err := asn1.Unmarshal(signature, &ecdsaSignature)
if err != nil {
return false, err
}
return ecdsa.Verify(key, hash, ecdsaSignature.R, ecdsaSignature.S), nil
}
// HMACKeyDerivation provides an implementation of a NIST-800-108 of a KDF (Key Derivation Function) in Counter Mode.
// For the purposes of this implantation HMAC is used as the PRF (Pseudorandom function), where the value of
// `r` is defined as a 4 byte counter.
func HMACKeyDerivation(hash func() hash.Hash, bitLen int, key []byte, label, context []byte) ([]byte, error) {
// verify that we won't overflow the counter
n := int64(math.Ceil((float64(bitLen) / 8) / float64(hash().Size())))
if n > 0x7FFFFFFF {
return nil, fmt.Errorf("unable to derive key of size %d using 32-bit counter", bitLen)
}
// verify the requested bit length is not larger then the length encoding size
if int64(bitLen) > 0x7FFFFFFF {
return nil, fmt.Errorf("bitLen is greater than 32-bits")
}
fixedInput := bytes.NewBuffer(nil)
fixedInput.Write(label)
fixedInput.WriteByte(0x00)
fixedInput.Write(context)
if err := binary.Write(fixedInput, binary.BigEndian, int32(bitLen)); err != nil {
return nil, fmt.Errorf("failed to write bit length to fixed input string: %v", err)
}
var output []byte
h := hmac.New(hash, key)
for i := int64(1); i <= n; i++ {
h.Reset()
if err := binary.Write(h, binary.BigEndian, int32(i)); err != nil {
return nil, err
}
_, err := h.Write(fixedInput.Bytes())
if err != nil {
return nil, err
}
output = append(output, h.Sum(nil)...)
}
return output[:bitLen/8], nil
}
+277
View File
@@ -0,0 +1,277 @@
package crypto
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"io"
"testing"
)
func TestECDSAPublicKeyDerivation_P256(t *testing.T) {
d := []byte{
0xc9, 0x80, 0x68, 0x98, 0xa0, 0x33, 0x49, 0x16, 0xc8, 0x60, 0x74, 0x88, 0x80, 0xa5, 0x41, 0xf0,
0x93, 0xb5, 0x79, 0xa9, 0xb1, 0xf3, 0x29, 0x34, 0xd8, 0x6c, 0x36, 0x3c, 0x39, 0x80, 0x03, 0x57,
}
x := []byte{
0xd0, 0x72, 0x0d, 0xc6, 0x91, 0xaa, 0x80, 0x09, 0x6b, 0xa3, 0x2f, 0xed, 0x1c, 0xb9, 0x7c, 0x2b,
0x62, 0x06, 0x90, 0xd0, 0x6d, 0xe0, 0x31, 0x7b, 0x86, 0x18, 0xd5, 0xce, 0x65, 0xeb, 0x72, 0x8f,
}
y := []byte{
0x96, 0x81, 0xb5, 0x17, 0xb1, 0xcd, 0xa1, 0x7d, 0x0d, 0x83, 0xd3, 0x35, 0xd9, 0xc4, 0xa8, 0xa9,
0xa9, 0xb0, 0xb1, 0xb3, 0xc7, 0x10, 0x6d, 0x8f, 0x3c, 0x72, 0xbc, 0x50, 0x93, 0xdc, 0x27, 0x5f,
}
testKeyDerivation(t, elliptic.P256(), d, x, y)
}
func TestECDSAPublicKeyDerivation_P384(t *testing.T) {
d := []byte{
0x53, 0x94, 0xf7, 0x97, 0x3e, 0xa8, 0x68, 0xc5, 0x2b, 0xf3, 0xff, 0x8d, 0x8c, 0xee, 0xb4, 0xdb,
0x90, 0xa6, 0x83, 0x65, 0x3b, 0x12, 0x48, 0x5d, 0x5f, 0x62, 0x7c, 0x3c, 0xe5, 0xab, 0xd8, 0x97,
0x8f, 0xc9, 0x67, 0x3d, 0x14, 0xa7, 0x1d, 0x92, 0x57, 0x47, 0x93, 0x16, 0x62, 0x49, 0x3c, 0x37,
}
x := []byte{
0xfd, 0x3c, 0x84, 0xe5, 0x68, 0x9b, 0xed, 0x27, 0x0e, 0x60, 0x1b, 0x3d, 0x80, 0xf9, 0x0d, 0x67,
0xa9, 0xae, 0x45, 0x1c, 0xce, 0x89, 0x0f, 0x53, 0xe5, 0x83, 0x22, 0x9a, 0xd0, 0xe2, 0xee, 0x64,
0x56, 0x11, 0xfa, 0x99, 0x36, 0xdf, 0xa4, 0x53, 0x06, 0xec, 0x18, 0x06, 0x67, 0x74, 0xaa, 0x24,
}
y := []byte{
0xb8, 0x3c, 0xa4, 0x12, 0x6c, 0xfc, 0x4c, 0x4d, 0x1d, 0x18, 0xa4, 0xb6, 0xc2, 0x1c, 0x7f, 0x69,
0x9d, 0x51, 0x23, 0xdd, 0x9c, 0x24, 0xf6, 0x6f, 0x83, 0x38, 0x46, 0xee, 0xb5, 0x82, 0x96, 0x19,
0x6b, 0x42, 0xec, 0x06, 0x42, 0x5d, 0xb5, 0xb7, 0x0a, 0x4b, 0x81, 0xb7, 0xfc, 0xf7, 0x05, 0xa0,
}
testKeyDerivation(t, elliptic.P384(), d, x, y)
}
func TestECDSAKnownSigningValue_P256(t *testing.T) {
d := []byte{
0x51, 0x9b, 0x42, 0x3d, 0x71, 0x5f, 0x8b, 0x58, 0x1f, 0x4f, 0xa8, 0xee, 0x59, 0xf4, 0x77, 0x1a,
0x5b, 0x44, 0xc8, 0x13, 0x0b, 0x4e, 0x3e, 0xac, 0xca, 0x54, 0xa5, 0x6d, 0xda, 0x72, 0xb4, 0x64,
}
testKnownSigningValue(t, elliptic.P256(), d)
}
func TestECDSAKnownSigningValue_P384(t *testing.T) {
d := []byte{
0x53, 0x94, 0xf7, 0x97, 0x3e, 0xa8, 0x68, 0xc5, 0x2b, 0xf3, 0xff, 0x8d, 0x8c, 0xee, 0xb4, 0xdb,
0x90, 0xa6, 0x83, 0x65, 0x3b, 0x12, 0x48, 0x5d, 0x5f, 0x62, 0x7c, 0x3c, 0xe5, 0xab, 0xd8, 0x97,
0x8f, 0xc9, 0x67, 0x3d, 0x14, 0xa7, 0x1d, 0x92, 0x57, 0x47, 0x93, 0x16, 0x62, 0x49, 0x3c, 0x37,
}
testKnownSigningValue(t, elliptic.P384(), d)
}
func testKeyDerivation(t *testing.T, curve elliptic.Curve, d, expectedX, expectedY []byte) {
privKey := ECDSAKey(curve, d)
if e, a := d, privKey.D.Bytes(); !bytes.Equal(e, a) {
t.Errorf("expected % x, got % x", e, a)
}
if e, a := expectedX, privKey.X.Bytes(); !bytes.Equal(e, a) {
t.Errorf("expected % x, got % x", e, a)
}
if e, a := expectedY, privKey.Y.Bytes(); !bytes.Equal(e, a) {
t.Errorf("expected % x, got % x", e, a)
}
}
func testKnownSigningValue(t *testing.T, curve elliptic.Curve, d []byte) {
signingKey := ECDSAKey(curve, d)
message := []byte{
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
}
sha256Hash := sha256.New()
_, err := io.Copy(sha256Hash, bytes.NewReader(message))
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
msgHash := sha256Hash.Sum(nil)
msgSignature, err := signingKey.Sign(rand.Reader, msgHash, crypto.SHA256)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
verified, err := VerifySignature(&signingKey.PublicKey, msgHash, msgSignature)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !verified {
t.Fatalf("failed to verify message msgSignature")
}
}
func TestECDSAInvalidSignature_P256(t *testing.T) {
testInvalidSignature(t, elliptic.P256())
}
func TestECDSAInvalidSignature_P384(t *testing.T) {
testInvalidSignature(t, elliptic.P384())
}
func TestECDSAGenKeySignature_P256(t *testing.T) {
testGenKeySignature(t, elliptic.P256())
}
func TestECDSAGenKeySignature_P384(t *testing.T) {
testGenKeySignature(t, elliptic.P384())
}
func testInvalidSignature(t *testing.T, curve elliptic.Curve) {
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
t.Fatalf("failed to generate key: %v", err)
}
message := []byte{
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
}
sha256Hash := sha256.New()
_, err = io.Copy(sha256Hash, bytes.NewReader(message))
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
msgHash := sha256Hash.Sum(nil)
msgSignature, err := privateKey.Sign(rand.Reader, msgHash, crypto.SHA256)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
byteToFlip := 15
switch msgSignature[byteToFlip] {
case 0:
msgSignature[byteToFlip] = 0x0a
default:
msgSignature[byteToFlip] &^= msgSignature[byteToFlip]
}
verified, err := VerifySignature(&privateKey.PublicKey, msgHash, msgSignature)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if verified {
t.Fatalf("expected message verification to fail")
}
}
func testGenKeySignature(t *testing.T, curve elliptic.Curve) {
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
t.Fatalf("failed to generate key: %v", err)
}
message := []byte{
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
}
sha256Hash := sha256.New()
_, err = io.Copy(sha256Hash, bytes.NewReader(message))
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
msgHash := sha256Hash.Sum(nil)
msgSignature, err := privateKey.Sign(rand.Reader, msgHash, crypto.SHA256)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
verified, err := VerifySignature(&privateKey.PublicKey, msgHash, msgSignature)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !verified {
t.Fatalf("expected message verification to fail")
}
}
func TestECDSASignatureFormat(t *testing.T) {
asn1Signature := []byte{
0x30, 0x45, 0x02, 0x21, 0x00, 0xd7, 0xc5, 0xb9, 0x9e, 0x0b, 0xb1, 0x1a, 0x1f, 0x32, 0xda, 0x66, 0xe0, 0xff,
0x59, 0xb7, 0x8a, 0x5e, 0xb3, 0x94, 0x9c, 0x23, 0xb3, 0xfc, 0x1f, 0x18, 0xcc, 0xf6, 0x61, 0x67, 0x8b, 0xf1,
0xc1, 0x02, 0x20, 0x26, 0x4d, 0x8b, 0x7c, 0xaa, 0x52, 0x4c, 0xc0, 0x2e, 0x5f, 0xf6, 0x7e, 0x24, 0x82, 0xe5,
0xfb, 0xcb, 0xc7, 0x9b, 0x83, 0x0d, 0x19, 0x7e, 0x7a, 0x40, 0x37, 0x87, 0xdd, 0x1c, 0x93, 0x13, 0xc4,
}
x := []byte{
0x1c, 0xcb, 0xe9, 0x1c, 0x07, 0x5f, 0xc7, 0xf4, 0xf0, 0x33, 0xbf, 0xa2, 0x48, 0xdb, 0x8f, 0xcc,
0xd3, 0x56, 0x5d, 0xe9, 0x4b, 0xbf, 0xb1, 0x2f, 0x3c, 0x59, 0xff, 0x46, 0xc2, 0x71, 0xbf, 0x83,
}
y := []byte{
0xce, 0x40, 0x14, 0xc6, 0x88, 0x11, 0xf9, 0xa2, 0x1a, 0x1f, 0xdb, 0x2c, 0x0e, 0x61, 0x13, 0xe0,
0x6d, 0xb7, 0xca, 0x93, 0xb7, 0x40, 0x4e, 0x78, 0xdc, 0x7c, 0xcd, 0x5c, 0xa8, 0x9a, 0x4c, 0xa9,
}
publicKey, err := ECDSAPublicKey(elliptic.P256(), x, y)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
message := []byte{
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
}
hash := sha256.New()
_, err = io.Copy(hash, bytes.NewReader(message))
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
msgHash := hash.Sum(nil)
verifySignature, err := VerifySignature(publicKey, msgHash, asn1Signature)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !verifySignature {
t.Fatalf("failed to verify signature")
}
}
@@ -2,7 +2,7 @@
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package providers
package aws
import (
"crypto/hmac"
@@ -32,15 +32,15 @@ var awsSigv4IgnoredHeaders = map[string]struct{}{
"x-amzn-trace-id": {},
}
type AWSCredentials struct {
type Credentials struct {
AccessKey string
SecretKey string
RegionName string
SessionToken string
}
func AWSCredentialsFromObject(v ast.Object) AWSCredentials {
var creds AWSCredentials
func CredentialsFromObject(v ast.Object) Credentials {
var creds Credentials
awsAccessKey := v.Get(ast.StringTerm("aws_access_key"))
awsSecretKey := v.Get(ast.StringTerm("aws_secret_access_key"))
awsRegion := v.Get(ast.StringTerm("aws_region"))
@@ -74,8 +74,8 @@ func sortKeys(strMap map[string][]string) []string {
return keys
}
// AWSSignV4 modifies a map[string][]string of headers to generate an AWS V4 signature + headers based on the config/credentials provided.
func AWSSignV4(headers map[string][]string, method string, theURL *url.URL, body []byte, service string, awsCreds AWSCredentials, theTime time.Time) (string, map[string]string) {
// SignV4 modifies a map[string][]string of headers to generate an AWS V4 signature + headers based on the config/credentials provided.
func SignV4(headers map[string][]string, method string, theURL *url.URL, body []byte, service string, awsCreds Credentials, theTime time.Time) (string, map[string]string) {
// General ref. https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
// S3 ref. https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html
// APIGateway ref. https://docs.aws.amazon.com/apigateway/api-reference/signing-requests/
+410
View File
@@ -0,0 +1,410 @@
// modified from github.com/aws/aws-sdk-go-v2/internal/v4a@7a32d707af
package aws
import (
"bytes"
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"hash"
"io"
"math/big"
"net/http"
"net/url"
"sort"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
signerCrypto "github.com/open-policy-agent/opa/internal/providers/aws/crypto"
v4Internal "github.com/open-policy-agent/opa/internal/providers/aws/v4"
)
const (
// AmzRegionSetKey represents the region set header used for sigv4a
AmzRegionSetKey = "X-Amz-Region-Set"
amzSecurityTokenKey = v4Internal.AmzSecurityTokenKey
amzDateKey = v4Internal.AmzDateKey
authorizationHeader = "Authorization"
signingAlgorithm = "AWS4-ECDSA-P256-SHA256"
timeFormat = "20060102T150405Z"
shortTimeFormat = "20060102"
)
var (
p256 elliptic.Curve
nMinusTwoP256 *big.Int
one = new(big.Int).SetInt64(1)
cache = credsCache{}
randomSource = rand.Reader
)
func init() {
// Ensure the elliptic curve parameters are initialized on package import rather then on first usage
p256 = elliptic.P256()
nMinusTwoP256 = new(big.Int).SetBytes(p256.Params().N.Bytes())
nMinusTwoP256 = nMinusTwoP256.Sub(nMinusTwoP256, new(big.Int).SetInt64(2))
}
type credsCache struct {
asymmetric atomic.Value
m sync.Mutex
}
// SetRandomSource used for testing to override rand so tests can expect stable output
func SetRandomSource(reader io.Reader) {
randomSource = reader
}
// deriveKeyFromAccessKeyPair derives a NIST P-256 PrivateKey from the given
// IAM AccessKey and SecretKey pair.
//
// Based on FIPS.186-4 Appendix B.4.2
func deriveKeyFromAccessKeyPair(accessKey, secretKey string) (*ecdsa.PrivateKey, error) {
params := p256.Params()
bitLen := params.BitSize // Testing random candidates does not require an additional 64 bits
counter := 0x01
buffer := make([]byte, 1+len(accessKey)) // 1 byte counter + len(accessKey)
kdfContext := bytes.NewBuffer(buffer)
inputKey := append([]byte("AWS4A"), []byte(secretKey)...)
d := new(big.Int)
for {
kdfContext.Reset()
kdfContext.WriteString(accessKey)
kdfContext.WriteByte(byte(counter))
key, err := signerCrypto.HMACKeyDerivation(sha256.New, bitLen, inputKey, []byte(signingAlgorithm), kdfContext.Bytes())
if err != nil {
return nil, err
}
// Check key first before calling SetBytes if key is in fact a valid candidate.
// This ensures the byte slice is the correct length (32-bytes) to compare in constant-time
cmp, err := signerCrypto.ConstantTimeByteCompare(key, nMinusTwoP256.Bytes())
if err != nil {
return nil, err
}
if cmp == -1 {
d.SetBytes(key)
break
}
counter++
if counter > 0xFF {
return nil, fmt.Errorf("exhausted single byte external counter")
}
}
d = d.Add(d, one)
priv := new(ecdsa.PrivateKey)
priv.PublicKey.Curve = p256
priv.D = d
priv.PublicKey.X, priv.PublicKey.Y = p256.ScalarBaseMult(d.Bytes())
return priv, nil
}
// v4aCredentials is Context, ECDSA, and Optional Session Token that can be used
// to sign requests using SigV4a
type v4aCredentials struct {
Context string
PrivateKey *ecdsa.PrivateKey
SessionToken string
}
// retrievePrivateKey returns credentials suitable for SigV4a signing
func retrievePrivateKey(symmetric Credentials) (v4aCredentials, error) {
cache.m.Lock()
defer cache.m.Unlock()
// try to get creds from cache
v := cache.asymmetric.Load()
if v != nil {
c := v.(*v4aCredentials)
// if the cached Context matches the symmetric AccessKey ID, then use cached value. Otherwise, creds have
// changed and we need to derive new asymmetric creds
if c != nil && c.Context == symmetric.AccessKey {
return *c, nil
}
}
privateKey, err := deriveKeyFromAccessKeyPair(symmetric.AccessKey, symmetric.SecretKey)
if err != nil {
return v4aCredentials{}, fmt.Errorf("failed to derive asymmetric key from credentials")
}
creds := v4aCredentials{
Context: symmetric.AccessKey,
PrivateKey: privateKey,
SessionToken: symmetric.SessionToken,
}
// cache derived asymmetric creds so we don't derive new ones until symmetric creds change
cache.asymmetric.Store(&creds)
return creds, nil
}
type httpSigner struct {
Request *http.Request
ServiceName string
RegionSet []string
Time time.Time
Credentials v4aCredentials
// PayloadHash is the hex encoded SHA-256 hash of the request payload
// If len(PayloadHash) == 0 the signer will attempt to send the request
// as an unsigned payload. Note: Unsigned payloads only work for a subset of services.
PayloadHash string
}
func (s *httpSigner) setRequiredSigningFields(headers http.Header, query url.Values) {
amzDate := s.Time.Format(timeFormat)
headers.Set(AmzRegionSetKey, strings.Join(s.RegionSet, ","))
headers.Set(amzDateKey, amzDate)
if len(s.Credentials.SessionToken) > 0 {
headers.Set(amzSecurityTokenKey, s.Credentials.SessionToken)
}
}
// Build modifies the Request attribute of the httpSigner, adding an Authorization header
func (s *httpSigner) Build() (signedRequest, error) {
req := s.Request
query := req.URL.Query()
headers := req.Header
// seemingly required by S3/MRAP -- 403 Forbidden otherwise
headers.Set("host", req.URL.Host)
headers.Set("x-amz-content-sha256", s.PayloadHash)
s.setRequiredSigningFields(headers, query)
// Sort Each Query Key's Values
for key := range query {
sort.Strings(query[key])
}
v4Internal.SanitizeHostForHeader(req)
credentialScope := s.buildCredentialScope()
credentialStr := s.Credentials.Context + "/" + credentialScope
unsignedHeaders := headers
host := req.URL.Host
if len(req.Host) > 0 {
host = req.Host
}
signedHeaders, signedHeadersStr, canonicalHeaderStr := s.buildCanonicalHeaders(host, v4Internal.IgnoredHeaders, unsignedHeaders, s.Request.ContentLength)
rawQuery := strings.Replace(query.Encode(), "+", "%20", -1)
canonicalURI := v4Internal.GetURIPath(req.URL)
canonicalString := s.buildCanonicalString(
req.Method,
canonicalURI,
rawQuery,
signedHeadersStr,
canonicalHeaderStr,
)
strToSign := s.buildStringToSign(credentialScope, canonicalString)
signingSignature, err := s.buildSignature(strToSign)
if err != nil {
return signedRequest{}, err
}
headers[authorizationHeader] = append(headers[authorizationHeader][:0], buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature))
req.URL.RawQuery = rawQuery
return signedRequest{
Request: req,
SignedHeaders: signedHeaders,
CanonicalString: canonicalString,
StringToSign: strToSign,
}, nil
}
func (s *httpSigner) buildCredentialScope() string {
return strings.Join([]string{
s.Time.Format(shortTimeFormat),
s.ServiceName,
"aws4_request",
}, "/")
}
func buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature string) string {
const credential = "Credential="
const signedHeaders = "SignedHeaders="
const signature = "Signature="
const commaSpace = ", "
var parts strings.Builder
parts.Grow(len(signingAlgorithm) + 1 +
len(credential) + len(credentialStr) + len(commaSpace) +
len(signedHeaders) + len(signedHeadersStr) + len(commaSpace) +
len(signature) + len(signingSignature),
)
parts.WriteString(signingAlgorithm)
parts.WriteRune(' ')
parts.WriteString(credential)
parts.WriteString(credentialStr)
parts.WriteString(commaSpace)
parts.WriteString(signedHeaders)
parts.WriteString(signedHeadersStr)
parts.WriteString(commaSpace)
parts.WriteString(signature)
parts.WriteString(signingSignature)
return parts.String()
}
func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, header http.Header, length int64) (signed http.Header, signedHeaders, canonicalHeadersStr string) {
signed = make(http.Header)
const hostHeader = "host"
headers := make([]string, 0)
if length > 0 {
const contentLengthHeader = "content-length"
headers = append(headers, contentLengthHeader)
signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10))
}
for k, v := range header {
if !rule.IsValid(k) {
continue // ignored header
}
lowerCaseKey := strings.ToLower(k)
if _, ok := signed[lowerCaseKey]; ok {
// include additional values
signed[lowerCaseKey] = append(signed[lowerCaseKey], v...)
continue
}
headers = append(headers, lowerCaseKey)
signed[lowerCaseKey] = v
}
sort.Strings(headers)
signedHeaders = strings.Join(headers, ";")
var canonicalHeaders strings.Builder
n := len(headers)
const colon = ':'
for i := 0; i < n; i++ {
if headers[i] == hostHeader {
canonicalHeaders.WriteString(hostHeader)
canonicalHeaders.WriteRune(colon)
canonicalHeaders.WriteString(v4Internal.StripExcessSpaces(host))
} else {
canonicalHeaders.WriteString(headers[i])
canonicalHeaders.WriteRune(colon)
// Trim out leading, trailing, and dedup inner spaces from signed header values.
values := signed[headers[i]]
for j, v := range values {
cleanedValue := strings.TrimSpace(v4Internal.StripExcessSpaces(v))
canonicalHeaders.WriteString(cleanedValue)
if j < len(values)-1 {
canonicalHeaders.WriteRune(',')
}
}
}
canonicalHeaders.WriteRune('\n')
}
canonicalHeadersStr = canonicalHeaders.String()
return signed, signedHeaders, canonicalHeadersStr
}
func (s *httpSigner) buildCanonicalString(method, uri, query, signedHeaders, canonicalHeaders string) string {
return strings.Join([]string{
method,
uri,
query,
canonicalHeaders,
signedHeaders,
s.PayloadHash,
}, "\n")
}
func (s *httpSigner) buildStringToSign(credentialScope, canonicalRequestString string) string {
return strings.Join([]string{
signingAlgorithm,
s.Time.Format(timeFormat),
credentialScope,
hex.EncodeToString(makeHash(sha256.New(), []byte(canonicalRequestString))),
}, "\n")
}
func makeHash(hash hash.Hash, b []byte) []byte {
hash.Reset()
hash.Write(b)
return hash.Sum(nil)
}
func (s *httpSigner) buildSignature(strToSign string) (string, error) {
sig, err := s.Credentials.PrivateKey.Sign(randomSource, makeHash(sha256.New(), []byte(strToSign)), crypto.SHA256)
if err != nil {
return "", err
}
return hex.EncodeToString(sig), nil
}
type signedRequest struct {
Request *http.Request
SignedHeaders http.Header
CanonicalString string
StringToSign string
}
// SignV4a returns a map[string][]string of headers, including an added AWS V4a signature based on the config/credentials provided.
func SignV4a(headers map[string][]string, method string, theURL *url.URL, body []byte, service string, awsCreds Credentials, theTime time.Time) map[string][]string {
bodyHexHash := fmt.Sprintf("%x", sha256.Sum256(body))
key, err := retrievePrivateKey(awsCreds)
if err != nil {
return map[string][]string{}
}
bodyReader := bytes.NewReader(body)
req, _ := http.NewRequest(method, theURL.String(), bodyReader)
req.Header = headers
signer := &httpSigner{
Request: req,
PayloadHash: bodyHexHash,
ServiceName: service,
RegionSet: []string{"*"},
Credentials: key,
Time: theTime,
}
_, err = signer.Build()
if err != nil {
return map[string][]string{}
}
return req.Header
}
+36
View File
@@ -0,0 +1,36 @@
package v4
const (
// EmptyStringSHA256 is the hex encoded sha256 value of an empty string
EmptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
// UnsignedPayload indicates that the request payload body is unsigned
UnsignedPayload = "UNSIGNED-PAYLOAD"
// AmzAlgorithmKey indicates the signing algorithm
AmzAlgorithmKey = "X-Amz-Algorithm"
// AmzSecurityTokenKey indicates the security token to be used with temporary credentials
AmzSecurityTokenKey = "X-Amz-Security-Token"
// AmzDateKey is the UTC timestamp for the request in the format YYYYMMDD'T'HHMMSS'Z'
AmzDateKey = "X-Amz-Date"
// AmzCredentialKey is the access key ID and credential scope
AmzCredentialKey = "X-Amz-Credential"
// AmzSignedHeadersKey is the set of headers signed for the request
AmzSignedHeadersKey = "X-Amz-SignedHeaders"
// AmzSignatureKey is the query parameter to store the SigV4 signature
AmzSignatureKey = "X-Amz-Signature"
// TimeFormat is the time format to be used in the X-Amz-Date header or query parameter
TimeFormat = "20060102T150405Z"
// ShortTimeFormat is the shorten time format used in the credential scope
ShortTimeFormat = "20060102"
// ContentSHAKey is the SHA256 of request body
ContentSHAKey = "X-Amz-Content-Sha256"
)
+87
View File
@@ -0,0 +1,87 @@
package v4
import (
"strings"
)
// Rules houses a set of Rule needed for validation of a
// string value
type Rules []Rule
// Rule interface allows for more flexible rules and just simply
// checks whether or not a value adheres to that Rule
type Rule interface {
IsValid(value string) bool
}
// IsValid will iterate through all rules and see if any rules
// apply to the value and supports nested rules
func (r Rules) IsValid(value string) bool {
for _, rule := range r {
if rule.IsValid(value) {
return true
}
}
return false
}
// MapRule generic Rule for maps
type MapRule map[string]struct{}
// IsValid for the map Rule satisfies whether it exists in the map
func (m MapRule) IsValid(value string) bool {
_, ok := m[value]
return ok
}
// AllowList is a generic Rule for whitelisting
type AllowList struct {
Rule
}
// IsValid for AllowList checks if the value is within the AllowList
func (w AllowList) IsValid(value string) bool {
return w.Rule.IsValid(value)
}
// DenyList is a generic Rule for blacklisting
type DenyList struct {
Rule
}
// IsValid for AllowList checks if the value is within the AllowList
func (b DenyList) IsValid(value string) bool {
return !b.Rule.IsValid(value)
}
// Patterns is a list of strings to match against
type Patterns []string
// FORK: copied from aws-sdk-go-v2/internal/strings
func hasPrefixFold(s, prefix string) bool {
return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix)
}
// IsValid for Patterns checks each pattern and returns if a match has
// been found
func (p Patterns) IsValid(value string) bool {
for _, pattern := range p {
if hasPrefixFold(value, pattern) {
return true
}
}
return false
}
// InclusiveRules rules allow for rules to depend on one another
type InclusiveRules []Rule
// IsValid will return true if all rules are true
func (r InclusiveRules) IsValid(value string) bool {
for _, rule := range r {
if !rule.IsValid(value) {
return false
}
}
return true
}
+67
View File
@@ -0,0 +1,67 @@
package v4
// IgnoredHeaders is a list of headers that are ignored during signing
var IgnoredHeaders = Rules{
DenyList{
MapRule{
"Authorization": struct{}{},
"User-Agent": struct{}{},
"X-Amzn-Trace-Id": struct{}{},
},
},
}
// RequiredSignedHeaders is a whitelist for Build canonical headers.
var RequiredSignedHeaders = Rules{
AllowList{
MapRule{
"Cache-Control": struct{}{},
"Content-Disposition": struct{}{},
"Content-Encoding": struct{}{},
"Content-Language": struct{}{},
"Content-Md5": struct{}{},
"Content-Type": struct{}{},
"Expires": struct{}{},
"If-Match": struct{}{},
"If-Modified-Since": struct{}{},
"If-None-Match": struct{}{},
"If-Unmodified-Since": struct{}{},
"Range": struct{}{},
"X-Amz-Acl": struct{}{},
"X-Amz-Copy-Source": struct{}{},
"X-Amz-Copy-Source-If-Match": struct{}{},
"X-Amz-Copy-Source-If-Modified-Since": struct{}{},
"X-Amz-Copy-Source-If-None-Match": struct{}{},
"X-Amz-Copy-Source-If-Unmodified-Since": struct{}{},
"X-Amz-Copy-Source-Range": struct{}{},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{},
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
"X-Amz-Grant-Full-control": struct{}{},
"X-Amz-Grant-Read": struct{}{},
"X-Amz-Grant-Read-Acp": struct{}{},
"X-Amz-Grant-Write": struct{}{},
"X-Amz-Grant-Write-Acp": struct{}{},
"X-Amz-Metadata-Directive": struct{}{},
"X-Amz-Mfa": struct{}{},
"X-Amz-Request-Payer": struct{}{},
"X-Amz-Server-Side-Encryption": struct{}{},
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{},
"X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{},
"X-Amz-Server-Side-Encryption-Customer-Key": struct{}{},
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
"X-Amz-Storage-Class": struct{}{},
"X-Amz-Website-Redirect-Location": struct{}{},
"X-Amz-Content-Sha256": struct{}{},
"X-Amz-Tagging": struct{}{},
},
},
Patterns{"X-Amz-Meta-"},
}
// AllowedQueryHoisting is a whitelist for Build query headers. The boolean value
// represents whether or not it is a pattern.
var AllowedQueryHoisting = InclusiveRules{
DenyList{RequiredSignedHeaders},
Patterns{"X-Amz-"},
}
+75
View File
@@ -0,0 +1,75 @@
package v4
import (
"net/http"
"strings"
)
// SanitizeHostForHeader removes default port from host and updates request.Host
func SanitizeHostForHeader(r *http.Request) {
host := getHost(r)
port := portOnly(host)
if port != "" && isDefaultPort(r.URL.Scheme, port) {
r.Host = stripPort(host)
}
}
// Returns host from request
func getHost(r *http.Request) string {
if r.Host != "" {
return r.Host
}
return r.URL.Host
}
// Hostname returns u.Host, without any port number.
//
// If Host is an IPv6 literal with a port number, Hostname returns the
// IPv6 literal without the square brackets. IPv6 literals may include
// a zone identifier.
//
// Copied from the Go 1.8 standard library (net/url)
func stripPort(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return hostport
}
if i := strings.IndexByte(hostport, ']'); i != -1 {
return strings.TrimPrefix(hostport[:i], "[")
}
return hostport[:colon]
}
// Port returns the port part of u.Host, without the leading colon.
// If u.Host doesn't contain a port, Port returns an empty string.
//
// Copied from the Go 1.8 standard library (net/url)
func portOnly(hostport string) string {
colon := strings.IndexByte(hostport, ':')
if colon == -1 {
return ""
}
if i := strings.Index(hostport, "]:"); i != -1 {
return hostport[i+len("]:"):]
}
if strings.Contains(hostport, "]") {
return ""
}
return hostport[colon+len(":"):]
}
// Returns true if the specified URI is using the standard port
// (i.e. port 80 for HTTP URIs or 443 for HTTPS URIs)
func isDefaultPort(scheme, port string) bool {
if port == "" {
return true
}
lowerCaseScheme := strings.ToLower(scheme)
if (lowerCaseScheme == "http" && port == "80") || (lowerCaseScheme == "https" && port == "443") {
return true
}
return false
}
+64
View File
@@ -0,0 +1,64 @@
package v4
import (
"net/url"
"strings"
)
const doubleSpace = " "
// StripExcessSpaces will rewrite the passed in slice's string values to not
// contain multiple side-by-side spaces.
func StripExcessSpaces(str string) string {
var j, k, l, m, spaces int
// Trim trailing spaces
for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- {
}
// Trim leading spaces
for k = 0; k < j && str[k] == ' '; k++ {
}
str = str[k : j+1]
// Strip multiple spaces.
j = strings.Index(str, doubleSpace)
if j < 0 {
return str
}
buf := []byte(str)
for k, m, l = j, j, len(buf); k < l; k++ {
if buf[k] == ' ' {
if spaces == 0 {
// First space.
buf[m] = buf[k]
m++
}
spaces++
} else {
// End of multiple spaces.
spaces = 0
buf[m] = buf[k]
m++
}
}
return string(buf[:m])
}
// GetURIPath returns the escaped URI component from the provided URL
func GetURIPath(u *url.URL) string {
var uri string
if len(u.Opaque) > 0 {
uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/")
} else {
uri = u.EscapedPath()
}
if len(uri) == 0 {
uri = "/"
}
return uri
}
+75
View File
@@ -0,0 +1,75 @@
package v4
import (
"testing"
)
func TestStripExcessHeaders(t *testing.T) {
vals := []string{
"",
"123",
"1 2 3",
"1 2 3 ",
" 1 2 3",
"1 2 3",
"1 23",
"1 2 3",
"1 2 ",
" 1 2 ",
"12 3",
"12 3 1",
"12 3 1",
"12 3 1abc123",
}
expected := []string{
"",
"123",
"1 2 3",
"1 2 3",
"1 2 3",
"1 2 3",
"1 23",
"1 2 3",
"1 2",
"1 2",
"12 3",
"12 3 1",
"12 3 1",
"12 3 1abc123",
}
for i := 0; i < len(vals); i++ {
r := StripExcessSpaces(vals[i])
if e, a := expected[i], r; e != a {
t.Errorf("%d, expect %v, got %v", i, e, a)
}
}
}
var stripExcessSpaceCases = []string{
`AWS4-HMAC-SHA256 Credential=AKIDFAKEIDFAKEID/20160628/us-west-2/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=1234567890abcdef1234567890abcdef1234567890abcdef`,
`123 321 123 321`,
` 123 321 123 321 `,
` 123 321 123 321 `,
"123",
"1 2 3",
" 1 2 3",
"1 2 3",
"1 23",
"1 2 3",
"1 2 ",
" 1 2 ",
"12 3",
"12 3 1",
"12 3 1",
"12 3 1abc123",
}
func BenchmarkStripExcessSpaces(b *testing.B) {
for i := 0; i < b.N; i++ {
for _, v := range stripExcessSpaceCases {
StripExcessSpaces(v)
}
}
}
+21 -18
View File
@@ -19,8 +19,7 @@ import (
"time"
"github.com/go-ini/ini"
"github.com/open-policy-agent/opa/internal/providers"
"github.com/open-policy-agent/opa/internal/providers/aws"
"github.com/open-policy-agent/opa/logging"
)
@@ -58,7 +57,7 @@ const (
// awsCredentialService represents the interface for AWS credential providers
type awsCredentialService interface {
credentials() (providers.AWSCredentials, error)
credentials() (aws.Credentials, error)
}
// awsEnvironmentCredentialService represents an static environment-variable credential provider for AWS
@@ -66,8 +65,8 @@ type awsEnvironmentCredentialService struct {
logger logging.Logger
}
func (cs *awsEnvironmentCredentialService) credentials() (providers.AWSCredentials, error) {
var creds providers.AWSCredentials
func (cs *awsEnvironmentCredentialService) credentials() (aws.Credentials, error) {
var creds aws.Credentials
creds.AccessKey = os.Getenv(accessKeyEnvVar)
if creds.AccessKey == "" {
return creds, errors.New("no " + accessKeyEnvVar + " set in environment")
@@ -114,8 +113,8 @@ type awsProfileCredentialService struct {
logger logging.Logger
}
func (cs *awsProfileCredentialService) credentials() (providers.AWSCredentials, error) {
var creds providers.AWSCredentials
func (cs *awsProfileCredentialService) credentials() (aws.Credentials, error) {
var creds aws.Credentials
filename, err := cs.path()
if err != nil {
@@ -142,7 +141,7 @@ func (cs *awsProfileCredentialService) credentials() (providers.AWSCredentials,
return creds, fmt.Errorf("profile \"%v\" in credentials file %v does not contain \"%v\"", cs.Profile, cs.Path, secretKeyGlobalSetting)
}
creds.SessionToken = profile.Key(securityTokenGlobalSetting).String() //default to empty string
creds.SessionToken = profile.Key(securityTokenGlobalSetting).String() // default to empty string
if cs.RegionName == "" {
if cs.RegionName = os.Getenv(awsRegionEnvVar); cs.RegionName == "" {
@@ -191,7 +190,7 @@ func (cs *awsProfileCredentialService) profile() string {
type awsMetadataCredentialService struct {
RoleName string `json:"iam_role,omitempty"`
RegionName string `json:"aws_region"`
creds providers.AWSCredentials
creds aws.Credentials
expiration time.Time
credServicePath string
tokenPath string
@@ -308,7 +307,7 @@ func (cs *awsMetadataCredentialService) refreshFromService() error {
return nil
}
func (cs *awsMetadataCredentialService) credentials() (providers.AWSCredentials, error) {
func (cs *awsMetadataCredentialService) credentials() (aws.Credentials, error) {
err := cs.refreshFromService()
if err != nil {
return cs.creds, err
@@ -323,7 +322,7 @@ type awsWebIdentityCredentialService struct {
RegionName string `json:"aws_region"`
SessionName string `json:"session_name"`
stsURL string
creds providers.AWSCredentials
creds aws.Credentials
expiration time.Time
logger logging.Logger
}
@@ -433,7 +432,7 @@ func (cs *awsWebIdentityCredentialService) refreshFromService() error {
return nil
}
func (cs *awsWebIdentityCredentialService) credentials() (providers.AWSCredentials, error) {
func (cs *awsWebIdentityCredentialService) credentials() (aws.Credentials, error) {
err := cs.refreshFromService()
if err != nil {
return cs.creds, err
@@ -482,7 +481,7 @@ func doMetaDataRequestWithClient(req *http.Request, client *http.Client, desc st
}
// signV4 modifies an http.Request to include an AWS V4 signature based on a credential provider
func signV4(req *http.Request, service string, credService awsCredentialService, theTime time.Time) error {
func signV4(req *http.Request, service string, credService awsCredentialService, theTime time.Time, sigVersion string) error {
// General ref. https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
// S3 ref. https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html
// APIGateway ref. https://docs.aws.amazon.com/apigateway/api-reference/signing-requests/
@@ -507,11 +506,15 @@ func signV4(req *http.Request, service string, credService awsCredentialService,
now := theTime.UTC()
authHeader, awsHeaders := providers.AWSSignV4(req.Header, req.Method, req.URL, body, service, creds, now)
req.Header.Set("Authorization", authHeader)
for k, v := range awsHeaders {
req.Header.Add(k, v)
if sigVersion == "4a" {
signedHeaders := aws.SignV4a(req.Header, req.Method, req.URL, body, service, creds, now)
req.Header = signedHeaders
} else {
authHeader, awsHeaders := aws.SignV4(req.Header, req.Method, req.URL, body, service, creds, now)
req.Header.Set("Authorization", authHeader)
for k, v := range awsHeaders {
req.Header.Add(k, v)
}
}
return nil
+183 -60
View File
@@ -5,6 +5,7 @@
package rest
import (
"crypto/rand"
"encoding/json"
"fmt"
"io"
@@ -16,7 +17,7 @@ import (
"testing"
"time"
"github.com/open-policy-agent/opa/internal/providers"
"github.com/open-policy-agent/opa/internal/providers/aws"
"github.com/open-policy-agent/opa/logging"
"github.com/open-policy-agent/opa/util/test"
)
@@ -30,13 +31,22 @@ type metadataPayload struct {
Expiration time.Time
}
// quicky and dirty assertions
// quick and dirty assertions
func assertEq(expected string, actual string, t *testing.T) {
t.Helper()
if actual != expected {
t.Error("expected: ", expected, " but got: ", actual)
}
}
func assertIn(candidates []string, actual string, t *testing.T) {
t.Helper()
for _, expected := range candidates {
if actual == expected {
return
}
}
t.Error("value: '", actual, "' not found in: ", candidates)
}
func assertErr(expected string, actual error, t *testing.T) {
t.Helper()
@@ -62,7 +72,7 @@ func TestEnvironmentCredentialService(t *testing.T) {
t.Setenv("AWS_REGION", "us-east-1")
expectedCreds := providers.AWSCredentials{
expectedCreds := aws.Credentials{
AccessKey: "MYAWSACCESSKEYGOESHERE",
SecretKey: "MYAWSSECRETACCESSKEYGOESHERE",
RegionName: "us-east-1",
@@ -135,7 +145,7 @@ aws_secret_access_key=%v
t.Fatal(err)
}
expected := providers.AWSCredentials{
expected := aws.Credentials{
AccessKey: fooKey,
SecretKey: fooSecret,
RegionName: fooRegion,
@@ -158,7 +168,7 @@ aws_secret_access_key=%v
t.Fatal(err)
}
expected = providers.AWSCredentials{
expected = aws.Credentials{
AccessKey: defaultKey,
SecretKey: defaultSecret,
RegionName: defaultRegion,
@@ -201,7 +211,7 @@ aws_session_token=%s
t.Fatal(err)
}
expected := providers.AWSCredentials{
expected := aws.Credentials{
AccessKey: defaultKey,
SecretKey: defaultSecret,
RegionName: defaultRegion,
@@ -250,7 +260,7 @@ aws_session_token=%s
t.Fatal(err)
}
expected := providers.AWSCredentials{
expected := aws.Credentials{
AccessKey: defaultKey,
SecretKey: defaultSecret,
RegionName: defaultRegion,
@@ -414,7 +424,7 @@ func TestMetadataCredentialService(t *testing.T) {
tokenPath: ts.server.URL + "/latest/api/token",
logger: logging.Get(),
}
var creds providers.AWSCredentials
var creds aws.Credentials
creds, err = cs.credentials()
if err != nil {
// Cannot proceed with test if unable to fetch credentials.
@@ -480,7 +490,7 @@ func TestMetadataCredentialService(t *testing.T) {
assertEq(creds.SessionToken, ts.payload.Token, t)
}
func TestV4Signing(t *testing.T) {
func TestMetadataServiceErrorHandled(t *testing.T) {
ts := ec2CredTestServer{}
ts.start()
defer ts.stop()
@@ -494,12 +504,18 @@ func TestV4Signing(t *testing.T) {
logger: logging.Get(),
}
req, _ := http.NewRequest("GET", "https://mybucket.s3.amazonaws.com/bundle.tar.gz", strings.NewReader(""))
err := signV4(req, "s3", cs, time.Unix(1556129697, 0))
err := signV4(req, "s3", cs, time.Unix(1556129697, 0), "4")
assertErr("error getting AWS credentials: metadata HTTP request returned unexpected status: 404 Not Found", err, t)
}
func TestV4Signing(t *testing.T) {
ts := ec2CredTestServer{}
ts.start()
defer ts.stop()
// happy path: sign correctly
cs = &awsMetadataCredentialService{
cs := &awsMetadataCredentialService{
RoleName: "my_iam_role", // not present
RegionName: "us-east-1",
credServicePath: ts.server.URL + "/latest/meta-data/iam/security-credentials/",
@@ -512,23 +528,55 @@ func TestV4Signing(t *testing.T) {
Code: "Success",
Token: "MYAWSSECURITYTOKENGOESHERE",
Expiration: time.Now().UTC().Add(time.Minute * 2)}
req, _ = http.NewRequest("GET", "https://mybucket.s3.amazonaws.com/bundle.tar.gz", strings.NewReader(""))
err = signV4(req, "s3", cs, time.Unix(1556129697, 0))
req, _ := http.NewRequest("GET", "https://mybucket.s3.amazonaws.com/bundle.tar.gz", strings.NewReader(""))
if err != nil {
t.Fatal("unexpected error during signing")
// force a non-random source so that we can predict the v4a signing key and, thus, signature
myReader := strings.NewReader("000000000000000000000000000000000")
aws.SetRandomSource(myReader)
defer func() { aws.SetRandomSource(rand.Reader) }()
tests := []struct {
sigVersion string
expectedAuthorization []string
}{
{
sigVersion: "4",
expectedAuthorization: []string{
"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/s3/aws4_request," +
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token," +
"Signature=d3f0561abae5e35d9ee2c15e678bb7acacc4b4743707a8f7fbcbfdb519078990",
},
},
{
sigVersion: "4a",
expectedAuthorization: []string{
// this signature is for go 1.18+, which changed crypto/ecdsa so signatures differ from go 1.17
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/s3/aws4_request, " +
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
"Signature=304402207d1bcb6fb68d85be3e9f6948a8dc8596a531b3f5a82ca2350acabe98941312bc02207d81ed07c7356226d93611820548a806c8e1f0cc72ff41ba672d23901e5a06bf",
// this signature is for go 1.17. Remove this and only test for a single value when OPA drops go 1.17
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/s3/aws4_request, " +
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
"Signature=3045022100f951364b6495e3fe6be830a3550043bfd98e5312f79091e7c87bd51455cfb93c02203a4ca3e29ad63b6a9b473172e6ebb870f3d1947f2c44334bfd7eb74dbda4ec97",
},
},
}
// expect mandatory headers
assertEq(req.Header.Get("Host"), "mybucket.s3.amazonaws.com", t)
assertEq(req.Header.Get("Authorization"),
"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/s3/aws4_request,"+
"SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token,"+
"Signature=d3f0561abae5e35d9ee2c15e678bb7acacc4b4743707a8f7fbcbfdb519078990", t)
assertEq(req.Header.Get("X-Amz-Content-Sha256"),
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", t)
assertEq(req.Header.Get("X-Amz-Date"), "20190424T181457Z", t)
assertEq(req.Header.Get("X-Amz-Security-Token"), "MYAWSSECURITYTOKENGOESHERE", t)
for _, test := range tests {
err := signV4(req, "s3", cs, time.Unix(1556129697, 0), test.sigVersion)
if err != nil {
t.Fatal("unexpected error during signing", err)
}
// expect mandatory headers
assertEq("mybucket.s3.amazonaws.com", req.Header.Get("Host"), t)
assertIn(test.expectedAuthorization, req.Header.Get("Authorization"), t)
assertEq("e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
req.Header.Get("X-Amz-Content-Sha256"), t)
assertEq("20190424T181457Z", req.Header.Get("X-Amz-Date"), t)
assertEq("MYAWSSECURITYTOKENGOESHERE", req.Header.Get("X-Amz-Security-Token"), t)
}
}
func TestV4SigningForApiGateway(t *testing.T) {
@@ -553,7 +601,7 @@ func TestV4SigningForApiGateway(t *testing.T) {
strings.NewReader("{ \"payload\": 42 }"))
req.Header.Set("Content-Type", "application/json")
err := signV4(req, "execute-api", cs, time.Unix(1556129697, 0))
err := signV4(req, "execute-api", cs, time.Unix(1556129697, 0), "4")
if err != nil {
t.Fatal("unexpected error during signing")
@@ -598,20 +646,51 @@ func TestV4SigningOmitsIgnoredHeaders(t *testing.T) {
req.Header.Set("Authorization", "Auth header will be overwritten, and shouldn't be signed")
req.Header.Set("X-Amzn-Trace-Id", "Some trace id")
err := signV4(req, "execute-api", cs, time.Unix(1556129697, 0))
// force a non-random source so that we can predict the v4a signing key and, thus, signature
myReader := strings.NewReader("000000000000000000000000000000000")
aws.SetRandomSource(myReader)
defer func() { aws.SetRandomSource(rand.Reader) }()
if err != nil {
t.Fatal("unexpected error during signing")
tests := []struct {
sigVersion string
expectedAuthorization []string
}{
{
sigVersion: "4",
expectedAuthorization: []string{"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/execute-api/aws4_request," +
"SignedHeaders=content-type;host;x-amz-date;x-amz-security-token," +
"Signature=c8ee72cc45050b255bcbf19defc693f7cd788959b5380fa0985de6e865635339",
},
},
{
sigVersion: "4a",
expectedAuthorization: []string{
// this signature is for go 1.18+, which changed crypto/ecdsa so signatures differ from go 1.17
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/execute-api/aws4_request, " +
"SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
"Signature=30450221009f3b0cda178456dfd1bec61b78bdbd115c0cf497eaa52c58bbb2850ad9c49c3002207009cb88a1219a4a6626056c31823a6b5bc2728bc88bc98a06e12e1148482c94",
// this signature is for go 1.17. Remove this and only test for a single value when OPA drops go 1.17
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/execute-api/aws4_request, " +
"SignedHeaders=content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
"Signature=304602210088b5a5ccf9e37aac765f7e6bf0507577eb1b919b80bc3c385b8856c7ab7a9912022100f4c558e36be338c9644240b722e06333ea9a5305b2e638d56ad0105995c9b1f7",
},
},
}
for _, test := range tests {
err := signV4(req, "execute-api", cs, time.Unix(1556129697, 0), test.sigVersion)
if err != nil {
t.Fatal("unexpected error during signing")
}
// Check the signed headers doesn't include user-agent, authorization or x-amz-trace-id
assertIn(test.expectedAuthorization, req.Header.Get("Authorization"), t)
// The headers omitted from signing should still be present in the request
assertEq(req.Header.Get("User-Agent"), "Unit Tests!", t)
assertEq(req.Header.Get("X-Amzn-Trace-Id"), "Some trace id", t)
}
// Check the signed headers doesn't include user-agent, authorization or x-amz-trace-id
assertEq(req.Header.Get("Authorization"),
"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/execute-api/aws4_request,"+
"SignedHeaders=content-type;host;x-amz-date;x-amz-security-token,"+
"Signature=c8ee72cc45050b255bcbf19defc693f7cd788959b5380fa0985de6e865635339", t)
// The headers omitted from signing should still be present in the request
assertEq(req.Header.Get("User-Agent"), "Unit Tests!", t)
assertEq(req.Header.Get("X-Amzn-Trace-Id"), "Some trace id", t)
}
func TestV4SigningCustomPort(t *testing.T) {
@@ -633,7 +712,7 @@ func TestV4SigningCustomPort(t *testing.T) {
Token: "MYAWSSECURITYTOKENGOESHERE",
Expiration: time.Now().UTC().Add(time.Minute * 2)}
req, _ := http.NewRequest("GET", "https://custom.s3.server:9000/bundle.tar.gz", strings.NewReader(""))
err := signV4(req, "s3", cs, time.Unix(1556129697, 0))
err := signV4(req, "s3", cs, time.Unix(1556129697, 0), "4")
if err != nil {
t.Fatal("unexpected error during signing")
@@ -669,18 +748,33 @@ func TestV4SigningDoesNotMutateBody(t *testing.T) {
Code: "Success",
Token: "MYAWSSECURITYTOKENGOESHERE",
Expiration: time.Now().UTC().Add(time.Minute * 2)}
req, _ := http.NewRequest("POST", "https://myrestapi.execute-api.us-east-1.amazonaws.com/prod/logs",
strings.NewReader("{ \"payload\": 42 }"))
err := signV4(req, "execute-api", cs, time.Unix(1556129697, 0))
// force a non-random source so that we can predict the v4a signing key and, thus, signature
myReader := strings.NewReader("000000000000000000000000000000000")
aws.SetRandomSource(myReader)
defer func() { aws.SetRandomSource(rand.Reader) }()
if err != nil {
t.Fatal("unexpected error during signing")
tests := []struct {
sigVersion string
}{
{sigVersion: "4"},
{sigVersion: "4a"},
}
// Read the body and check that it was not mutated
body, _ := io.ReadAll(req.Body)
assertEq(string(body), "{ \"payload\": 42 }", t)
for _, test := range tests {
req, _ := http.NewRequest("POST", "https://myrestapi.execute-api.us-east-1.amazonaws.com/prod/logs",
strings.NewReader("{ \"payload\": 42 }"))
err := signV4(req, "execute-api", cs, time.Unix(1556129697, 0), test.sigVersion)
if err != nil {
t.Fatal("unexpected error during signing")
}
// Read the body and check that it was not mutated
body, _ := io.ReadAll(req.Body)
assertEq(string(body), "{ \"payload\": 42 }", t)
}
}
func TestV4SigningWithMultiValueHeaders(t *testing.T) {
@@ -706,24 +800,53 @@ func TestV4SigningWithMultiValueHeaders(t *testing.T) {
req.Header.Add("Accept", "text/plain")
req.Header.Add("Accept", "text/html")
err := signV4(req, "execute-api", cs, time.Unix(1556129697, 0))
// force a non-random source so that we can predict the v4a signing key and, thus, signature
myReader := strings.NewReader("000000000000000000000000000000000")
aws.SetRandomSource(myReader)
defer func() { aws.SetRandomSource(rand.Reader) }()
if err != nil {
t.Fatal("unexpected error during signing")
tests := []struct {
sigVersion string
expectedAuthorization []string
}{
{
sigVersion: "4",
expectedAuthorization: []string{
"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/execute-api/aws4_request," +
"SignedHeaders=accept;host;x-amz-date;x-amz-security-token," +
"Signature=0237b0c789cad36212f0efba70c02549e1f659ab9caaca16423930cc7236c046",
},
},
{
sigVersion: "4a",
expectedAuthorization: []string{
// this signature is for go 1.18+, which changed crypto/ecdsa so signatures differ from go 1.17
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/execute-api/aws4_request, " +
"SignedHeaders=accept;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
"Signature=304402202d5f2d4d42fe59b2e61fa455cb35a335139d109c2d37aaa8946d45fd0fb4989c022068238cbfbc80326f5cc391f2b6837910191ceabb58ec0bf986c0141f76046594",
// this signature is for go 1.17. Remove this and only test for a single value when OPA drops go 1.17
"AWS4-ECDSA-P256-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/execute-api/aws4_request, " +
"SignedHeaders=accept;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-region-set;x-amz-security-token, " +
"Signature=304502206ac05ae8f63689e989227fac6c6c16008c25c1d66903f6535610df6496942701022100e1db05ec77d5142462537f7fd4d14db1d1e9b5c8c14643f2434206fe7284dfd6",
},
},
}
// Check the signed headers includes our multi-value 'accept' header
assertEq(req.Header.Get("Authorization"),
"AWS4-HMAC-SHA256 Credential=MYAWSACCESSKEYGOESHERE/20190424/us-east-1/execute-api/aws4_request,"+
"SignedHeaders=accept;host;x-amz-date;x-amz-security-token,"+
"Signature=0237b0c789cad36212f0efba70c02549e1f659ab9caaca16423930cc7236c046", t)
// Ensure 'authorization' is not multi-valued.
if len(req.Header.Values("Authorization")) != 1 {
t.Fatal("Authorization header is multi-valued. This will break AWS v4 signing.")
for _, test := range tests {
err := signV4(req, "execute-api", cs, time.Unix(1556129697, 0), test.sigVersion)
if err != nil {
t.Fatal("unexpected error during signing")
}
if len(req.Header.Values("Authorization")) != 1 {
t.Fatal("Authorization header is multi-valued. This will break AWS v4 signing.")
}
// Check the signed headers includes our multi-value 'accept' header
assertIn(test.expectedAuthorization, req.Header.Get("Authorization"), t)
// The multi-value headers are preserved
assertEq("text/plain", req.Header.Values("Accept")[0], t)
assertEq("text/html", req.Header.Values("Accept")[1], t)
}
// The multi-value headers are preserved
assertEq(req.Header.Values("Accept")[0], "text/plain", t)
assertEq(req.Header.Values("Accept")[1], "text/html", t)
}
// simulate EC2 metadata service
+9 -4
View File
@@ -26,7 +26,7 @@ import (
"github.com/open-policy-agent/opa/internal/jwx/jwa"
"github.com/open-policy-agent/opa/internal/jwx/jws"
"github.com/open-policy-agent/opa/internal/jwx/jws/sign"
"github.com/open-policy-agent/opa/internal/providers"
"github.com/open-policy-agent/opa/internal/providers/aws"
"github.com/open-policy-agent/opa/internal/uuid"
"github.com/open-policy-agent/opa/keys"
"github.com/open-policy-agent/opa/logging"
@@ -518,6 +518,7 @@ type awsSigningAuthPlugin struct {
AWSWebIdentityCredentials *awsWebIdentityCredentialService `json:"web_identity_credentials,omitempty"`
AWSProfileCredentials *awsProfileCredentialService `json:"profile_credentials,omitempty"`
AWSService string `json:"service,omitempty"`
AWSSignatureVersion string `json:"signature_version,omitempty"`
logger logging.Logger
}
@@ -531,7 +532,7 @@ func (acs *awsCredentialServiceChain) addService(service awsCredentialService) {
acs.awsCredentialServices = append(acs.awsCredentialServices, service)
}
func (acs *awsCredentialServiceChain) credentials() (providers.AWSCredentials, error) {
func (acs *awsCredentialServiceChain) credentials() (aws.Credentials, error) {
for _, service := range acs.awsCredentialServices {
credential, err := service.credentials()
if err == nil {
@@ -544,7 +545,7 @@ func (acs *awsCredentialServiceChain) credentials() (providers.AWSCredentials, e
reflect.TypeOf(service).String(), err)
}
return providers.AWSCredentials{}, errors.New("all AWS credential providers failed")
return aws.Credentials{}, errors.New("all AWS credential providers failed")
}
func (ap *awsSigningAuthPlugin) awsCredentialService() awsCredentialService {
@@ -602,7 +603,7 @@ func (ap *awsSigningAuthPlugin) NewClient(c Config) (*http.Client, error) {
func (ap *awsSigningAuthPlugin) Prepare(req *http.Request) error {
ap.logger.Debug("Signing request with AWS credentials.")
return signV4(req, ap.AWSService, ap.awsCredentialService(), time.Now())
return signV4(req, ap.AWSService, ap.awsCredentialService(), time.Now(), ap.AWSSignatureVersion)
}
func (ap *awsSigningAuthPlugin) validateConfig() error {
@@ -632,5 +633,9 @@ func (ap *awsSigningAuthPlugin) validateConfig() error {
ap.AWSService = awsSigv4SigningDefaultService
}
if ap.AWSSignatureVersion == "" {
ap.AWSSignatureVersion = "4"
}
return nil
}
+3 -3
View File
@@ -10,7 +10,7 @@ import (
"time"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/internal/providers"
"github.com/open-policy-agent/opa/internal/providers/aws"
"github.com/open-policy-agent/opa/topdown/builtins"
)
@@ -104,7 +104,7 @@ func builtinAWSSigV4SignReq(ctx BuiltinContext, operands []*ast.Term, iter func(
return err
}
service := stringFromTerm(awsConfigObj.Get(ast.StringTerm("aws_service")))
awsCreds := providers.AWSCredentialsFromObject(awsConfigObj)
awsCreds := aws.CredentialsFromObject(awsConfigObj)
// Timestamp for signing.
var signingTimestamp time.Time
@@ -172,7 +172,7 @@ func builtinAWSSigV4SignReq(ctx BuiltinContext, operands []*ast.Term, iter func(
}
// Sign the request object's headers, and reconstruct the headers map.
authHeader, signedHeadersMap := providers.AWSSignV4(objectToMap(headers), method, theURL, body, service, awsCreds, signingTimestamp)
authHeader, signedHeadersMap := aws.SignV4(objectToMap(headers), method, theURL, body, service, awsCreds, signingTimestamp)
signedHeadersObj := ast.NewObject()
signedHeadersObj.Insert(ast.StringTerm("Authorization"), ast.StringTerm(authHeader))
for k, v := range signedHeadersMap {