Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TT-11908] add request signing to OAS upstream authentication #6850

Merged
merged 2 commits into from
Feb 7, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 0 additions & 7 deletions apidef/oas/oas_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,13 +210,6 @@ func TestOAS_ExtractTo_ResetAPIDefinition(t *testing.T) {
expectedFields := []string{
"APIDefinition.Slug",
"APIDefinition.EnableProxyProtocol",
"APIDefinition.RequestSigning.IsEnabled",
"APIDefinition.RequestSigning.Secret",
"APIDefinition.RequestSigning.KeyId",
"APIDefinition.RequestSigning.Algorithm",
"APIDefinition.RequestSigning.HeaderList[0]",
"APIDefinition.RequestSigning.CertificateId",
"APIDefinition.RequestSigning.SignatureHeader",
"APIDefinition.VersionData.Versions[0].ExtendedPaths.TransformJQ[0].Filter",
"APIDefinition.VersionData.Versions[0].ExtendedPaths.TransformJQ[0].Path",
"APIDefinition.VersionData.Versions[0].ExtendedPaths.TransformJQ[0].Method",
Expand Down
35 changes: 35 additions & 0 deletions apidef/oas/schema/x-tyk-api-gateway.json
Original file line number Diff line number Diff line change
Expand Up @@ -2110,6 +2110,9 @@
},
"oauth": {
"$ref": "#/definitions/X-Tyk-UpstreamOAuth"
},
"requestSigning": {
"$ref": "#/definitions/X-Tyk-UpstreamRequestSigning"
}
},
"required": [
Expand Down Expand Up @@ -2239,6 +2242,38 @@
"allowedAuthorizeTypes"
]
},
"X-Tyk-UpstreamRequestSigning": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
},
"signatureHeader": {
"type": "string"
},
"algorithm": {
"type": "string"
},
"keyId": {
"type": "string"
},
"headers": {
"type": "array",
"items": {
"type": "string"
}
},
"secret": {
"type": "string"
},
"certificateId": {
"type": "string"
}
},
"required": [
"enabled"
]
},
"X-Tyk-NonEmptyString": {
"type": "string",
"pattern": "\\S+"
Expand Down
90 changes: 78 additions & 12 deletions apidef/oas/upstream.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ func (u *Upstream) Fill(api apidef.APIDefinition) {
u.Authentication = &UpstreamAuth{}
}

u.Authentication.Fill(api.UpstreamAuth)
u.Authentication.Fill(api)
if ShouldOmit(u.Authentication) {
u.Authentication = nil
}
Expand Down Expand Up @@ -171,7 +171,7 @@ func (u *Upstream) ExtractTo(api *apidef.APIDefinition) {
}()
}

u.Authentication.ExtractTo(&api.UpstreamAuth)
u.Authentication.ExtractTo(api)

u.loadBalancingExtractTo(api)

Expand Down Expand Up @@ -618,48 +618,74 @@ type UpstreamAuth struct {
BasicAuth *UpstreamBasicAuth `bson:"basicAuth,omitempty" json:"basicAuth,omitempty"`
// OAuth contains the configuration for OAuth2 Client Credentials flow.
OAuth *UpstreamOAuth `bson:"oauth,omitempty" json:"oauth,omitempty"`
// RequestSigning holds the configuration for generating signed requests to an upstream API.
RequestSigning *UpstreamRequestSigning `bson:"requestSigning,omitempty" json:"requestSigning,omitempty"`
}

