Получение 400 неверных запросов при выполнении запроса S3 GET - PullRequest
0 голосов
/ 08 апреля 2020

Я пытаюсь сделать авторизацию AWS версии 4, подписанную GET-запросом к S3, и получаю ошибку неверного запроса 400 Code:InvalidRequest Message:Missing required header для этого запроса: x-amz-content-sha256

Если я добавляю заголовок к "Authorization: ", я получаю ошибку Code:InvalidArgument Message:Unsupported Authorization Type <ArgumentName>Authorization</ArgumentName> <ArgumentValue>Authorization: AWS4-HMAC-SHA256 Credential=XXXXXXXXXXXXXXXXXXX/20200408/eu-west-3/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=vdchzint97uwyt3g%2fjehszrc8zpkbjsx4tfqacsqfow%3d</ArgumentValue>

Я использую Delphi XE5 с компонентом Indy's TIdHTTP. Может кто-нибудь сказать мне, что я делаю не так? Я включил свой код ниже.

begin
  bucket := 'mybucket.ata-test';
  obj := 'test.xml';
  region := 'eu-west-3';
  service := 's3';
  aws := 'amazonaws.com';
  YYYYMMDD := FormatDateTime('yyyymmdd', now);
  amzDate := FormatDateTime('yyyymmdd"T"hhnnss"Z"', TTimeZone.Local.ToUniversalTime(Now), TFormatSettings.Create('en-US'));
  emptyHash := lowercase(SHA256HashAsHex(''));
  host := Format('%s.%s.%s.%s', [bucket, service, region, aws]);
  url := Format('%s://%s.%s.%s.%s/%s', ['https', bucket, service, region, aws, obj]);

// *** 1. Build the Canonical Request for Signature Version 4 ***
  // HTTPRequestMethod
  CanonicalRequest := URLEncodeValue('GET') +#10;
  // CanonicalURI
  CanonicalRequest := CanonicalRequest + '/' + URLEncodeValue(obj) +#10;
  // CanonicalQueryString (empty just a newline)
  CanonicalRequest := CanonicalRequest +#10;
  // CanonicalHeaders
  CanonicalRequest := CanonicalRequest + 'host:' + Trim(host) +#10
                                       + 'x-amz-content-sha256:' + emptyHash +#10
                                       + 'x-amz-date:' + Trim(amzDate) +#10;
  // SignedHeaders
  CanonicalRequest := CanonicalRequest + 'host;x-amz-content-sha256;x-amz-date' +#10;
  // HexEncode(Hash(RequestPayload)) - (hash of an empty string)
  CanonicalRequest := CanonicalRequest + emptyHash;

// *** 2. Create a String to Sign for Signature Version 4 ***
  StringToSign := 'AWS4-HMAC-SHA256' +#10
                  + amzDate +#10
                  + UTF8String(YYYYMMDD) +'/'+ UTF8String(region) +'/'+ UTF8String(service) +UTF8String('/aws4_request') +#10
                  + lowercase(SHA256HashAsHex(CanonicalRequest));

// *** 3. Calculate the Signature for AWS Signature Version 4 ***
  DateKey := CalculateHMACSHA256(YYYYMMDD, 'AWS4' + SecretAccessKey);
  DateRegionKey := CalculateHMACSHA256(region, DateKey);
  DateRegionServiceKey := CalculateHMACSHA256(service, DateRegionKey);
  SigningKey := CalculateHMACSHA256('aws4_request', DateRegionServiceKey);

  Signature := lowercase(UrlEncodeValue(CalculateHMACSHA256(StringToSign, SigningKey)));

// *** 4. Create Authorisation Header and Add the Signature to the HTTP Request ***
  AuthorisationHeader := 'AWS4-HMAC-SHA256 Credential='+AccessIdKey+'/'+YYYYMMDD+'/'+region+'/'+service+'/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature='+signature;  
// (Gives <Code>InvalidRequest</Code> <Message>Missing required header for this request: x-amz-content-sha256</Message>)

// Have also tried
// AuthorisationHeader := 'Authorization: AWS4-HMAC-SHA256 Credential='+AccessIdKey+'/'+YYYYMMDD+'/'+region+'/'+service+'/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature='+signature;  
// (Gives <Code>InvalidArgument</Code> <Message>Unsupported Authorization Type</Message>)

// *** 5. Add Header and Make Request ***
  stm := TMemoryStream.Create;
  try
    try
      Idhttp.Request.CustomHeaders.FoldLines := False;
      Idhttp.Request.CustomHeaders.AddValue('Authorization', AuthorisationHeader);
      Idhttp.Get(URL, stm);
    except
      on PE: EIdHTTPProtocolException do begin
        s := PE.ErrorMessage;
        Raise;
      end;
      on E: Exception do begin
        s := E.Message;
        Raise;
      end;
    end;

    stm.Position := 0;
    Memo1.Lines.LoadFromStream(stm);
  finally
    FreeAndNil(stm);
  end; 
end;

function SHA256HashAsHex(const value: string): String;
/// used for stringtosign
var
  sha: TIdHashSHA256;
