From 7710c76ee19b6b8108cf832ba93791e16a574f76 Mon Sep 17 00:00:00 2001 From: Ananth Bhaskararaman Date: Mon, 9 Oct 2023 11:45:24 +0530 Subject: [PATCH] CertAuthz tests --- internal/middleware/certauthz.go | 14 +-- internal/middleware/certauthz_test.go | 117 ++++++++++++++++++++++++++ internal/middleware/requestcontext.go | 20 +++-- internal/middleware/tlsident.go | 3 +- 4 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 internal/middleware/certauthz_test.go diff --git a/internal/middleware/certauthz.go b/internal/middleware/certauthz.go index 9cd3cbc..a95d663 100644 --- a/internal/middleware/certauthz.go +++ b/internal/middleware/certauthz.go @@ -18,11 +18,14 @@ import ( const ServiceUnavailableMsg = "guru meditation error" +type authzFn func(context.Context, AuthenticatedRequestContext) (events.APIGatewayCustomAuthorizerResponse, error) + // CertAuthorizer returns a Lambda Authorizer function that authorizes requests -// based on the client certificate. -func CertAuthorizer( - namespace uuid.UUID, -) func(context.Context, AuthenticatedRequestContext) (events.APIGatewayCustomAuthorizerResponse, error) { +// based on the client certificate in the request context. +// If the certificate is valid, the Authorizer returns an Allow policy. +// The the certificate namespace does not match the configured namespace, +// the Authorizer returns a Deny policy. +func CertAuthorizer(namespace uuid.UUID) authzFn { return func(ctx context.Context, rctx AuthenticatedRequestContext) (events.APIGatewayCustomAuthorizerResponse, error) { block, _ := pem.Decode([]byte(rctx.Authentication.ClientCert.ClientCertPem)) if block == nil { @@ -37,8 +40,7 @@ func CertAuthorizer( return events.APIGatewayCustomAuthorizerResponse{}, errors.New(ServiceUnavailableMsg) } - var pubKey JWK - pubKey.FromECDSA(cert.PublicKey) + pubKey := JWKFromECDSA(cert.PublicKey) pubKeyStr, err := json.Marshal(pubKey) if err != nil { slog.ErrorCtx(ctx, "failed to marshal public key", "error", err) diff --git a/internal/middleware/certauthz_test.go b/internal/middleware/certauthz_test.go new file mode 100644 index 0000000..6ef4a4d --- /dev/null +++ b/internal/middleware/certauthz_test.go @@ -0,0 +1,117 @@ +package middleware + +import ( + "context" + "reflect" + "testing" + + "github.com/aws/aws-lambda-go/events" + "github.com/google/uuid" +) + +type certAuthzTestCase struct { + ns uuid.UUID + in AuthenticatedRequestContext + out events.APIGatewayCustomAuthorizerResponse + err bool +} + +var certAuthzTestCases = []certAuthzTestCase{ + { + ns: uuid.MustParse("80485314-6c73-40ff-86c5-a5942a0f514f"), + in: AuthenticatedRequestContext{ + APIGatewayCustomAuthorizerRequest: events.APIGatewayCustomAuthorizerRequest{ + MethodArn: "arn:aws:execute-api:us-east-1:123456789012:api-id/stage-name/GET/resource-path", + }, + Authentication: Authentication{ + ClientCert: ClientCert{ + ClientCertPem: "-----BEGIN CERTIFICATE-----\nMIIB4DCCAYagAwIBAgIBATAKBggqhkjOPQQDAjBeMS0wKwYDVQQKEyQ4MDQ4NTMx\nNC02YzczLTQwZmYtODZjNS1hNTk0MmEwZjUxNGYxLTArBgNVBAMTJGI5Mjg5ZGE3\nLTg4MTMtNTFlZC05NTdiLWI2YmM1YTRkNjQxNjAeFw0yMzA5MjAxODQyMDhaFw0y\nMzA5MjAxOTQyMDhaMF4xLTArBgNVBAoTJDgwNDg1MzE0LTZjNzMtNDBmZi04NmM1\nLWE1OTQyYTBmNTE0ZjEtMCsGA1UEAxMkYjkyODlkYTctODgxMy01MWVkLTk1N2It\nYjZiYzVhNGQ2NDE2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7pyPlY0DYYm7\n8D+BugKXrNDxXn2NfOibB+wV3IMGBRiL8D6rhJuTWcgMUmhuPI6Ssy9yKexpxNYV\nrxsvwF84u6M1MDMwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB\nMAwGA1UdEwEB/wQCMAAwCgYIKoZIzj0EAwIDSAAwRQIhAPXeYIqFROWKpYrBwN9M\n96rmqQJcC9+x+N0n6PzVfB96AiA5d/3q16GG219mdSpc05CtFpYp4CW/oVzlwUQt\nc+gqcQ==\n-----END CERTIFICATE-----", + IssuerDN: "CN=b9289da7-8813-51ed-957b-b6bc5a4d6416,O=80485314-6c73-40ff-86c5-a5942a0f514f", + SubjectDN: "CN=b9289da7-8813-51ed-957b-b6bc5a4d6416,O=80485314-6c73-40ff-86c5-a5942a0f514f", + }, + }, + }, + out: events.APIGatewayCustomAuthorizerResponse{ + PrincipalID: "b9289da7-8813-51ed-957b-b6bc5a4d6416", + PolicyDocument: events.APIGatewayCustomAuthorizerPolicy{ + Version: "2012-10-17", + Statement: []events.IAMPolicyStatement{ + { + Action: []string{"execute-api:Invoke"}, + Effect: "Allow", + Resource: []string{ + "arn:aws:execute-api:us-east-1:123456789012:api-id/stage-name/GET/resource-path", + }, + }, + }, + }, + Context: map[string]interface{}{ + "namespace": "80485314-6c73-40ff-86c5-a5942a0f514f", + "publicKey": "{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"107927077086532896835579100061901814530678651729391130141381261794751161959704\",\"y\":\"63295961781010443906011747343675505672305089399194087223428542059136675690683\"}", + }, + }, + }, + { + in: AuthenticatedRequestContext{}, + err: true, + }, + { + in: AuthenticatedRequestContext{ + Authentication: Authentication{ + ClientCert: ClientCert{ + ClientCertPem: "-----BEGIN CERTIFICATE REQUEST-----\nMIIBGTCBwAIBADBeMS0wKwYDVQQDDCQ3NmViZGJkNS1kYzQwLTU4YzEtYTEwMS0x\nY2U2YjBlZDllYjAxLTArBgNVBAoMJGQyNTdjMTY3LTFhOTgtNDkwZS04MWEzLWIz\nOTVmMWFiZmY3YTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABIRKO/ou3QfVp5Ym\naKyBForLVwIKx67Ts9q1tC2lyGXCTYhFAFpE8zBSq2NCWT1QaFBF4GBh4Ve4XNyH\nf/l+B/agADAKBggqhkjOPQQDAgNIADBFAiAaAejXa589rggsr3VHeTAkbi1ULSXw\njDIeM4TUVgM2cgIhAOy09QkVAYVeq2ksf6n/kCMm2CAZNX5wLjVzpRUCaD6T\n-----END CERTIFICATE REQUEST-----", + }, + }, + }, + err: true, + }, + { + ns: uuid.MustParse("b9289da7-8813-51ed-957b-b6bc5a4d6416"), + in: AuthenticatedRequestContext{ + APIGatewayCustomAuthorizerRequest: events.APIGatewayCustomAuthorizerRequest{ + MethodArn: "arn:aws:execute-api:us-east-1:123456789012:api-id/stage-name/GET/resource-path", + }, + Authentication: Authentication{ + ClientCert: ClientCert{ + ClientCertPem: "-----BEGIN CERTIFICATE-----\nMIIB4DCCAYagAwIBAgIBATAKBggqhkjOPQQDAjBeMS0wKwYDVQQKEyQ4MDQ4NTMx\nNC02YzczLTQwZmYtODZjNS1hNTk0MmEwZjUxNGYxLTArBgNVBAMTJGI5Mjg5ZGE3\nLTg4MTMtNTFlZC05NTdiLWI2YmM1YTRkNjQxNjAeFw0yMzA5MjAxODQyMDhaFw0y\nMzA5MjAxOTQyMDhaMF4xLTArBgNVBAoTJDgwNDg1MzE0LTZjNzMtNDBmZi04NmM1\nLWE1OTQyYTBmNTE0ZjEtMCsGA1UEAxMkYjkyODlkYTctODgxMy01MWVkLTk1N2It\nYjZiYzVhNGQ2NDE2MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE7pyPlY0DYYm7\n8D+BugKXrNDxXn2NfOibB+wV3IMGBRiL8D6rhJuTWcgMUmhuPI6Ssy9yKexpxNYV\nrxsvwF84u6M1MDMwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMB\nMAwGA1UdEwEB/wQCMAAwCgYIKoZIzj0EAwIDSAAwRQIhAPXeYIqFROWKpYrBwN9M\n96rmqQJcC9+x+N0n6PzVfB96AiA5d/3q16GG219mdSpc05CtFpYp4CW/oVzlwUQt\nc+gqcQ==\n-----END CERTIFICATE-----", + IssuerDN: "CN=b9289da7-8813-51ed-957b-b6bc5a4d6416,O=80485314-6c73-40ff-86c5-a5942a0f514f", + SubjectDN: "CN=b9289da7-8813-51ed-957b-b6bc5a4d6416,O=80485314-6c73-40ff-86c5-a5942a0f514f", + }, + }, + }, + out: events.APIGatewayCustomAuthorizerResponse{ + PrincipalID: "b9289da7-8813-51ed-957b-b6bc5a4d6416", + PolicyDocument: events.APIGatewayCustomAuthorizerPolicy{ + Version: "2012-10-17", + Statement: []events.IAMPolicyStatement{ + { + Action: []string{"execute-api:Invoke"}, + Effect: "Deny", + Resource: []string{ + "arn:aws:execute-api:us-east-1:123456789012:api-id/stage-name/GET/resource-path", + }, + }, + }, + }, + }, + }, +} + +func TestCertAuthorizer(t *testing.T) { + for _, tc := range certAuthzTestCases { + out, err := CertAuthorizer(tc.ns)(context.Background(), tc.in) + if tc.err { + if err == nil { + t.Errorf("expected error, got nil") + } + continue + } + if err != nil { + t.Errorf("unexpected error: %v", err) + continue + } + if !reflect.DeepEqual(out, tc.out) { + t.Errorf("expected %v, got %v", tc.out, out) + } + } +} diff --git a/internal/middleware/requestcontext.go b/internal/middleware/requestcontext.go index 84a1a61..ade5a0c 100644 --- a/internal/middleware/requestcontext.go +++ b/internal/middleware/requestcontext.go @@ -73,13 +73,6 @@ func (j *JWK) UnmarshalJSON(data []byte) error { return nil } -func (j *JWK) FromECDSA(key *ecdsa.PublicKey) { - j.KeyType = keyTypeEC - j.Curve = curveP256 - j.X = key.X.String() - j.Y = key.Y.String() -} - func (j JWK) ToECDSA() (*ecdsa.PublicKey, bool) { var x, y big.Int if _, ok := x.SetString(j.X, 10); !ok { @@ -95,6 +88,19 @@ func (j JWK) ToECDSA() (*ecdsa.PublicKey, bool) { }, true } +func (j *JWK) FromECDSA(key *ecdsa.PublicKey) { + j.KeyType = keyTypeEC + j.Curve = curveP256 + j.X = key.X.String() + j.Y = key.Y.String() +} + +func JWKFromECDSA(key *ecdsa.PublicKey) JWK { + var j JWK + j.FromECDSA(key) + return j +} + type AuthenticatedRequestContext struct { events.APIGatewayCustomAuthorizerRequest Authentication Authentication `json:"authentication"` diff --git a/internal/middleware/tlsident.go b/internal/middleware/tlsident.go index 2b40895..8301bf6 100644 --- a/internal/middleware/tlsident.go +++ b/internal/middleware/tlsident.go @@ -46,8 +46,7 @@ func TLSIdentifier(namespace uuid.UUID) func(http.Handler) http.Handler { return } - var j JWK - j.FromECDSA(cert.PublicKey) + j := JWKFromECDSA(cert.PublicKey) rctx := AuthorizedRequestContext{ Authorizer: Authorizer{