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

The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details. #62

Closed
mickey101 opened this issue Sep 1, 2023 · 9 comments
Assignees
Labels
bug Something isn't working

Comments

@mickey101
Copy link

mickey101 commented Sep 1, 2023

I have the following configuration while attempting to use AWS Rest API to invoke a lambda.

import http from "k6/http";
import {check, group, sleep} from "k6";
import {randomIntBetween} from "https://jslib.k6.io/k6-utils/1.4.0/index.js"
import {AWSConfig, SignatureV4} from "https://jslib.k6.io/aws/0.9.0/aws.js"

const awsConfig = new AWSConfig({
    region: "eu-west-1",
    accessKeyId: __ENV.AWS_ACCESS_KEY_ID,
    secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY,
    sessionToken: __ENV.AWS_SESSION_TOKEN,
})

const lambdaInvoke = () => {
    const signer = new SignatureV4({
        service: 'lambda',
        region: awsConfig.region,
        credentials: {
            accessKeyId: awsConfig.accessKeyId,
            secretAccessKey: awsConfig.secretAccessKey,
            sessionToken: awsConfig.sessionToken,
        }
    })

    const signedRequest = signer.sign({
        method: 'POST',
        protocol: 'https',
        hostname: 'lambda.eu-west-1.amazonaws.com',
        path: '/2015-03-31/functions/FUNCTION-NAME/invocations',
        headers: {
            'content-type': 'application/json'
        },
        applyChecksum: true,
        uriEscapePath: true
    })
    const data = {
        "hi": 'K6'
    }
    return http.post(signedRequest.url, data, {headers: signedRequest.headers});
};

export default () => {
    group("Spike Test Set", () => {
        const startDataRes = lambdaInvoke();
        console.log(startDataRes)
        sleep(randomIntBetween(0, 1));

        check([startDataRes], {
            "status is 200": (r) => r.every((res) => res.status === 200),
        });
    });
};

It is failing with generating the proper signature. The provided access keys and session tokens are valid. Is there some missing
configuration the I have missed?

@oleiade oleiade self-assigned this Sep 1, 2023
@oleiade
Copy link
Member

oleiade commented Sep 5, 2023

Hi @mickey101 👋🏻

Thanks for reporting it. Just letting you know that I'm not forgetting this, and will get back to it as soon as possible. Thanks for your patience 😸

@mariana-mendes
Copy link

Hello! Any news about it? I'm facing the exact same error. Here is my code, if it helps in the investigations:

import { AWSConfig, SignatureV4 } from 'https://jslib.k6.io/aws/0.9.0/aws.js'
import { getExprs } from './data/get_data.js'
import http from 'k6/http'

export const options = {
    discardResponseBodies: true,
    scenarios: {
      constant_scenario: {
        executor: 'constant-vus',
        exec: 'sigv4',
        vus: 1,
        duration: '1s',
      },
    },
  }
  
const awsConfig = new AWSConfig({
  region: __ENV.AWS_REGION,
  accessKeyId: __ENV.AWS_ACCESS_KEY_ID,
  secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY,
  sessionToken: __ENV.AWS_SESSION_TOKEN,
})

export function sigv4() {
  const signer = new SignatureV4({
    service: 'aps',
    region: awsConfig.region,
    credentials: {
      accessKeyId: awsConfig.accessKeyId,
      secretAccessKey: awsConfig.secretAccessKey,
      sessionToken: awsConfig.sessionToken,
    },
  })

    const exprs = getExprs()
    const now = new Date();
    now.setHours(now.getHours() - 1);
    const start = Math.floor(now.getTime() / 1000)
    const end = Math.floor(Date.now() / 1000)

    for (const expr of exprs) {
      const signedRequest = signer.sign(
        {
          method: 'GET',
          protocol: 'https',
          hostname: 'aps-workspaces.us-east-1.amazonaws.com',
          path: `/workspaces/<my-workspace>/api/v1/query_range?query=${encodeURIComponent(expr)}&start=${start}&end=${end}&step=1m`,
          headers: { "Host": 'aps-workspaces.us-east-1.amazonaws.com' },
          uriEscapePath: false,
          applyChecksum: false,
        },
        {
            signingDate: new Date(),
            signingService: 'aps',
            signingRegion: 'us-east-1',
        }
      )

      const result = http.get(signedRequest.url, { headers: signedRequest.headers })

    }
}

@oleiade
Copy link
Member

oleiade commented Sep 28, 2023

Hey folks 👋🏻 sorry for the delay in my reply, holidays happened 🙇🏻

I'm currently looking into it more thoroughly, but the first thing that strikes me in both your examples is that the uriEscapePath and applyChecksum parameters should be used in the SignatureV4 constructor rather than as an argument to the sign method. I'm not 100% convinced this is it, but that's a good first candidate.

It appears to be a mistake in our documentation website too, which will be addressed as soon as possible.

Does moving those values to the constructor address the issue for you? (and removing them from the sign method call)

  const signer = new SignatureV4({
    service: 'aps',
    region: awsConfig.region,
    credentials: {
      accessKeyId: awsConfig.accessKeyId,
      secretAccessKey: awsConfig.secretAccessKey,
      sessionToken: awsConfig.sessionToken,
    },
    uriEscapePath: false,
    applyChecksum: false,
  })

@oleiade
Copy link
Member

oleiade commented Sep 28, 2023

@mickey101 👋🏻

I've investigated your issue a little bit more, and I think I found the root cause. The Lambda API expects a payload, and when such a payload is present, the sign method needs to know about it. You can set it with the optional body field of the request parameter.

The reason for that is that as part of the AWS signature v4 signing process, the request body is included and used to generate the signature sent to AWS for comparison.

