iOS AWS Request 4


iOSにAWS Request 4リクエストを送るのは思ったほど簡単ではないので、整理することにしました.
PUT test$file.text HTTP/1.1
Host: examplebucket.s3.amazonaws.com
Date: Fri, 24 May 2013 00:00:00 GMT
Authorization: SignatureToBeCalculated
x-amz-date: 20130524T000000Z 
x-amz-storage-class: REDUCED_REDUNDANCY
x-amz-content-sha256: 44ce7dd67c959e0d3524ffac1771dfbba87d2b6b4b4e99e42034a8b803f8b072

<Payload>
多くのプロセスがありますが、最終的には上のようにヘッダーを作成します.

Date


まずDate形式から始め、DateのtimezoneをUTC時間に設定します.
Dateには、indexを使用して切り取られたx-amz-dateに必要なフォーマットとは異なるタイムスタンプフォーマットも必要です.
private var dateString:String{
        switch self {
        case .saveFile:
            let dateFormatter =  DateFormatter()
            dateFormatter.dateFormat = "yyyyMMdd'T'HHmmssXXXXX"
            dateFormatter.locale = Locale(identifier: "en_US_POSIX")
            dateFormatter.timeZone = TimeZone(secondsFromGMT: 0)
            return dateFormatter.string(from: Date())
        }
    }
private var timestampString:String{
	let index = dateString.index(dateString.startIndex, offsetBy: 7)
    return String(dateString[...index])
}

Payload


PUT演算にはハッシュPayloadが必要であり,それを頭の中に置く.
 private var hashedPayload: String{
        switch self {
        case .saveFile(let url):
            do{
                let payload = try Data(contentsOf:url)
                return SHA256.hash(data: payload).map{String(format: "%02hhx", $0)}.joined()
            }catch{
                fatalError("파일 hash 과정 중 오류 발생: " + error.localizedDescription)
            }
        }
    }

CanonicalRequest


次に、CanonicalRequestから文字列が生成されます.
let canonicalHeaders = """
content-type:application/zip\n\
host:\(host)\n\
x-amz-content-sha256:\(hashedPayload)\n\
x-amz-date:\(dateString)
"""
let signedHeaders = "content-type;host;x-amz-content-sha256;x-amz-date"
let canonicalRequest = "PUT\n/\(endPoint)\n\n\(canonicalHeaders)\n\n\(signedHeaders)\n\(hashedPayload)"
このときsignedheadersの順序はアルファベット順である.

StringToSign


stringToSignは、ハッシュCanonicalRequestによってフォーマットを調整します.
let credentialScope:String = timestampString + "/" + region + "/" + service + "/" + "aws4_request"    
let stringToSign:String = algorithm + "\n" + dateString + "\n" + credentialScope + "\n" 
                          + SHA256.hash(data: canonicalRequest.data(using: .utf8)!).map{String(format: "%02hhx", $0)}.joined()
let algorithm = "AWS4-HMAC-SHA256"
let region = "ap-northeast-2"
let service = "s3"

Signature


やっと署名を生成する準備ができました.SWIFTのCryptoKitを用いるとHMACが容易に実現できる.
private var signatureKey: SymmetricKey {
	guard let secretKey = Bundle.main.infoDictionary!["AWS_SECRET_KEY"] as? String else{
		fatalError("SECRET KEY 없음")
	}
	let kDate = HMAC<SHA256>.authenticationCode(for: timestampString.data(using: .utf8)!, using: SymmetricKey(data:("AWS4" + secretKey).data(using: .utf8)!))
	let kRegion = HMAC<SHA256>.authenticationCode(for: region.data(using: .utf8)!, using: SymmetricKey(data:kDate))
	let kService = HMAC<SHA256>.authenticationCode(for: service.data(using: .utf8)!, using: SymmetricKey(data:kRegion))
	return SymmetricKey(data:HMAC<SHA256>.authenticationCode(for: "aws4_request".data(using: .utf8)!, using: SymmetricKey(data:kService)))
 }
let signature = HMAC<SHA256>.authenticationCode(for: stringToSign.data(using: .utf8)!, using: signatureKey)
                .map{String(format: "%02hhx", $0)}.joined()

Authorization Header


最後に、承認ヘッダを作成し、リクエストを発行すれば成功します.できない場合は、response bodyを出力できない理由がわかります.
let awsSignature = """
\(algorithm) Credential=\(accessKey)/\(credentialScope), \
SignedHeaders=\(signedHeaders), Signature=\(signature)
"""
private var headers: HTTPHeaders{
	switch self {
    	case .saveFile:
        	return [
                "Content-Type":"application/zip",
                "Host":"\(host)",
                "X-Amz-Content-SHA256":hashedPayload,
                "X-Amz-Date":dateString,
                "Authorization":awsSignature
            ]
        }
  }
リファレンス
https://gist.github.com/elmyn/b63913d7ba4ffa26b37d55c7b7e260e1
https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
https://docs.aws.amazon.com/ko_kr/general/latest/gr/sigv4-signed-request-examples.html