Stopped trying to edit HTTPClient.Request

Instead construct signed request from url, method, headers and body
Added helper functions to HTTPClient.Request extension
This commit is contained in:
Adam Fowler 2019-08-30 11:09:56 +01:00
parent 0747cc22e5
commit 70277f6976
5 changed files with 173 additions and 180 deletions

View File

@ -2,17 +2,19 @@
// credentials.swift
// aws-sign
//
// Created by Adam Fowler on 2019/08/29.
// Created by Adam Fowler on 29/08/2019.
//
import Foundation
import INIParser
/// Protocol for providing credential details for accessing AWS services
public protocol CredentialProvider {
var accessKeyId: String {get}
var secretAccessKey: String {get}
var sessionToken: String? {get}
}
/// basic version of CredentialProvider where you supply the credentials
public struct Credential : CredentialProvider {
public let accessKeyId: String
public let secretAccessKey: String
@ -25,6 +27,7 @@ public struct Credential : CredentialProvider {
}
}
/// environment variable version of credential provider that uses system environment variables to get credential details
public struct EnvironmentCredential: CredentialProvider {
public let accessKeyId: String
public let secretAccessKey: String

View File

@ -2,47 +2,13 @@
// hash.swift
// AsyncHTTPClient
//
// Created by Adam Fowler on 29/08/2019.
// Created by Adam Fowler on 2019/08/29.
//
import Foundation
//
// Hash.swift
// AWSSDKSwift
//
// Created by Yuki Takei on 2017/03/13.
//
//
import Foundation
#if canImport(CAWSSignOpenSSL)
import CAWSSignOpenSSL
public func sha256(_ string: String) -> [UInt8] {
var bytes = Array(string.utf8)
return sha256(&bytes)
}
public func sha256(_ bytes: inout [UInt8]) -> [UInt8] {
var hash = [UInt8](repeating: 0, count: Int(SHA256_DIGEST_LENGTH))
SHA256(&bytes, bytes.count, &hash)
return hash
}
public func sha256(_ data: Data) -> [UInt8] {
return data.withUnsafeBytes { ptr in
var hash = [UInt8](repeating: 0, count: Int(SHA256_DIGEST_LENGTH))
if let bytes = ptr.baseAddress?.assumingMemoryBound(to: UInt8.self) {
SHA256(bytes, data.count, &hash)
}
return hash
}
}
#elseif canImport(CommonCrypto)
// Currently only works if CommonCrypto exists. Will look into doing something for Linux later
#if canImport(CommonCrypto)
import CommonCrypto

View File

@ -0,0 +1,26 @@
//
// request.swift
// AWSSigner
//
// Created by Adam Fowler on 2019/08/30.
//
import AsyncHTTPClient
import Foundation
import NIO
import NIOHTTP1
public extension HTTPClient.Request {
/// return signed HTTPClient request with signature in the headers
static func awsHeaderSignedRequest(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: AWSSigner.BodyData? = nil, date: Date = Date(), signer: AWSSigner) throws -> HTTPClient.Request {
let signedHeaders = signer.signHeaders(url: url, method: method, headers: headers, body: body, date: date)
return try HTTPClient.Request(url: url, method: method, headers: signedHeaders, body: body?.body)
}
/// return signed HTTPClient request with signature in the URL
static func awsURLSignedRequest(url: URL, method: HTTPMethod = .GET, body: AWSSigner.BodyData? = nil, date: Date = Date(), expires: Int = 86400, signer: AWSSigner) throws -> HTTPClient.Request {
let signedURL = signer.signURL(url: url, method: method, body: body, date: date, expires: expires)
return try HTTPClient.Request(url: signedURL, method: method, body: body?.body)
}
}

View File

@ -6,106 +6,85 @@
// Amazon Web Services V4 Signer
// AWS documentation about signing requests is here https://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html
//
import AsyncHTTPClient
import Foundation
import NIO
import AsyncHTTPClient
import NIOHTTP1
/// Amazon Web Services V4 Signer
public class AWSSigner {
static let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
/// security credentials for accessing AWS services
let credentials: CredentialProvider
let service: String
/// service signing name. In general this is the same as the service name
let name: String
/// AWS region you are working in
let region: String
/// Initialise the Signer class with AWS credentials
public init(credentials: CredentialProvider, service: String, region: String) {
public init(credentials: CredentialProvider, name: String, region: String) {
self.credentials = credentials
self.service = service
self.name = name
self.region = region
}
/// sign a HTTP Request and place in "Authorization" header
public func signInHeader(request: HTTPClient.Request) -> HTTPClient.Request {
/// Enum for holding your body data
public enum BodyData {
case string(String)
case data(Data)
case byteBuffer(ByteBuffer)
var request = request
// add date, host, sha256 and if available security token headers
request.headers.add(name: "X-Amz-Date", value: AWSSigner.timestamp(Date()))
request.headers.add(name: "host", value: request.url.host ?? "")
request.headers.add(name: "x-amz-content-sha256", value: AWSSigner.hashedPayload(request.body))
if let sessionToken = credentials.sessionToken {
request.headers.add(name: "x-amz-security-token", value: sessionToken)
var body : HTTPClient.Body {
switch self {
case .string(let string):
return .string(string)
case .data(let data):
return .data(data)
case .byteBuffer(let byteBuffer):
return .byteBuffer(byteBuffer)
}
}
}
/// Generate signed headers, for a HTTP request
public func signHeaders(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: BodyData? = nil, date: Date = Date()) -> HTTPHeaders {
var headers = headers
// add date, host, sha256 and if available security token headers
headers.add(name: "X-Amz-Date", value: AWSSigner.timestamp(date))
headers.add(name: "host", value: url.host ?? "")
headers.add(name: "x-amz-content-sha256", value: AWSSigner.hashedPayload(body))
if let sessionToken = credentials.sessionToken {
headers.add(name: "x-amz-security-token", value: sessionToken)
}
// construct signing data. Do this after adding the headers as it uses data from the headers
let signingData = SigningData(request: request, signer: self)
let signingData = AWSSigner.SigningData(url: url, method: method, headers: headers, body: body, date: date, signer: self)
// construct authorization string
let authorization = "AWS4-HMAC-SHA256 " +
"Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(service)/aws4_request, " +
"Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(name)/aws4_request, " +
"SignedHeaders=\(signingData.signedHeaders), " +
"Signature=\(signature(signingData: signingData))"
// add Authorization header
request.headers.add(name: "Authorization", value: authorization)
return request
headers.add(name: "Authorization", value: authorization)
return headers
}
/// structure used to store data used throughout the signing process
struct SigningData {
let request : HTTPClient.Request
let hashedPayload : String
let datetime : String
let headersToSign: [String: String]
let signedHeaders : String
var unsignedURL : URL
var date : String { return String(datetime.prefix(8))}
init(request: HTTPClient.Request, signer: AWSSigner) {
self.request = request
self.datetime = request.headers["x-amz-date"].first ?? AWSSigner.timestamp(Date())
self.hashedPayload = request.headers["x-amz-content-sha256"].first ?? AWSSigner.hashedPayload(request.body)
self.unsignedURL = request.url
let headersNotToSign: Set<String> = [
"Authorization"
]
var headersToSign : [String: String] = [:]
for header in request.headers {
if headersNotToSign.contains(header.name) {
continue
}
headersToSign[header.name] = header.value
}
self.headersToSign = headersToSign
self.signedHeaders = headersToSign.map { return "\($0.key.lowercased())" }
.sorted()
.joined(separator: ";")
}
}
/// return a request with a signed URL
public func signURL(request: HTTPClient.Request, expires: Int = 86400) throws -> HTTPClient.Request {
var request = request
// add date, host headers. If the service is s3 then add a sha256 of "UNSIGNED-PAYLOAD"
request.headers.add(name: "X-Amz-Date", value: AWSSigner.timestamp(Date()))
request.headers.add(name: "host", value: request.url.host ?? "")
if service == "s3" {
request.headers.add(name: "x-amz-content-sha256", value:"UNSIGNED-PAYLOAD")
}
/// Generate a signed URL, for a HTTP request
public func signURL(url: URL, method: HTTPMethod = .GET, body: BodyData? = nil, date: Date = Date(), expires: Int = 86400) -> URL {
let headers = HTTPHeaders([("host", url.host ?? "")])
// Create signing data
var signingData = SigningData(request: request, signer: self)
var signingData = AWSSigner.SigningData(url: url, method: method, headers: headers, body: body, date: date, signer: self)
// Construct query string. Start with original query strings and append all the signing info.
var query = request.url.query ?? ""
var query = url.query ?? ""
if query.count > 0 {
query += "&"
}
query += "X-Amz-Algorithm=AWS4-HMAC-SHA256"
query += "&X-Amz-Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(service)/aws4_request"
query += "&X-Amz-Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(name)/aws4_request"
query += "&X-Amz-Date=\(signingData.datetime)"
query += "&X-Amz-Expires=\(expires)"
query += "&X-Amz-SignedHeaders=\(signingData.signedHeaders)"
@ -118,20 +97,63 @@ public class AWSSigner {
.joined(separator: "&")
.addingPercentEncoding(withAllowedCharacters: AWSSigner.queryAllowedCharacters)!
// update unsignURL in the signingData so when the canonical request is constructed it includes all the signing query items
signingData.unsignedURL = URL(string: request.url.absoluteString.split(separator: "?")[0]+"?"+query)! // NEED TO DEAL WITH SITUATION WHERE THIS FAILS
// update unsignedURL in the signingData so when the canonical request is constructed it includes all the signing query items
signingData.unsignedURL = URL(string: url.absoluteString.split(separator: "?")[0]+"?"+query)! // NEED TO DEAL WITH SITUATION WHERE THIS FAILS
query += "&X-Amz-Signature=\(signature(signingData: signingData))"
// Add signature to query items and build a new Request
let signedURL = URL(string: request.url.absoluteString.split(separator: "?")[0]+"?"+query)!
return try HTTPClient.Request(url: signedURL, method: request.method, headers: request.headers, body: request.body)
let signedURL = URL(string: url.absoluteString.split(separator: "?")[0]+"?"+query)!
return signedURL
}
/// structure used to store data used throughout the signing process
struct SigningData {
let url : URL
let method : HTTPMethod
let hashedPayload : String
let datetime : String
let headersToSign: [String: String]
let signedHeaders : String
var unsignedURL : URL
var date : String { return String(datetime.prefix(8))}
init(url: URL, method: HTTPMethod = .GET, headers: HTTPHeaders = HTTPHeaders(), body: BodyData? = nil, date: Date = Date(), signer: AWSSigner) {
self.url = url
self.method = method
self.datetime = headers["x-amz-date"].first ?? AWSSigner.timestamp(date)
self.unsignedURL = self.url
if let hash = headers["x-amz-content-sha256"].first {
self.hashedPayload = hash
} else if signer.name == "s3" {
self.hashedPayload = "UNSIGNED-PAYLOAD"
} else {
self.hashedPayload = AWSSigner.hashedPayload(body)
}
let headersNotToSign: Set<String> = [
"Authorization"
]
var headersToSign : [String: String] = [:]
for header in headers {
if headersNotToSign.contains(header.name) {
continue
}
headersToSign[header.name] = header.value
}
self.headersToSign = headersToSign
self.signedHeaders = headersToSign.map { return "\($0.key.lowercased())" }
.sorted()
.joined(separator: ";")
}
}
// Calculating signature as in https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
func signature(signingData: SigningData) -> String {
let kDate = hmac(string:signingData.date, key:Array("AWS4\(credentials.secretAccessKey)".utf8))
let kRegion = hmac(string: region, key: kDate)
let kService = hmac(string: service, key: kRegion)
let kService = hmac(string: name, key: kRegion)
let kSigning = hmac(string: "aws4_request", key: kService)
let kSignature = hmac(string: stringToSign(signingData: signingData), key: kSigning)
return AWSSigner.hexEncoded(kSignature)
@ -141,7 +163,7 @@ public class AWSSigner {
func stringToSign(signingData: SigningData) -> String {
let stringToSign = "AWS4-HMAC-SHA256\n" +
"\(signingData.datetime)\n" +
"\(signingData.date)/\(region)/\(service)/aws4_request\n" +
"\(signingData.date)/\(region)/\(name)/aws4_request\n" +
AWSSigner.hexEncoded(sha256(canonicalRequest(signingData: signingData)))
return stringToSign
}
@ -151,7 +173,7 @@ public class AWSSigner {
let canonicalHeaders = signingData.headersToSign.map { return "\($0.key.lowercased()):\($0.value)" }
.sorted()
.joined(separator: "\n") // REMEMBER TO TRIM THE VALUE
let canonicalRequest = "\(signingData.request.method.rawValue)\n" +
let canonicalRequest = "\(signingData.method.rawValue)\n" +
"\(signingData.unsignedURL.path)\n" +
"\(signingData.unsignedURL.query ?? "")\n" +
"\(canonicalHeaders)\n\n" +
@ -161,15 +183,27 @@ public class AWSSigner {
}
/// Create a SHA256 hash of the Requests body
static func hashedPayload(_ payload: HTTPClient.Body?) -> String {
static func hashedPayload(_ payload: BodyData?) -> String {
guard let payload = payload else { return AWSSigner.hexEncoded(sha256([UInt8]())) }
var hash : [UInt8] = []
_ = payload.stream(HTTPClient.Body.StreamWriter { (data) -> EventLoopFuture<Void> in
guard case .byteBuffer(let buffer) = data else {return AWSSigner.eventLoopGroup.next().makeSucceededFuture(Void())}
hash = sha256(Array(buffer.readableBytesView))
return AWSSigner.eventLoopGroup.next().makeSucceededFuture(Void())
})
return AWSSigner.hexEncoded(hash)
let hash : [UInt8]?
switch payload {
case .string(let string):
hash = sha256(string)
case .data(let data):
hash = data.withContiguousStorageIfAvailable { bytes in
return sha256(bytes)
}
case .byteBuffer(let byteBuffer):
let byteBufferView = byteBuffer.readableBytesView
hash = byteBufferView.withContiguousStorageIfAvailable { bytes in
return sha256(bytes)
}
}
if let hash = hash {
return AWSSigner.hexEncoded(hash)
} else {
return AWSSigner.hexEncoded(sha256([UInt8]()))
}
}
/// return a hexEncoded string buffer from an array of bytes
@ -186,5 +220,5 @@ public class AWSSigner {
return formatter.string(from: date)
}
static let queryAllowedCharacters = CharacterSet(charactersIn:"/;+").inverted
static let queryAllowedCharacters = CharacterSet(charactersIn:"/;").inverted
}

View File

@ -3,69 +3,33 @@ import AsyncHTTPClient
@testable import AWSSigner
final class aws_signTests: XCTestCase {
let credentials : CredentialProvider = (SharedCredential() ?? EnvironmentCredential()) ?? Credential(accessKeyId: "", secretAccessKey: "")
let credentials : CredentialProvider = Credential(accessKeyId: "MYACCESSKEY", secretAccessKey: "MYSECRETACCESSKEY")
func testSignS3Headers() {
do {
let client = HTTPClient(eventLoopGroupProvider: .createNew)
let request = try HTTPClient.Request(url: "https://s3.us-east-1.amazonaws.com/test-bucket", method: .PUT, headers: [:])
let signedRequest = try AWSSigner(credentials: credentials, service:"s3", region:"us-east-1").signURL(request: request)
let response = try client.execute(request: signedRequest).wait()
print("Status code \(response.status.code)")
XCTAssertTrue(200..<300 ~= response.status.code || response.status.code == 409)
if let body = response.body {
let bodyString = body.getString(at: 0, length: body.readableBytes)
print(bodyString ?? "")
}
try client.syncShutdown()
} catch {
XCTFail(error.localizedDescription)
}
func testSignGetHeaders() {
let signer = AWSSigner(credentials: credentials, name: "glacier", region:"us-east-1")
let headers = signer.signHeaders(url: URL(string:"https://glacier.us-east-1.amazonaws.com/-/vaults")!, method: .GET, headers: ["x-amz-glacier-version":"2012-06-01"], date: Date(timeIntervalSinceReferenceDate: 2000000))
XCTAssertEqual(headers["Authorization"].first, "AWS4-HMAC-SHA256 Credential=MYACCESSKEY/20010124/us-east-1/glacier/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-glacier-version, Signature=acfa9b03fca6b098d7b88bfd9bbdb4687f5b34e944a9c6ed9f4814c1b0b06d62")
print(headers["Authorization"])
}
func testSignSNSHeaders() {
do {
let client = HTTPClient(eventLoopGroupProvider: .createNew)
let request = try HTTPClient.Request(url: "https://sns.eu-west-1.amazonaws.com/", method: .POST, headers: ["Content-Type": "application/x-www-form-urlencoded; charset=utf-8"], body: .string("Action=ListTopics&Version=2010-03-31"))
let signedRequest = AWSSigner(credentials: credentials, service:"sns", region:"eu-west-1").signInHeader(request: request)
let response = try client.execute(request: signedRequest).wait()
print("Status code \(response.status.code)")
XCTAssertTrue(200..<300 ~= response.status.code)
if let body = response.body {
let bodyString = body.getString(at: 0, length: body.readableBytes)
print(bodyString ?? "")
}
try client.syncShutdown()
} catch {
XCTFail(error.localizedDescription)
}
func testSignPutHeaders() {
let signer = AWSSigner(credentials: credentials, name: "sns", region:"eu-west-1")
let headers = signer.signHeaders(url: URL(string: "https://sns.eu-west-1.amazonaws.com/")!, method: .POST, headers: ["Content-Type": "application/x-www-form-urlencoded; charset=utf-8"], body: .string("Action=ListTopics&Version=2010-03-31"), date: Date(timeIntervalSinceReferenceDate: 200))
XCTAssertEqual(headers["Authorization"].first, "AWS4-HMAC-SHA256 Credential=MYACCESSKEY/20010101/eu-west-1/sns/aws4_request, SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date, Signature=1d29943055a8ad094239e8de06082100f2426ebbb2c6a5bbcbb04c63e6a3f274")
}
func testSignS3URL() {
do {
let client = HTTPClient(eventLoopGroupProvider: .createNew)
let request = try HTTPClient.Request(url: "https://s3.eu-west-1.amazonaws.com/", method: .GET, headers: [:])
let signedRequest = try AWSSigner(credentials: credentials, service:"s3", region:"eu-west-1").signURL(request: request)
let response = try client.execute(request: signedRequest).wait()
print("Status code \(response.status.code)")
XCTAssertTrue(200..<300 ~= response.status.code)
if let body = response.body {
let bodyString = body.getString(at: 0, length: body.readableBytes)
print(bodyString ?? "")
} else {
XCTFail("Empty body")
}
try client.syncShutdown()
} catch {
XCTFail(error.localizedDescription)
}
func testSignS3GetURL() {
let signer = AWSSigner(credentials: credentials, name: "s3", region:"us-east-1")
let url = signer.signURL(url: URL(string: "https://s3.us-east-1.amazonaws.com/")!, method: .GET, date:Date(timeIntervalSinceReferenceDate: 100000))
XCTAssertEqual(url.absoluteString, "https://s3.us-east-1.amazonaws.com/?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MYACCESSKEY%2F20010102%2Fus-east-1%2Fs3%2Faws4_request&X-Amz-Date=20010102T034640Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=27957103c8bfdff3560372b1d85976ed29c944f34295eca2d4fdac7fc02c375a")
}
func testSignS3PutURL() {
let signer = AWSSigner(credentials: credentials, name: "s3", region:"eu-west-1")
let url = signer.signURL(url: URL(string: "https://test-bucket.s3.amazonaws.com/test-put.txt")!, method: .PUT, body: .string("Testing signed URLs"), date:Date(timeIntervalSinceReferenceDate: 100000))
XCTAssertEqual(url.absoluteString, "https://test-bucket.s3.amazonaws.com/test-put.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=MYACCESSKEY%2F20010102%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Date=20010102T034640Z&X-Amz-Expires=86400&X-Amz-SignedHeaders=host&X-Amz-Signature=13d665549a6ea5eb6a1615ede83440eaed3e0ee25c964e62d188c896d916d96f")
}
static var allTests = [
("testSignS3URL", testSignS3URL),
("testSignSNSHeaders", testSignSNSHeaders),
("testSignS3Headers", testSignS3Headers),
("testSignS3GetURL", testSignS3GetURL),
]
}