mhart / aws4

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

Request signature we calculated does not match signature you provided #101

Closed max77p closed 4 years ago

max77p commented 4 years ago

I am getting the above error..am I passsing the accesskey and secretaccesskey correctlhy to sign this request in axios?

`axios(
    aws4.sign(
      {
        host: domain,
        method: "GET",
        url: `https://${domain}/bldglink/_search?q=warden`
      },
      {
        accessKeyId: "accesskey",
        secretAccessKey: "secretkey"
      }
    )
  )
    .then(res => {
      console.log(res);
    })
    .catch(e => {
      console.log(e);
    });`
epegzz commented 4 years ago

I am getting the same error message when trying to use SES.

I can make it work however, when using region us-east-1 instead of eu-central-1

Using the AWS CLI both regions are working.

Here's some code to reproduce the issue:

const aws4 = require('aws4')
const request = require('request')
const querystring = require('querystring')

const credentials = {
  accessKeyId: 'XXXXXXXXXX',
  secretAccessKey: 'XXXXXXXXXX'
}

const fromEmail = 'some@email.com'
const toEmail = 'some@email.com'

const options = {
  // Those DO NOT work:
  uri: 'https://email.eu-central-1.amazonaws.com',
  host: 'email.eu-central-1.amazonaws.com',

  // Those DO work:
  //uri: 'https://email.us-east-1.amazonaws.com',
  //host: 'email.us-east-1.amazonaws.com',

  headers: {
    Connection: 'Keep-Alive',
  },

  service: 'ses',
  body: querystring.stringify({
    Action: 'SendEmail',
    AWSAccessKeyId: credentials.accessKeyId,
    Source: fromEmail,
    'ReplyToAddresses.member.1': fromEmail,
    'Destination.ToAddresses.member.1': toEmail,
    'Message.Subject.Data': 'Test',
    'Message.Subject.Charset': 'UTF-8',
    'Message.Body.Text.Data': 'Test',
    'Message.Body.Text.Charset': 'UTF-8',
  })
}

const signedOpts = aws4.sign(options, credentials)

console.log({ signedOpts})

request(signedOpts, function (err, res) {
  console.log(res.body)
})
epegzz commented 4 years ago

I just figured, that not passing the host option fixes it

mhart commented 4 years ago

If you don't pass all the following options, then aws4 tries to figure out which you mean: host, region, service. If you're having errors, then you need to make sure all of these match

mhart commented 4 years ago

Closing this – it seems like an axios issue (unless you can reproduce with the Node.js http/https module)

epegzz commented 4 years ago

@mhart actually NOT passing the host option does not fix it. This just causes the Host header to be set to the default region us-east-1 which means I am ending up using the wrong region 😬

As soon as I pass hostname or host to 'eu-central-1' I get the SignatureDoesNotMatch error.

mhart commented 4 years ago

Right, but in your code it doesn't look like you're setting region

mhart commented 4 years ago

Just try setting service (ses) and region (eu-central-1) – aws4 should automatically figure out the host from those two and you won't need to supply it.

epegzz commented 4 years ago

Okay, tried that. Only passing region and service, but same result.

And yeah, aws4 DOES figure out hostname automatically based on service and region. But with region set to eu-central-1 the request results in the SignatureDoesNotMatch error.

This is the signedOpts for eu-central-1 which does not work:

{ signedOpts:
   { region: 'eu-central-1',
     headers:
      { Connection: 'Keep-Alive',
        Host: 'email.eu-central-1.amazonaws.com:443',
        'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
        'Content-Length': 297,
        'X-Amz-Date': '20200414T195958Z',
        Authorization:
         'AWS4-HMAC-SHA256 Credential=XXXXXX/20200414/eu-central-1/ses/aws4_request, SignedHeaders=connection;content-length;content-type;host;x-amz-date, Signature=XXXXXX' },
     port: 443,
     method: 'POST',
     service: 'ses',
     body: '…',
     hostname: 'email.eu-central-1.amazonaws.com:443',
     path: '/' } }

And the same for us-east-1 which does work:

{ signedOpts:
   { region: 'us-east-1',
     headers:
      { Connection: 'Keep-Alive',
        Host: 'email.us-east-1.amazonaws.com:443',
        'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
        'Content-Length': 297,
        'X-Amz-Date': '20200414T200253Z',
        Authorization:
         'AWS4-HMAC-SHA256 Credential=XXXXXX/20200414/us-east-1/ses/aws4_request, SignedHeaders=connection;content-length;content-type;host;x-amz-date, Signature=XXXXXX' },
     port: 443,
     method: 'POST',
     service: 'ses',
     body: '…',
     hostname: 'email.us-east-1.amazonaws.com:443',
     path: '/' } }

And again, using the CLI works with both regions :(

epegzz commented 4 years ago

I tried replacing all us-east-1 with eu-central-1 in aws4.js to see if different fallbacks have any impact, but did not change anything. The host in the header is set correctly, just the signature is not valid.

mhart commented 4 years ago

How are you making your http requests? Are you using the Node.js http/https modules? Or are you using a third-party library? Please post the code you're using to make the request.

mhart commented 4 years ago

EDIT: nevermind about the host question, let's just figure out what code is being used to make the request

epegzz commented 4 years ago

I was using the request lib, but I get the same result using the https lib.

const aws4 = require('aws4')
const https = require('https')
const querystring = require('querystring')

const credentials = {
  accessKeyId: 'XXXXXXX',
  secretAccessKey: 'XXXXXXX'
}

const fromEmail = 'mail@example.org'
const toEmail = 'mail@example.org'

const options = {
  service: 'ses',
  region: 'eu-central-1',

  headers: {
    Connection: 'Keep-Alive',
  },

  body: querystring.stringify({
    Action: 'SendEmail',
    AWSAccessKeyId: credentials.accessKeyId,
    Source: fromEmail,
    'ReplyToAddresses.member.1': fromEmail,
    'Destination.ToAddresses.member.1': toEmail,
    'Message.Subject.Data': 'Testxxx',
    'Message.Subject.Charset': 'UTF-8',
    'Message.Body.Text.Data': 'Test',
    'Message.Body.Text.Charset': 'UTF-8',
  })
}

const signedOpts = aws4.sign(options, credentials)

const req = https.request({
  hostname: 'email.eu-central-1.amazonaws.com',
  port: 443,
  path: '/',
  method: 'POST',
  headers: signedOpts.headers
}, res => {
  console.log(`statusCode: ${res.statusCode}`)

  res.on('data', d => {
    process.stdout.write(d)
  })
})

req.on('error', error => {
  console.error(error)
})

req.write(signedOpts.body)
req.end()

Prints:

statusCode: 403
<ErrorResponse xmlns="http://ses.amazonaws.com/doc/2010-12-01/">
  <Error>
    <Type>Sender</Type>
    <Code>SignatureDoesNotMatch</Code>
    <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.</Message>
  </Error>
  <RequestId>7d62a9c3-f1d2-4f26-bd42-90222b40cba9</RequestId>
</ErrorResponse>

Value of signedOpts:

{ region: 'eu-central-1',
     headers:
      { Connection: 'Keep-Alive',
        Host: 'email.eu-central-1.amazonaws.com:443',
        'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
        'Content-Length': 297,
        'X-Amz-Date': '20200414T203457Z',
        Authorization:
         'AWS4-HMAC-SHA256 Credential=XXXXXXXX/20200414/eu-central-1/ses/aws4_request, SignedHeaders=connection;content-length;content-type;host;x-amz-date, Signature=3ee97a7149f5ca7f0274e2e20fb5c9363e70748cfdc2278e916de250b6e9b755' },
     port: 443,
     method: 'POST',
     service: 'ses',
     body: '…',
     hostname: 'email.eu-central-1.amazonaws.com:443',
     path: '/'
}
mhart commented 4 years ago

You're only passing the headers. The README has an example of how to use this library.

const req = https.request(signedOpts)
mhart commented 4 years ago

(also, try removing the Connection: 'Keep-Alive')

epegzz commented 4 years ago

.....it was the Keep-Alive header! But why the hell does it work with region us-east-1?? I am so puzzled.

epegzz commented 4 years ago

Anyway, thank you very much for the support @mhart!πŸ™ πŸ™‚

mhart commented 4 years ago

Not sure – it might be that different regions have different versions of the signing code for verifying the signatures.

You can always try adding the Connection header again after signing. Eg:

const signedOpts = aws4.sign(options, credentials)
signedOpts.headers.Connection = 'keep-alive'
const req = https.request(signedOpts)
mhart commented 4 years ago

Also, I've never seen it capitalized before – it might be expecting 'keep-alive' (or, aws4 is signing it as 'Keep-Alive', but then Node.js is changing it to 'keep-alive' before it sends, and so the server gets a mismatch)

epegzz commented 4 years ago

I tried lowercase keep-alive but this has no effect either. So I changed the code to add the header after signing. Thanks again for your help πŸ‘Œ