Skip to content

Instantly share code, notes, and snippets.

@elmyn
Created October 3, 2018 10:17
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save elmyn/b63913d7ba4ffa26b37d55c7b7e260e1 to your computer and use it in GitHub Desktop.
Save elmyn/b63913d7ba4ffa26b37d55c7b7e260e1 to your computer and use it in GitHub Desktop.
Signing AWS GET S3 requests with signature version 4 in Swift 4, Xcode 10
// This is free and unencumbered software released into the public domain.
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
// For more information, please refer to <https://unlicense.org>
//
// AWSS3RequestSigner.swift
// SampleRESTApi
//
// Created by Michal Piwowarczyk on 01.10.2018.
//
import Foundation
import CommonCrypto
//based on:
//https://medium.com/@lewisjkl/signing-aws4-31dcff1bf1f0
//https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
//https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html
class AWSS3RequestSigner: NSObject {
private let hmacShaTypeString = "AWS4-HMAC-SHA256"
private let awsRegion = "eu-central-1"
private let serviceType = "s3"
private let aws4Request = "aws4_request"
private let iso8601Formatter: DateFormatter = {
let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601)
formatter.locale = Locale(identifier: "en_GB")
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyyMMdd'T'HHmmssXXXXX"
return formatter
}()
private func iso8601() -> (full: String, short: String) {
let date = iso8601Formatter.string(from: Date())
let index = date.index(date.startIndex, offsetBy: 8)
let shortDate = String(date[..<index])
return (full: date, short: shortDate)
}
func signGET(request: URLRequest, secretSigningKey: String, accessKeyId: String) -> URLRequest? {
var signedRequest = request
let date = iso8601()
guard let url = signedRequest.url, let host = url.host else { return nil }
signedRequest.addValue(host, forHTTPHeaderField: "Host")
signedRequest.addValue(date.full, forHTTPHeaderField: "X-Amz-Date")
signedRequest.addValue("".sha256(), forHTTPHeaderField: "x-amz-content-sha256")
signedRequest.addValue("", forHTTPHeaderField: "Range")
guard let headers = signedRequest.allHTTPHeaderFields, let method = signedRequest.httpMethod
else { return nil }
let signedHeaders = headers.map{ $0.key.lowercased() }.sorted().joined(separator: ";")
//If there is no payload in the request, x-amz-content-sha256 should be empty string hashed with sha256
//If its GET method there should be empty line under x-amz-date
let canonicalRequest = """
\(method)
\(url.path)
\(url.query ?? "")
host:\(host)
range:
x-amz-content-sha256:\("".sha256())
x-amz-date:\(date.full)
\(signedHeaders)
\("".sha256())
"""
let canonicalRequestHash = canonicalRequest.sha256()
let credential = getCredential(date: date.short, accessKeyId: accessKeyId)
let stringToSign = [hmacShaTypeString, date.full, credential, canonicalRequestHash].joined(separator: "\n")
guard let signature = signatureWith(stringToSign: stringToSign, secretAccessKey: secretSigningKey, shortDateString: date.short)
else { return nil }
let authorization = hmacShaTypeString + " Credential=" + accessKeyId + "/" + credential + ",SignedHeaders=" + signedHeaders + ",Signature=" + signature
signedRequest.addValue(authorization, forHTTPHeaderField: "Authorization")
return signedRequest
}
private func getCredential(date: String, accessKeyId: String) -> String {
let credential = [date, awsRegion, serviceType, aws4Request].joined(separator: "/")
return credential
}
/*
DateKey = HMAC-SHA256("AWS4"+"<SecretAccessKey>", "<YYYYMMDD>")
DateRegionKey = HMAC-SHA256(<DateKey>, "<aws-region>")
DateRegionServiceKey = HMAC-SHA256(<DateRegionKey>, "<aws-service>")
SigningKey = HMAC-SHA256(<DateRegionServiceKey>, "aws4_request")
*/
private func signatureWith(stringToSign: String, secretAccessKey: String, shortDateString: String) -> String? {
let firstKey = "AWS4" + secretAccessKey
let dateKey = shortDateString.hmac(keyString: firstKey)
let dateRegionKey = awsRegion.hmac(keyData: dateKey)
let dateRegionServiceKey = serviceType.hmac(keyData: dateRegionKey)
let signingKey = aws4Request.hmac(keyData: dateRegionServiceKey)
let signature = stringToSign.hmac(keyData: signingKey)
return signature.toHexString()
}
}
private extension String {
func sha256() -> String {
guard let data = self.data(using: .utf8) else { return "" }
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
data.withUnsafeBytes {
_ = CC_SHA256($0, CC_LONG(data.count), &hash)
}
let outputData = Data(bytes: hash)
return outputData.toHexString()
}
func hmac(keyString: String) -> Data {
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyString, keyString.count, self, self.count, &digest)
let data = Data(bytes: digest)
return data
}
func hmac(keyData: Data) -> Data {
let keyBytes = keyData.bytes()
let data = self.cString(using: String.Encoding.utf8)
let dataLen = Int(self.lengthOfBytes(using: String.Encoding.utf8))
var result = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
CCHmac(CCHmacAlgorithm(kCCHmacAlgSHA256), keyBytes, keyData.count, data, dataLen, &result);
return Data(bytes: result)
}
}
private extension Data {
func toHexString() -> String {
let hexString = self.map{ String(format:"%02x", $0) }.joined()
return hexString
}
func bytes() -> [UInt8] {
let array = [UInt8](self)
return array
}
}
@elmyn
Copy link
Author

