mhart / aws4

Signs and prepares Node.js requests using AWS Signature Version 4
MIT License
703 stars 176 forks source link

Lambda@Edge signature mismatch #163

Closed FabulousGinger closed 6 months ago

FabulousGinger commented 6 months ago

I've looked all through the issues here, however haven't seen something to that quite fits. I have a Lambda@Edge function with an origin request on CloudFront. This will sign the request, return it, and continue to the origin, which is a Lambda function URL. AWS recently released OAC for Lambda, However it still needs a signed payload for PUT and POST. I was able to sign the request with a JSON body using PodMan without issue, which leads me to believe I'm doing something wrong with aws4. Below are some examples.

Lamnda@Edge code

import { fromNodeProviderChain } from '@aws-sdk/credential-providers';
import aws4 from 'aws4';

const credentialProvider = fromNodeProviderChain();

export const handler = async (event) => {
    const request = event.Records[0].cf.request;
    console.info("request:", JSON.stringify(request));

    // Remove x-forwarded-for from the headers
    delete request.headers['x-forwarded-for'];

    if (!request.origin.hasOwnProperty('custom')) {
        throw new Error("Unexpected origin type. Expected 'custom'. Got: " + JSON.stringify(request.origin));
    }

    // Extract query parameters
    const queryParams = new URLSearchParams(request.querystring);
    console.log("queryParams.toString():", queryParams.toString());

    // Remove the "behaviour" path from the URI
    let uri = request.uri.substring(1);
    uri = '/' + uri.split('/').slice(1).join('/');
    request.uri = uri;

    const path = uri + (queryParams.toString() ? '?' + queryParams.toString() : '');
    const hostname = request.headers['host'][0].value;
    const region = hostname.split(".")[2];

    // Get credentials
    const credentials = await credentialProvider();

    const body = JSON.stringify(request.body);

    console.log('body:', body);

    // Sign the request using aws4
    const signedRequest = aws4.sign({
        method: request.method,
        path: path,
        host: hostname,
        service: 'lambda',
        region: region,
        body: body,
        headers: {
            'Content-Type': 'application/json'
        },
        extraHeadersToIgnore: {
            'content-length': true
        },    
    }, credentials);

    console.info("not signed request:", JSON.stringify(request));

    console.log('signedRequest var:', JSON.stringify(signedRequest))

    // Reformat the headers for CloudFront
    for (const header in signedRequest.headers) {
        request.headers[header.toLowerCase()] = [{
            key: header,
            value: signedRequest.headers[header].toString(),
        }];
    }

    console.info("signed request:", JSON.stringify(request));

    return request;
};

The request:

{
    "body": {
        "action": "read-only",
        "data": "eyJjYXQiOiAibGF6eSJ9",
        "encoding": "base64",
        "inputTruncated": false
    },
    "clientIp": "MYIP",
    "headers": {
        "host": [
            {
                "key": "Host",
                "value": "MYLAMBDA.lambda-url.us-west-2.on.aws"
            }
        ],
        "x-forwarded-for": [
            {
                "key": "X-Forwarded-For",
                "value": "IP"
            }
        ],
        "via": [
            {
                "key": "Via",
                "value": "NUMBER.cloudfront.net (CloudFront)"
            }
        ],
        "content-length": [
            {
                "key": "Content-Length",
                "value": "15"
            }
        ],
        "content-type": [
            {
                "key": "Content-Type",
                "value": "application/json"
            }
        ],
        "postman-token": [
            {
                "key": "Postman-Token",
                "value": "TOKEN"
            }
        ]
    },
    "method": "POST",
    "origin": {
        "custom": {
            "customHeaders": {},
            "domainName": "MYLAMBDA.lambda-url.us-west-2.on.aws",
            "keepaliveTimeout": 5,
            "path": "",
            "port": 443,
            "protocol": "https",
            "readTimeout": 30,
            "sslProtocols": [
                "TLSv1.2"
            ]
        }
    },
    "querystring": "",
    "uri": "/v1/log"
}

the signed request:

{
    "body": {
        "action": "read-only",
        "data": "eyJjYXQiOiAibGF6eSJ9",
        "encoding": "base64",
        "inputTruncated": false
    },
    "clientIp": "MYIP",
    "headers": {
        "host": [
            {
                "key": "Host",
                "value": "MYLAMBDA.lambda-url.us-west-2.on.aws"
            }
        ],
        "via": [
            {
                "key": "Via",
                "value": "NUMBERS.cloudfront.net (CloudFront)"
            }
        ],
        "content-length": [
            {
                "key": "Content-Length",
                "value": "15"
            }
        ],
        "content-type": [
            {
                "key": "Content-Type",
                "value": "application/json"
            }
        ],
        "postman-token": [
            {
                "key": "Postman-Token",
                "value": "TOKEN"
            }
        ],
        "x-amz-security-token": [
            {
                "key": "X-Amz-Security-Token",
                "value": "TOKEN"
            }
        ],
        "x-amz-date": [
            {
                "key": "X-Amz-Date",
                "value": "20240417T164705Z"
            }
        ],
        "authorization": [
            {
                "key": "Authorization",
                "value": "AWS4-HMAC-SHA256 Credential=CREDENTIAL/20240417/us-west-2/lambda/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token, Signature=SIGNATURE"
            }
        ]
    },
    "method": "POST",
    "origin": {
        "custom": {
            "customHeaders": {},
            "domainName": "MYLAMBDA.lambda-url.us-west-2.on.aws",
            "keepaliveTimeout": 5,
            "path": "",
            "port": 443,
            "protocol": "https",
            "readTimeout": 30,
            "sslProtocols": [
                "TLSv1.2"
            ]
        }
    },
    "querystring": "",
    "uri": "/log"
}

error message: { "message": "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." }

What I've tried so far:

Any help would be appreciated. For now, I will leave the OAC on and figure out a way to sign the payload and bypass aws4.

FabulousGinger commented 6 months ago

more details:

updated code

   const signedRequest = aws4.sign({
        method: request.method,
        path: path,
        host: hostname,
        service: 'lambda',
        region: region,
        body: request.body.data || '',
        headers: {
            'Content-Type': 'application/json'
        },
        extraHeadersToIgnore: {
            'content-length': true
        }
    }, credentials);

This works with no body, fails with body. below are the scrapped results from signedRequest

works with empty body:

   {
    "method": "POST",
    "path": "/log",
    "host": "MYLAMBDA.lambda-url.us-west-2.on.aws",
    "service": "lambda",
    "region": "us-west-2",
    "body": "",
    "headers": {
        "Content-Type": "application/json",
        "Host": "MYLAMBDA.lambda-url.us-west-2.on.aws",
        "X-Amz-Security-Token": "TOKEN",
        "X-Amz-Date": "20240418T173656Z",
        "Authorization": "AWS4-HMAC-SHA256 Credential=CREDS/20240418/us-west-2/lambda/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token, Signature=SIGNATURE"
    },
    "extraHeadersToIgnore": {
        "content-length": true
    }
}

fails with body:

{
    "method": "POST",
    "path": "/log",
    "host": "MYLAMBDA.lambda-url.us-west-2.on.aws",
    "service": "lambda",
    "region": "us-west-2",
    "body": "eyJjYXQiOiB7ImN1ZGRsZXMiOiAieWVzIiwibGF6eSI6ICJ5ZXMifX0=",
    "headers": {
        "Content-Type": "application/json",
        "Host": "MYLAMBDA.lambda-url.us-west-2.on.aws",
        "X-Amz-Security-Token": "TOKEN",
        "X-Amz-Date": "20240418T173700Z",
        "Authorization": "AWS4-HMAC-SHA256 Credential=CREDS/20240418/us-west-2/lambda/aws4_request, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token, Signature=SIGNATURE"
    },
    "extraHeadersToIgnore": {
        "content-length": true
    }
}

