mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-13 03:42:35 -06:00
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:
@@ -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
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
AWS SDK for Go
|
||||
Copyright 2015 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
||||
Copyright 2014-2015 Stripe, Inc.
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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/
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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-"},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user