begin
  LoadOpenSSLLibrary;
  if not TIdHashSHA256.IsAvailable then
    raise Exception.Create('SHA256 hashing is not available!');
  sha := TIdHashSHA256.Create;
  try
    result := sha.HashStringAsHex(value, nil);
  finally
    sha.Free;
  end;
end;

function CalculateHMACSHA256(const value, salt: String): String;
/// used for signingkey
var
  hmac: TIdHMACSHA256;
  hash: TIdBytes;
begin
  LoadOpenSSLLibrary;
  if not TIdHashSHA256.IsAvailable then
    raise Exception.Create('SHA256 hashing is not available!');
  hmac := TIdHMACSHA256.Create;
  try
    hmac.Key := IndyTextEncoding_UTF8.GetBytes(salt);
    hash := hmac.HashValue(IndyTextEncoding_UTF8.GetBytes(value));
    Result := EncodeBytes64(TArray<Byte>(hash));
  finally
    hmac.Free;
  end;
end;

1 Ответ

0 голосов
/ 08 апреля 2020

В вашем коде я заметил несколько вещей:

  • при создании значений YYYYMMDD и amzDate вы дважды вызываете Now(), что создает условие гонки с Потенциал из-за того, что эти переменные представляют разные даты. Вряд ли, но возможно. Чтобы избежать этого, вы должны вызывать Now() только 1 раз и сохранять результат в локальной переменной TDateTime, а затем использовать эту переменную во всех ваших FormatDateTime() вызовах.
dtNow := Now();
YYYYMMDD := FormatDateTime('yyyymmdd', dtNow);
amzDate := FormatDateTime('yyyymmdd"T"hhnnss"Z"', TTimeZone.Local.ToUniversalTime(dtNow), TFormatSettings.Create('en-US'));
  • При использовании свойства TIdHTTP *1017* для установки настраиваемого заголовка Authorization убедитесь, что для свойства Request.BasicAuthentication также установлено значение False, в противном случае TIdHTTP может создать свое собственное Authorization: Basic ... заголовок, используя его свойства Request.Username и Request.Password. Вам не нужны два Authorization заголовка в вашем GET запросе.
Idhttp.Request.BasicAuthentication := False;
  • Вы используете x-amz-content-sha256 и x-amz-date заголовки в своих вычислениях авторизации, но вы не добавляют эти заголовки к фактическому HTTP-запросу. TIdHTTP добавит заголовок Host для вас, но вам нужно добавить другие заголовки самостоятельно.
Idhttp.Request.CustomHeaders.AddValue('x-amz-content-sha256', emptyHash);
Idhttp.Request.CustomHeaders.AddValue('x-amz-date', amzDate);
  • Ваша функция SHA256HashAsHex() не указывает байтную кодировку, когда вызов метода TIdHashSHA256.HashStringAsHex() в Indy (на самом деле он явно пытается установить кодировку на nil). Таким образом, будет использоваться байтовая кодировка Indy по умолчанию, которая является US-ASCII (если только вы не установите переменную Indy GIdDefaultTextEncoding в блоке IdGlobal на что-то другое). Однако ваша CalculateHMACSHA256() функция явно использует UTF-8. Ваша SHA256HashAsHex() функция должна использовать IndyTextEncoding_UTF8 для соответствия:
result := sha.HashStringAsHex(value, IndyTextEncoding_UTF8);
  • входная соль и выходное значение для CalculateHMACSHA256() должны быть двоичными байтами, а не строками и, конечно, не строки в кодировке base64 или в шестнадцатеричном формате. Ничто в документации Calculate Signature для AWS Signature Version 4 вообще не упоминает использование base64.
var
  DateKey, RegionKey, ServiceKey, SigningKey: TArray<Byte>;
...

// *** 3. Calculate the Signature for AWS Signature Version 4 ***
DateKey := CalculateHMACSHA256(YYYYMMDD, TEncoding.UTF8.GetBytes('AWS4' + SecretAccessKey));
RegionKey := CalculateHMACSHA256(region, DateKey);
ServiceKey := CalculateHMACSHA256(service, RegionKey);
SigningKey := CalculateHMACSHA256('aws4_request', ServiceKey);

Signature := CalculateHMACSHA256Hex(StringToSign, SigningKey); 

... 

function CalculateHMACSHA256(const value: string; const salt: TArray<Byte>): TArray<Byte>;
/// used for signingkey
var
  hmac: TIdHMACSHA256;
  hash: TIdBytes;
begin
  LoadOpenSSLLibrary;
  if not TIdHashSHA256.IsAvailable then
    raise Exception.Create('SHA256 hashing is not available!');
  hmac := TIdHMACSHA256.Create;
  try
    hmac.Key := TIdBytes(salt);
    hash := hmac.HashValue(IndyTextEncoding_UTF8.GetBytes(value));
    Result := TArray<Byte>(hash);
  finally
    hmac.Free;
  end;
end;

function CalculateHMACSHA256Hex(const value: string; const salt: TArray<Byte>): string;
var
  hash: TArray<Byte>;
begin
  hash := CalculateHMACSHA256(value, salt)
  Result := lowercase(ToHex(TIdBytes(hash)));
end;
...