I have also added the following, which both fail signature with body and without:

        headers: {
            'Content-Type': 'application/json',
            'X-Amz-Content-Sha256': 'UNSIGNED-PAYLOAD'
        },
FabulousGinger commented 6 months ago

here is a successful PodMan request, received from the origin bypassing aws4

{
  version: '2.0',
  routeKey: '$default',
  rawPath: '/v1/log',
  rawQueryString: '',
  headers: {
    'x-amz-content-sha256': 'SHA',
    'content-length': '15',
    'x-amzn-tls-version': 'TLSv1.2',
    'x-amz-date': '20240417T171826Z',
    'x-forwarded-proto': 'https',
    'postman-token': 'TOKEN',
    'x-amz-source-account': 'ACCOUNT',
    'x-forwarded-port': '443',
    'x-forwarded-for': 'IP',
    'x-amz-security-token': 'TOKEN',
    via: 'NUMBER.cloudfront.net (CloudFront)',
    'x-amz-source-arn': 'ARN',
    'x-amzn-tls-cipher-suite': 'ECDHE-RSA-AES128-GCM-SHA256',
    'x-amzn-trace-id': 'Root=ID',
    host: 'MYLAMBDA.lambda-url.us-west-2.on.aws',
    'content-type': 'application/json',
    'x-amz-cf-id': 'ID',
    'user-agent': 'Amazon CloudFront'
  },
  requestContext: {
    accountId: 'ACCOUNT',
    apiId: 'ID',
    authorizer: { iam: [Object] },
    domainName: 'MYLAMBDA.lambda-url.us-west-2.on.aws',
    domainPrefix: 'mylambda',
    http: {
      method: 'POST',
      path: '/v1/log',
      protocol: 'HTTP/1.1',
      sourceIp: 'IP',
      userAgent: 'Amazon CloudFront'
    },
    requestId: '7744eb78-b726-440d-9767-1e8a7789ace3',
    routeKey: '$default',
    stage: '$default',
    time: '17/Apr/2024:17:18:26 +0000',
    timeEpoch: 1713374306283
  },
  body: '{"cat": "lazy"}',
  isBase64Encoded: false
}
FabulousGinger commented 6 months ago

successful request received from origin with no body using aws4

{
  version: '2.0',
  routeKey: '$default',
  rawPath: '/log',
  rawQueryString: '',
  headers: {
    'content-length': '0',
    'x-amzn-tls-version': 'TLSv1.2',
    'x-amz-date': '20240418T173656Z',
    'x-forwarded-proto': 'https',
    'postman-token': 'TOKEN',
    'x-forwarded-port': '443',
    'x-forwarded-for': 'IP',
    'x-amz-security-token': 'TOKEN',
    via: 'NUMBER.cloudfront.net (CloudFront)',
    'x-amzn-tls-cipher-suite': 'ECDHE-RSA-AES128-GCM-SHA256',
    'x-amzn-trace-id': 'Root=ID',
    host: 'MYLAMBDA.lambda-url.us-west-2.on.aws',
    'content-type': 'application/json',
    'x-amz-cf-id': 'ID',
    'user-agent': 'Amazon CloudFront'
  },
  requestContext: {
    accountId: 'ACCOUNT',
    apiId: 'ID',
    authorizer: { iam: [Object] },
    domainName: 'MYLAMBDA.lambda-url.us-west-2.on.aws',
    domainPrefix: 'mylambda',
    http: {
      method: 'POST',
      path: '/log',
      protocol: 'HTTP/1.1',
      sourceIp: 'IP',
      userAgent: 'Amazon CloudFront'
    },
    requestId: 'd6db08b7-9d2c-4c03-9c7f-0efa800c74bd',
    routeKey: '$default',
    stage: '$default',
    time: '18/Apr/2024:17:36:56 +0000',
    timeEpoch: 1713461816462
  },
  isBase64Encoded: false
}
mhart commented 6 months ago

content-length

I notice in a few of your examples you're not adding content-length to the headers list when you have a body – you're just adding content-type