elmyn commented Oct 3, 2018

usage:
` func makeRequest(urlString: String, accessKeyId: String, secretAccessKey: String) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)

    guard let URL = URL(string: urlString) else { return }
    var request = URLRequest(url: URL)
    request.httpMethod = "GET"
    
    request.httpBody = urlString.data(using: String.Encoding.utf8)
    
    guard let signedRequest = AWSS3RequestSigner().signGET(request: request, secretSigningKey: secretAccessKey, accessKeyId: accessKeyId) else { return }
    
    let task = session.dataTask(with: signedRequest, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
        if let httpResponse = response as? HTTPURLResponse {
            print("statusCode \(httpResponse.statusCode)")
        }
    })
    
    task.resume()
}`

@luannguyenkhoa
Copy link

luannguyenkhoa commented Dec 12, 2018

Hi elmyn,

Thanks for your awesome gist.
Following your guide, I can get signed request worked with IAM user access key and secret key, but running into a 403 issue when using temporary credentials including access, secret and session keys that I got from Cognito authentication.

I tried to add session: String? = nil parameter to signGet function, and then:

if let session = session {
      signedRequest.addValue(session, forHTTPHeaderField: "x-amz-security-token")
}

but the issue is still there.
Have you ever made effort with this before?
Any ideas to resolve this is very much appreciated.
Thanks.

@johndpope
Copy link

johndpope commented Jul 2, 2019

so cognito has user authentication separate to s3 buckets. you can't grant access to a s3 bucket per cognito user that I'm aware of.

UPDATE - @luannguyenkhoa
so what you're describing is - you want to hit your api - and get a pre-signed s3 url back using the cognito credentials....
this is the way - embedding the IAM credentials in app is subject to hacking as anyone could man in the middle attack - and grab keys using charles.

@Ansu016
Copy link

Ansu016 commented Apr 8, 2020

Hi elmyn,

Thanks for the code.

I am using this code to "createmultipartupload", My request is getting success in postman, Where as it not working in code.
Signature is same as postman.
Am getting 403 error.
Is there anything specific which i should use for "POST" request .
Any ideas to resolve this is very much appreciated.
Thanks.

@raya1995
Copy link

can i get this code with android

@Vinod-kumar-ios
Copy link

I have calling a GET request but getting 403.
For more detail check

@luannguyenkhoa
Copy link

luannguyenkhoa commented Apr 20, 2021

It has been a long time to revisit this thread.
I remember managed to make the sign GET request S3 worked with these simple lines of code.
Maybe some code get deprecated by the latest aws ios sdk version, just replace with the new recommendation.
Hope it helps someone stuck in this freaking issue 😄

public func signS3Request(url: URL) -> URLRequest {
    let credentials = AWSServiceManager.default()?.defaultServiceConfiguration.credentialsProvider
    let signature = AWSSignatureV4Signer(credentialsProvider: credentials!, endpoint: AWSEndpoint(region: .USEast1, service: .S3, url: url))
    let date = (NSDate.aws_clockSkewFixed()! as NSDate).aws_stringValue(AWSDateISO8601DateFormat2)!
    let req = NSMutableURLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: 30.0)
    req.httpMethod = "GET"
    req.allHTTPHeaderFields = ["Content-Type": "application/x-www-form-urlencoded", "X-Amz-Date": date]
    signature.interceptRequest(req)?.continueWith(block: { _ in return nil })
    return req as URLRequest
  }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment