发出S3 GET请求时收到400错误的请求

时间:2020-04-08 09:34:07

标签: amazon-s3 delphi-xe indy10

我正在尝试向S3发出AWS 4版本授权签名的GET请求,并收到以下错误请求错误400 Code:InvalidRequest Message:Missing required headerx-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的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;

2 个答案:

答案 0 :(得分:1)

我在您的代码中注意到了几件事:

  • 在创建YYYYMMDDamzDate值时,您两次调用Now(),这会创建一个竞争条件,该竞争条件具有潜在可能导致这些变量代表不同的日期。不太可能,但可能。为避免这种情况,您应该只调用Now()一次,并将结果保存到本地TDateTime变量中,然后在所有FormatDateTime()调用中都使用该变量。
dtNow := Now();
YYYYMMDD := FormatDateTime('yyyymmdd', dtNow);
amzDate := FormatDateTime('yyyymmdd"T"hhnnss"Z"', TTimeZone.Local.ToUniversalTime(dtNow), TFormatSettings.Create('en-US'));
  • 使用TIdHTTP的{​​{1}}属性设置自定义Request.CustomHeaders标头时,请确保还将Authorization属性也设置为False,否则将{{ 1}}可以使用其Request.BasicAuthenticationTIdHTTP属性创建自己的Authorization: Basic ...标头。您不需要Request.Username请求中的两个Request.Password标头。
Authorization
  • 您在授权计算中使用了GETIdhttp.Request.BasicAuthentication := False; 标头,但是没有将这些标头添加到实际的HTTP请求中。 x-amz-content-sha256将为您添加x-amz-date标头,但您需要自己添加其他标头。
TIdHTTP
  • 在调用Indy的Host方法时,您的Idhttp.Request.CustomHeaders.AddValue('x-amz-content-sha256', emptyHash); Idhttp.Request.CustomHeaders.AddValue('x-amz-date', amzDate); 函数未指定字节编码(实际上,它已将其编码明确设置为SHA256HashAsHex())。这样,将使用Indy的默认字节编码,即US-ASCII(除非您将TIdHashSHA256.HashStringAsHex()单元中的Indy的nil变量设置为其他值)。但是,您的GIdDefaultTextEncoding函数显式地使用UTF-8。您的IdGlobal函数应使用CalculateHMACSHA256()进行匹配:
SHA256HashAsHex()
  • IndyTextEncoding_UTF8的输入盐和输出值必须是二进制字节,而不是字符串,并且肯定不是base64编码或十六进制编码的字符串。 Calculate the Signature for AWS Signature Version 4文档中什么都没有提到使用base64。
result := sha.HashStringAsHex(value, IndyTextEncoding_UTF8);

答案 1 :(得分:0)

我刚刚遇到了类似的问题,我将在这里留下我的贡献,因为这篇文章帮助我找到了解决方案。 就我而言,我需要生成一个具有特定到期时间的签名网址。

    unit Data.Cloud.AmazonAPI.Utils;
    
    interface
    
      //See https://docs.aws.amazon.com/pt_br/AmazonS3/latest/API/sigv4-query-string-auth.html#query-string-auth-v4-signing-example
      //Use example:
      // MyUlrSigned := GetUrlPreSigned('mybucketname','/MyFolder/MyFile.zip','sa-east-1',3600);
    
      function GetUrlPreSigned(ABucket:string;AObjectName:string;ARegion:string;AExpiresIn:Int64=3600):string;
    
    implementation
    
    uses
      System.Classes, System.SysUtils, System.Hash, System.DateUtils;
    
    const
     
      AWS_ACCOUNTNAME     = '<AWSAccessKeyId>';
      AWS_ACCOUNTKEY      = '<AWSSecretAccessKey>';
    
    function SignString(const Signkey: TBytes; const StringToSign: string): TBytes;
    begin
      Result := THashSHA2.GetHMACAsBytes(StringToSign, Signkey);
    end;
    
    function BuildSignature(const StringToSign, DateISO, Region:string; AService: string; LSecretAccessKey:string): string;
    
      function GetSignatureKey(const datestamp, region, serviceName: string): TBytes;
      begin
        Result := SignString(TEncoding.Default.GetBytes('AWS4'+LSecretAccessKey) ,datestamp);
        Result := SignString(Result, region);
        Result := SignString(Result, serviceName);
        Result := SignString(Result, 'aws4_request');
      end;
    
    var
      Signature:string;
      SigningKey : TBytes;
    begin
      SigningKey := GetSignatureKey(DateISO, Region, AService);
      Result  := THash.DigestAsString(SignString(SigningKey, StringToSign));
    end;
    
    function GetHashSHA256Hex( HashString: string): string;
    var
      LBytes: TArray<Byte>;
    begin
      LBytes := THashSHA2.GetHashBytes(HashString);
      Result := THash.DigestAsString(LBytes);
    end;
    
    function GetUrlPreSigned(ABucket:string;AObjectName:string;ARegion:string;AExpiresIn:Int64=3600):string;
    var
      LNow : TDateTime;
      LData : string;
      LTimeStamp : string;
      LAccessKey : string;
      LSecretAccessKey : string;
      LService : string;
      LAws : string;
      LHost : string;
      LUrl : string;
      LQueryParams : string;
      LCanonicalRequest : string;
      LStringToSign : string;
      LSignature : string;
    begin
      LNow := Now();
      LData := FormatDateTime('yyyymmdd', LNow);
      LTimeStamp :=  FormatDateTime('yyyymmdd"T"hhnnss"Z"', TTimeZone.Local.ToUniversalTime(LNow), TFormatSettings.Create('en-US'));
      LAccessKey       := AWS_ACCOUNTNAME;
      LSecretAccessKey := AWS_ACCOUNTKEY;
    
      if AObjectName.StartsWith('/') then
        Delete(AObjectName,1,1);
    
      LService := 's3';
      LAws := 'amazonaws.com';
      LHost := Format('%s-%s.%s', [LService, ARegion, LAws]);
      LUrl := Format('%s://%s-%s.%s/%s/%s', ['https', LService, ARegion, LAws, ABucket, AObjectName]);
    
      LQueryParams := 'X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential='+LAccessKey
                      +'%2F'+LData
                      +'%2F'+ARegion
                      +'%2F'+LService+'%2F'
                      +'aws4_request'
                      +'&X-Amz-Date='+LTimeStamp
                      +'&X-Amz-Expires='+AExpiresIn.ToString
                      +'&X-Amz-SignedHeaders=host';
    
      //1 - CanonicalRequest
      LCanonicalRequest := 'GET' +#10+
                        '/'+ABucket+'/'+AObjectName +#10
                        +LQueryParams +#10
                        +'host:'+LHost+#10
                        +#10
                        +'host'+#10
                        +'UNSIGNED-PAYLOAD';
    
      //2 - StringToSign
      LStringToSign := 'AWS4-HMAC-SHA256' +#10
                      + LTimeStamp +#10
                      + UTF8String(LData)+'/'+ UTF8String(ARegion) +'/'+ UTF8String(LService)+UTF8String('/aws4_request') +#10
                      + lowercase(GetHashSHA256Hex(LCanonicalRequest));
      //3 - Signature
      LSignature := BuildSignature(LStringToSign,LData,ARegion,LService,LSecretAccessKey);
    
      //4 - Signed URL
      Result := LUrl+'?'+LQueryParams+'&X-Amz-Signature='+LSignature;
    end;
    
    end.