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



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

我使用的是带有Indy的TIdHTTP组件的DelphiXE5。有人能告诉我我做错了什么吗?我在下面包含了我的代码。

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;

我在您的代码中注意到了一些事情:

  • 在创建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'));
  • 使用TIdHTTPRequest.CustomHeaders属性设置自定义Authorization标头时,请确保还将Request.BasicAuthentication属性设置为False,否则TIdHTTP可能会使用其Request.UsernameRequest.Password属性创建自己的Authorization: Basic ...标头。您不希望在GET请求中包含两个Authorization标头
Idhttp.Request.BasicAuthentication := False;
  • 您在授权计算中使用了x-amz-content-sha256x-amz-date标头,但没有将这些标头添加到实际的HTTP请求中。TIdHTTP将为您添加Host标头,但您需要自己添加其他标头
Idhttp.Request.CustomHeaders.AddValue('x-amz-content-sha256', emptyHash);
Idhttp.Request.CustomHeaders.AddValue('x-amz-date', amzDate);
  • 您的SHA256HashAsHex()函数在调用Indy的TIdHashSHA256.HashStringAsHex()方法时没有指定字节编码(事实上,它会特意将编码显式设置为nil(。因此,将使用Indy的默认字节编码,即US-ASCII(除非将IdGlobal单元中Indy的GIdDefaultTextEncoding变量设置为其他值(。但是,CalculateHMACSHA256()函数显式地使用UTF-8。您的SHA256HashAsHex()函数应该使用IndyTextEncoding_UTF8来匹配:
result := sha.HashStringAsHex(value, IndyTextEncoding_UTF8);
  • CalculateHMACSHA256()的输入salt和输出值需要是二进制字节,而不是字符串,当然也不是base64编码或十六进制编码的字符串。AWS Signature Version 4的Calculate the Signature文档中没有提到使用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;

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

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.

相关内容

最新更新