0

I really try hard to solve this problem on my project. But I can't.

I try to sign URLRequest headers and body by Amazon AWS Signature Version 4. And send it to server with Alamofire SDK.

But only headers without body work correctly.

I dunno why I get the response "403 forbidden" from server when I put httpBody into URLRequest.

Here is my source.

...
var request = URLRequest(url: convUrl)
if let jsonString = String(data: jsonData, encoding: .utf8) {
   AWS4Signer().aws4Sign(request: &request, method: httpMethodType, payload: jsonString, query: [:], path: url.replacingOccurrences(of: self.url.host, with: ""))
}


AF.request(request).responseData { response in

}

func aws4Sign(request: inout URLRequest, method: HttpMethodType, payload: String?, query: [String:String]?, path: String) {
        var headers = [String:String]()
        
        let requested_date_time = self.getAmzDate(date: Date())
        headers["x-amz-date"] = requested_date_time
        
        // x-amz-content-sha256
        var payload_hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" // empty
        if let payload {
            let utf8Data = payload.data(using: .utf8) ?? Data()
            payload_hash = SHA256.hash(data: utf8Data).hexDigest()
            if !payload.isEmpty {
                headers["x-amz-content-sha256"] = payload_hash
                request.httpBody = utf8Data
            }
        }
        
        let canonical_querystring = ""

        headers["host"] = "hosthost.com"
        
        let sortedHeaders = headers.filter { ["host", "x-amz-date", "x-amz-content-sha256"].contains($0.key) }
            .sorted { $0.key < $1.key }
        let canonical_headers = sortedHeaders.map { "\($0.key.lowercased()):\($0.value)" }.joined(separator: "\n") + "\n"
        
        let signed_headers = sortedHeaders.map { $0.key.lowercased() }.joined(separator: ";")
        
        let canonical_uri = path
        
        let canonical_request = "\(method.rawValue)\n\(canonical_uri)\n\(canonical_querystring)\n\(canonical_headers)\n\(signed_headers)\n\(payload_hash)"
        
        let hashed_canonical_request = SHA256.hash(data: canonical_request.data(using: .utf8)!).hexDigest()
        
        let algorithm = "AWS4-HMAC-SHA256"
        let nowDate = Date().getString(format: "YYYYMMdd", timeZone: TimeZone(identifier: "UTC")!)
        let credential_scope = "\(nowDate)/\(Const.key.awsRegion)/\(Const.key.awsServiceType)/aws4_request"
        let string_to_sign = "\(algorithm)\n\(requested_date_time)\n\(credential_scope)\n\(hashed_canonical_request)"

        let signing_key = self.getSignatureKey(key: Const.key.awsSecretAccessKey, dateStamp: nowDate, regionName: Const.key.awsRegion, serviceName: Const.key.awsServiceType)

        let signature = self.sign(key: signing_key, msg: string_to_sign).hexDigest()

        
        let authorization_header = "\(algorithm) Credential=\(Const.key.awsAccessKeyID)/\(credential_scope), SignedHeaders=\(signed_headers), Signature=\(signature)"
        
        request.addValue(requested_date_time, forHTTPHeaderField: "X-amz-date")
        request.addValue(authorization_header, forHTTPHeaderField: "Authorization")
    }


func getSignatureKey(key: String, dateStamp: String, regionName: String, serviceName: String) -> Data {
        let keyData = Data("AWS4\(key)".utf8)
        let dateStampData = Data(dateStamp.utf8)
        let regionNameData = Data(regionName.utf8)
        let serviceNameData = Data(serviceName.utf8)
        let signingData = Data("aws4_request".utf8)

        var symmetricKey = SymmetricKey(data: keyData)
        let dateSHA256 = HMAC<SHA256>.authenticationCode(for: dateStampData, using: symmetricKey)

        symmetricKey = SymmetricKey(data: Data(dateSHA256))
        let regionSHA256 = HMAC<SHA256>.authenticationCode(for: regionNameData, using: symmetricKey)

        symmetricKey = SymmetricKey(data: Data(regionSHA256))
        let serviceNameSHA256 = HMAC<SHA256>.authenticationCode(for: serviceNameData, using: symmetricKey)

        symmetricKey = SymmetricKey(data: Data(serviceNameSHA256))
        let signingSHA256 = HMAC<SHA256>.authenticationCode(for: signingData, using: symmetricKey)

        let skeyString = keyData.map { String(format: "%02hhx", $0) }.joined()

        let kDateString = Data(dateSHA256).map { String(format: "%02hhx", $0) }.joined()

        let kRegionString = Data(regionSHA256).map { String(format: "%02hhx", $0) }.joined()

        let kServiceString = Data(serviceNameSHA256).map { String(format: "%02hhx", $0) }.joined()

        let kSigningString = Data(signingSHA256).map { String(format: "%02hhx", $0) }.joined()

        return Data(signingSHA256)
    }

func sign(key: Data, msg: String) -> Data {
        let hmac = HMAC<SHA256>.authenticationCode(for: msg.data(using: .utf8)!, using: SymmetricKey(data: key))
        return Data(hmac)
    }

func getAmzDate(date: Date) -> String {
        let formatter = DateFormatter()
        formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
        formatter.timeZone = TimeZone(identifier: "UTC")
        return formatter.string(from: date)
    }
    
    func getCanonicalQuerystring(query: [String: String]) -> String {
        let sortedParameters = query.sorted { $0.0 < $1.0 }
        let encodedQueryParameters = sortedParameters.map { (key, value) in
            return "\(key.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")=\(value.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? "")"
        }
        let canonicalQuerystring = encodedQueryParameters.joined(separator: "&")
        return canonicalQuerystring
    }

The "Authorization" data has it.

"AWS4-HMAC-SHA256 Credential=ACCESS_KEY_ID/20230213/ap-northeast-2/execute-api/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=cfa667aa18472d3d5a419f67aa2c321977428abac391a33f9f3e31839bcb4665"

ACCESS_KEY_ID is my access_key_id of AWS.

So far, only the POST method is tested.

David
  • 35
  • 4

0 Answers0