Also, as the body is JSON in your case, we should encode it to a string (this might change in the future, but this is how it's done right now).

Here's an example based on yours that I got to work:

import http from 'k6/http'

import {AWSConfig, SignatureV4} from "https://jslib.k6.io/aws/0.9.0/aws.js"

const awsConfig = new AWSConfig({
    region: __ENV.AWS_REGION,
    accessKeyId: __ENV.AWS_ACCESS_KEY_ID,
    secretAccessKey: __ENV.AWS_SECRET_ACCESS_KEY,
    sessionToken: __ENV.AWS_SESSION_TOKEN,
})

export default function () {
    const data = JSON.stringify({
        key1: 'value1',
        key2: 'value2',
        key3: 'value3',
    })

    /**
     * In order to be able to sign an HTTP request's,
     * we need to instantiate a SignatureV4 object.
     */
    const signer = new SignatureV4({
        service: 'lambda',
        region: awsConfig.region,
        credentials: {
            accessKeyId: awsConfig.accessKeyId,
            secretAccessKey: awsConfig.secretAccessKey,
            sessionToken: awsConfig.sessionToken,
        },
    })

    /**
     * The sign operation will return a new HTTP request with the
     * AWS signature v4 protocol headers added. It returns an Object
     * implementing the SignedHTTPRequest interface, holding a `url` and a `headers`
     * properties, ready to use in the context of k6's http call.
     */
    const signedRequest = signer.sign(
        /**
         * HTTP request description
         */
        {
            /**
             * The HTTP method we will use in the request.
             */
            method: 'POST',

            /**
             * The network protocol we will use to make the request.
             */
            protocol: 'https',

            /**
             * The hostname of the service we will be making the request to.
             */
            hostname: 'lambda.us-east-1.amazonaws.com',

            /**
             * The path of the request.
             */
            path: '/2015-03-31/functions/myFunctionName/invocations',

            /**
             * The headers we will be sending in the request.
             */
            headers: {},

            body: data,
        },
        /**
         * (optional) Signature operation options.
         */
        {
            /**
             * The date and time to be used as signature metadata. This value should be
             * a Date object, a unix (epoch) timestamp, or a string that can be
             * understood by the JavaScript `Date` constructor.If not supplied, the
             * value returned by `new Date()` will be used.
             */
            signingDate: new Date(),

            /**
             * The service signing name. It will override the service name of the signer
             * in current invocation
             */
            signingService: 'lambda',

            /**
             * The region name to sign the request. It will override the signing region of the
             * signer in current invocation
             */
            signingRegion: 'us-east-1',
        }
    )


    const res = http.post(signedRequest.url, data, { headers: signedRequest.headers })
    console.log(JSON.stringify(res, null, 2))
}

@mariana-mendes I haven't double-checked your specific use-case, but could you also verify and try and let me know if that solved the issue on your side? Thank you 🙇🏻

@oleiade oleiade added the bug Something isn't working label Sep 28, 2023
@mariana-mendes
Copy link

@oleiade Thanks for the answer!!

I've tried to use uriEscapePath: false and applyChecksum: false in the constructor, removed them, and also tried combinations of the values. But I am still getting 403 :(

@oleiade
Copy link
Member

oleiade commented Sep 29, 2023

Fair enough 😇

The next thing I notice about your specific example is that you include the query parameters directly in the path parameter of the sign method: path: /workspaces/<my-workspace>/api/v1/query_range?query=${encodeURIComponent(expr)}&start=${start}&end=${end}&step=1m.

I believe this is very likely to lead a signature problem indeed, and the reason is that the signature process encodes and signs query parameters separately following a dedicated process.

Could you try passing the query parameters through the dedicated query parameter of the sign method, and see if that helps? I expect something along the lines of:

      const signedRequest = signer.sign(
        {
          method: 'GET',
          protocol: 'https',
          hostname: 'aps-workspaces.us-east-1.amazonaws.com',
          path: `/workspaces/<my-workspace>/api/v1/query_range`,
          headers: { "Host": 'aps-workspaces.us-east-1.amazonaws.com' },
          query: {  // query params arg
            query: encodeURIComponent(expr),  // your actual prometheus query
            start: start,
            end: end,
            step: '1m',
        },
        {
            signingDate: new Date(),
            signingService: 'aps',
            signingRegion: 'us-east-1',
        }
      )

The resulting signed URL should have the query parameters correctly added, and the signature should correctly account for those too. Let me know if that was helpful 🤝

@mariana-mendes
Copy link

Hi!!, it's working now. My code is something like this:

      const signedRequest = signer.sign(
        {
          method: 'GET',
          protocol: 'https',
          hostname: 'aps-workspaces.us-east-1.amazonaws.com',
          path: '/workspaces/<my-workspace>/api/v1/query_range',
          headers: {"host":'aps-workspaces.us-east-1.amazonaws.com'},
          query: {  // query params arg
            query: expr,
            start: startTime.toString(),
            end: end.toString(),
            step: '1m'
           
        },
        }
      )

I figured out here: In this case, you don't need to encode the query, and the timestamps need to be strings.

I was following this tutorial . The tip of the `query``� parameter is already in some documentation? I couldn't find it.

Thank you so much for your help!!! 🚀

@mickey101
Copy link
Author

Hey @oleiade!
Thanks Including the request body as a string in the signer.sign body parameter solved the signature issue.
I am happy to close this issue

@oleiade
Copy link
Member

oleiade commented Oct 2, 2023

I'm glad we killed two birds with one stone 🤝
I will look into improving the documentation for it accordingly 🙇🏻

@oleiade oleiade closed this as completed Oct 2, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

3 participants