// Fill fills *UpstreamAuth from apidef.UpstreamAuth.
func (u *UpstreamAuth) Fill(api apidef.UpstreamAuth) {
u.Enabled = api.Enabled
// Fill fills *UpstreamAuth from apidef.APIDefinition.
func (u *UpstreamAuth) Fill(api apidef.APIDefinition) {
u.Enabled = api.UpstreamAuth.Enabled

if u.BasicAuth == nil {
u.BasicAuth = &UpstreamBasicAuth{}
}
u.BasicAuth.Fill(api.BasicAuth)
u.BasicAuth.Fill(api.UpstreamAuth.BasicAuth)
if ShouldOmit(u.BasicAuth) {
u.BasicAuth = nil
}

if u.OAuth == nil {
u.OAuth = &UpstreamOAuth{}
}
u.OAuth.Fill(api.OAuth)
u.OAuth.Fill(api.UpstreamAuth.OAuth)
if ShouldOmit(u.OAuth) {
u.OAuth = nil
}

u.fillRequestSigning(api)
}

// ExtractTo extracts *UpstreamAuth into *apidef.UpstreamAuth.
func (u *UpstreamAuth) ExtractTo(api *apidef.UpstreamAuth) {
api.Enabled = u.Enabled
// ExtractTo extracts *UpstreamAuth into *apidef.APIDefinition.
func (u *UpstreamAuth) ExtractTo(api *apidef.APIDefinition) {
api.UpstreamAuth.Enabled = u.Enabled

if u.BasicAuth == nil {
u.BasicAuth = &UpstreamBasicAuth{}
defer func() {
u.BasicAuth = nil
}()
}
u.BasicAuth.ExtractTo(&api.BasicAuth)
u.BasicAuth.ExtractTo(&api.UpstreamAuth.BasicAuth)

if u.OAuth == nil {
u.OAuth = &UpstreamOAuth{}
defer func() {
u.OAuth = nil
}()
}
u.OAuth.ExtractTo(&api.OAuth)
u.OAuth.ExtractTo(&api.UpstreamAuth.OAuth)

u.requestSigningExtractTo(api)
}

func (u *UpstreamAuth) fillRequestSigning(api apidef.APIDefinition) {
if u.RequestSigning == nil {
u.RequestSigning = &UpstreamRequestSigning{}
}
u.RequestSigning.Fill(api)
if ShouldOmit(u.RequestSigning) {
u.RequestSigning = nil
}
}

func (u *UpstreamAuth) requestSigningExtractTo(api *apidef.APIDefinition) {
if u.RequestSigning == nil {
u.RequestSigning = &UpstreamRequestSigning{}
defer func() {
u.BasicAuth = nil
}()
}
u.RequestSigning.ExtractTo(api)
}

// UpstreamBasicAuth holds upstream basic authentication configuration.
Expand Down Expand Up @@ -865,6 +891,46 @@ func (u *UpstreamOAuth) ExtractTo(api *apidef.UpstreamOAuth) {
u.PasswordAuthentication.ExtractTo(&api.PasswordAuthentication)
}

// UpstreamRequestSigning represents configuration for generating signed requests to an upstream API.
type UpstreamRequestSigning struct {
// Enabled determines if request signing is enabled or disabled.
Enabled bool `bson:"enabled" json:"enabled"` // required
// SignatureHeader specifies the HTTP header name for the signature.
SignatureHeader string `bson:"signatureHeader,omitempty" json:"signatureHeader,omitempty"`
// Algorithm represents the signing algorithm used (e.g., HMAC-SHA256).
Algorithm string `bson:"algorithm,omitempty" json:"algorithm,omitempty"`
// KeyID identifies the key used for signing purposes.
KeyID string `bson:"keyId,omitempty" json:"keyId,omitempty"`
// Headers contains a list of headers included in the signature calculation.
Headers []string `bson:"headers,omitempty" json:"headers,omitempty"`
// Secret holds the secret used for signing when applicable.
Secret string `bson:"secret,omitempty" json:"secret,omitempty"`
// CertificateID specifies the certificate ID used in signing operations.
CertificateID string `bson:"certificateId,omitempty" json:"certificateId,omitempty"`
}

// Fill populates the UpstreamRequestSigning fields from the given apidef.APIDefinition configuration.
func (l *UpstreamRequestSigning) Fill(api apidef.APIDefinition) {
l.Enabled = api.RequestSigning.IsEnabled
l.SignatureHeader = api.RequestSigning.SignatureHeader
l.Algorithm = api.RequestSigning.Algorithm
l.KeyID = api.RequestSigning.KeyId
l.Headers = api.RequestSigning.HeaderList
l.Secret = api.RequestSigning.Secret
l.CertificateID = api.RequestSigning.CertificateId
}

// ExtractTo populates the given apidef.APIDefinition RequestSigning fields with values from the UpstreamRequestSigning.
func (l *UpstreamRequestSigning) ExtractTo(api *apidef.APIDefinition) {
api.RequestSigning.IsEnabled = l.Enabled
api.RequestSigning.SignatureHeader = l.SignatureHeader
api.RequestSigning.Algorithm = l.Algorithm
api.RequestSigning.KeyId = l.KeyID
api.RequestSigning.HeaderList = l.Headers
api.RequestSigning.Secret = l.Secret
api.RequestSigning.CertificateId = l.CertificateID
}

// LoadBalancing represents the configuration for load balancing between multiple upstream targets.
type LoadBalancing struct {
// Enabled determines if load balancing is active.
Expand Down
136 changes: 136 additions & 0 deletions apidef/oas/upstream_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,142 @@ func TestCertificatePinning(t *testing.T) {
})
}

func TestUpstreamRequestSigning(t *testing.T) {
t.Parallel()
t.Run("fill", func(t *testing.T) {
t.Parallel()
testcases := []struct {
title string
input apidef.APIDefinition
expected *UpstreamAuth
}{
{
title: "request signing disabled and everything else is empty should omit",
input: apidef.APIDefinition{
RequestSigning: apidef.RequestSigningMeta{
IsEnabled: false,
Secret: "",
KeyId: "",
Algorithm: "",
HeaderList: nil,
CertificateId: "",
SignatureHeader: "",
},
},
expected: nil,
},
{
title: "request signing enabled and values are set",
input: apidef.APIDefinition{
RequestSigning: apidef.RequestSigningMeta{
IsEnabled: true,
Secret: "secret",
KeyId: "key-1",
Algorithm: "hmac-sha256",
HeaderList: []string{"header1", "header2"},
CertificateId: "cert-1",
SignatureHeader: "Signature",
},
},
expected: &UpstreamAuth{
RequestSigning: &UpstreamRequestSigning{
Enabled: true,
SignatureHeader: "Signature",
Algorithm: "hmac-sha256",
KeyID: "key-1",
Headers: []string{"header1", "header2"},
Secret: "secret",
CertificateID: "cert-1",
},
},
},
}

for _, tc := range testcases {
tc := tc
t.Run(tc.title, func(t *testing.T) {
t.Parallel()

g := new(Upstream)
g.Fill(tc.input)

assert.Equal(t, tc.expected, g.Authentication)
})
}
})

t.Run("extractTo", func(t *testing.T) {
t.Parallel()

testcases := []struct {
title string
input *UpstreamRequestSigning
expectedRequestSigning apidef.RequestSigningMeta
}{
{
title: "request signing disabled and everything else is empty",
input: &UpstreamRequestSigning{
Enabled: false,
SignatureHeader: "",
Algorithm: "",
KeyID: "",
Headers: nil,
Secret: "",
CertificateID: "",
},
expectedRequestSigning: apidef.RequestSigningMeta{
IsEnabled: false,
Secret: "",
KeyId: "",
Algorithm: "",
HeaderList: nil,
CertificateId: "",
SignatureHeader: "",
},
},
{
title: "request signing enabled and values are set",
input: &UpstreamRequestSigning{
Enabled: true,
SignatureHeader: "Signature",
Algorithm: "hmac-sha256",
KeyID: "key-1",
Headers: []string{"header1", "header2"},
Secret: "secret",
CertificateID: "cert-1",
},
expectedRequestSigning: apidef.RequestSigningMeta{
IsEnabled: true,
Secret: "secret",
KeyId: "key-1",
Algorithm: "hmac-sha256",
HeaderList: []string{"header1", "header2"},
CertificateId: "cert-1",
SignatureHeader: "Signature",
},
},
}

for _, tc := range testcases {
tc := tc // Creating a new 'tc' scoped to the loop
t.Run(tc.title, func(t *testing.T) {
t.Parallel()

g := new(Upstream)
g.Authentication = &UpstreamAuth{
RequestSigning: tc.input,
}

var apiDef apidef.APIDefinition
apiDef.RequestSigning.HeaderList = []string{"headerOld1", "headerOld2"}
g.ExtractTo(&apiDef)

assert.Equal(t, tc.expectedRequestSigning, apiDef.RequestSigning)
})
}
})
}

func TestLoadBalancing(t *testing.T) {
t.Parallel()
t.Run("fill", func(t *testing.T) {
Expand Down
19 changes: 18 additions & 1 deletion gateway/mw_request_signing.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func (s *RequestSigning) getRequestPath(r *http.Request) string {
}

func (s *RequestSigning) ProcessRequest(w http.ResponseWriter, r *http.Request, _ interface{}) (error, int) {
if (s.Spec.RequestSigning.Secret == "" && s.Spec.RequestSigning.CertificateId == "") || s.Spec.RequestSigning.KeyId == "" || s.Spec.RequestSigning.Algorithm == "" {
if !s.isRequestSigningConfigValid() {
log.Error("Fields required for signing the request are missing")
return errors.New("Fields required for signing the request are missing"), http.StatusInternalServerError
}
Expand Down Expand Up @@ -180,6 +180,23 @@ func (s *RequestSigning) ProcessRequest(w http.ResponseWriter, r *http.Request,
return nil, http.StatusOK
}

func (s *RequestSigning) isRequestSigningConfigValid() bool {
if s.Spec.RequestSigning.KeyId == "" || s.Spec.RequestSigning.Algorithm == "" {
return false
}

isRSAAlgorithm := strings.HasPrefix(s.Spec.RequestSigning.Algorithm, "rsa")
if isRSAAlgorithm && s.Spec.RequestSigning.CertificateId == "" {
return false
}

if !isRSAAlgorithm && s.Spec.RequestSigning.Secret == "" {
return false
}

return true
}

func generateRSAEncodedSignature(signatureString string, privateKey *rsa.PrivateKey, algorithm string) (string, error) {
var hashFunction hash.Hash
var hashType crypto.Hash
Expand Down
Loading
Loading