title: DynamoDB API: 400 Bad Request without response data
body: <p>Last week I worked on the implementation of Signature Version 4 on Javascript (not SDK because I do not like external dependencies). During debugging many times I faced with the error that my signature is incorrect. And now I see the same 400 Bad Request but without response data - so I have no idea what to do with that.</p>
  
  <p>I checked my code and documentation few times.</p>
  
  <p>Below code about signing (maybe will be useful for somebody too as the example of implementation). <code>storageCache['Credentials']</code> is the data returned from <code>AWSCognitoIdentityService.GetCredentialsForIdentity</code>.</p>
  
  <pre><code>async function _updatePlayedStatus(id, val) {
  
      const dateObj = new Date();
  
      const body = JSON.stringify({
          'TableName': 'history',
          'Key': {'email': nodes.email.innerText, 'utc': id},
          'UpdateExpression': 'SET isNew = :isNew, updated = :updated',
          'ExpressionAttributeValues': {
              ':isNew': val.toString(),
              ':updated': dateObj.getTime() / 1000 | 0}
      });
  
      const headers = {
          'Authorization': await _buildAuthorizationHeaderForDynamoDB(
              storageCache['Credentials']['SessionToken'],
              storageCache['Credentials']['SecretKey'],
              storageCache['Credentials']['AccessKeyId'],
              body,
              dateObj),
          'content-type': 'application/x-amz-json-1.0',
          'x-amz-date': _getDatetimeIn8601Format(dateObj),
          'x-amz-target': 'DynamoDB_20120810.UpdateItem',
          'x-amz-security-token': storageCache['Credentials']['SessionToken']
          /* Without date will be 400 Bad Request.
           * I tried shorter Date instead of x-amz-date but 400 Bad Request
           * com.amazon.coral.service#IncompleteSignatureException
  
           * 'Authorization header requires existence of either a 'X-Amz-Date' or a 'Date' header.'
           * When 'Date' - I see it in console.debug() but not in request headers.
           */
      };
      console.debug('headers:', headers);
      fetch('https://dynamodb.us-east-1.amazonaws.com', {
          'method': 'POST',
          'headers': headers,
          'body': body
      });
  }
  
  
  /**
   * https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
   */
  async function _buildAuthorizationHeaderForDynamoDB(SessionToken, SecretKey, AccessKeyId, body, dateObj) {
      console.debug('_buildAuthorizationHeaderForDynamoDB args:', arguments);
  
      /** Task 1: Create a Canonical Request for Signature Version 4
       *  https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html */
      const canonicalRequest = `POST
  /
  
  content-type:application/x-amz-json-1.0
  host:dynamodb.us-east-1.amazonaws.com
  x-amz-date:${_getDatetimeIn8601Format(dateObj)}
  x-amz-security-token:${SessionToken}
  x-amz-target:DynamoDB_20120810.UpdateItem
  content-type;host;x-amz-date;x-amz-security-token;x-amz-target
  ${await _sha256(body)}`;
      const dateStr = dateObj.toISOString().slice(0, 10).replace(/-/g, '');
      // YYYYMMDD, for example 20150830
  
      /** Task 2: Create a String to Sign for Signature Version 4
       *  https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html */
      const stringToSign = `AWS4-HMAC-SHA256
  ${_getDatetimeIn8601Format(dateObj)}
  ${dateStr}/us-east-1/dynamodb/aws4_request
  ${await _sha256(canonicalRequest)}`;
  
      /** Task 3: Calculate the Signature for AWS Signature Version 4
      *   https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
      *   https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign */
  
      const credential = `${AccessKeyId}/${dateStr}/us-east-1/dynamodb/aws4_request`;
  
      const kDate = await crypto.subtle.sign(
          'HMAC',
          await _importKey(gTextEncoder.encode(`AWS4${SecretKey}`)),
          gTextEncoder.encode(dateStr)
      );
  
      const kRegion = await crypto.subtle.sign(
          'HMAC',
          await _importKey(kDate),
          gTextEncoder.encode('us-east-1')
      );
  
      const kService = await crypto.subtle.sign(
          'HMAC',
          await _importKey(kRegion),
          gTextEncoder.encode('dynamodb')
      );
  
      const kSigning = await crypto.subtle.sign(
          'HMAC',
          await _importKey(kService),
          gTextEncoder.encode('aws4_request')
      );
  
      const signatureArrayBuffer = await crypto.subtle.sign(
          'HMAC',
          await _importKey(kSigning),
          gTextEncoder.encode(stringToSign)
      );
  
      const signatureHex = _binaryToHex(Array.from(new Uint8Array(signatureArrayBuffer)));
      // Array.from() because ArrayBuffer does not have the map() method.
  
      return `AWS4-HMAC-SHA256 Credential=${credential}, SignedHeaders=content-type;host;x-amz-date;x-amz-security-token;x-amz-target, Signature=${signatureHex}`;
  }
  
  
  /**
   * https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/importKey
   */
  async function _importKey(keyDataArrayBuffer) {
      return crypto.subtle.importKey(
          'raw',
          keyDataArrayBuffer,
          {'name': 'HMAC', 'hash': {'name': 'SHA-256'}},
          false,
          ['sign']);
  }
  
  
  /**
   * Function from
   * https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
   */
  async function _sha256(message) {
  
      // encode as UTF-8
      const msgBuffer = gTextEncoder.encode(message);
  
      // hash the message
      const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
  
      // convert ArrayBuffer to Array
      const hashArray = Array.from(new Uint8Array(hashBuffer));
  
      return _binaryToHex(hashArray);
  }
  
  
  function _binaryToHex(array) {
      return array.map(b =&gt; ('00' + b.toString(16)).slice(-2)).join('');
  }
  
  
  /** Date format is specified with ISO8601 basic format
   * YYYYMMDD'T'HHMMSS'Z'
   * for example
   * 20180112T101849
   */
  function _getDatetimeIn8601Format(dateObj) {
      return dateObj.toISOString().replace(/[:-]/g, '').slice(0, 15) + 'Z';
  }
  </code></pre>