Initial commit of code
This commit is contained in:
parent
cacaf4e437
commit
0747cc22e5
|
@ -0,0 +1,4 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"object": {
|
||||
"pins": [
|
||||
{
|
||||
"package": "async-http-client",
|
||||
"repositoryURL": "https://github.com/swift-server/async-http-client",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "64851a1a0a2a9e8fa7ae7b3508ce46a1da4a2e1d",
|
||||
"version": "1.0.0-alpha.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "INIParser",
|
||||
"repositoryURL": "https://github.com/swift-aws/Perfect-INIParser",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "42de0efc7a01105e19b80d533d3d282a98277f6c",
|
||||
"version": "3.0.3"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "32760eae40e6b7cb81d4d543bb0a9f548356d9a2",
|
||||
"version": "2.7.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"package": "swift-nio-ssl",
|
||||
"repositoryURL": "https://github.com/apple/swift-nio-ssl.git",
|
||||
"state": {
|
||||
"branch": null,
|
||||
"revision": "f5dd7a60ff56f501ff7bf9be753e4b1875bfaf20",
|
||||
"version": "2.4.0"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"version": 1
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
// swift-tools-version:5.0
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "AWSSigner",
|
||||
products: [
|
||||
.library(name: "AWSSigner", targets: ["AWSSigner"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/swift-server/async-http-client", .upToNextMajor(from: "1.0.0-alpha.1")),
|
||||
.package(url: "https://github.com/swift-aws/Perfect-INIParser", .upToNextMajor(from:"3.0.0"))
|
||||
],
|
||||
targets: [
|
||||
.target(name: "AWSSigner", dependencies: ["AsyncHTTPClient", "INIParser"]),
|
||||
.testTarget(name: "AWSSignerTests", dependencies: ["AWSSigner"]),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,93 @@
|
|||
//
|
||||
// credentials.swift
|
||||
// aws-sign
|
||||
//
|
||||
// Created by Adam Fowler on 2019/08/29.
|
||||
//
|
||||
import Foundation
|
||||
import INIParser
|
||||
|
||||
public protocol CredentialProvider {
|
||||
var accessKeyId: String {get}
|
||||
var secretAccessKey: String {get}
|
||||
var sessionToken: String? {get}
|
||||
}
|
||||
|
||||
public struct Credential : CredentialProvider {
|
||||
public let accessKeyId: String
|
||||
public let secretAccessKey: String
|
||||
public let sessionToken: String?
|
||||
|
||||
public init(accessKeyId: String, secretAccessKey: String, sessionToken: String? = nil) {
|
||||
self.accessKeyId = accessKeyId
|
||||
self.secretAccessKey = secretAccessKey
|
||||
self.sessionToken = sessionToken
|
||||
}
|
||||
}
|
||||
|
||||
public struct EnvironmentCredential: CredentialProvider {
|
||||
public let accessKeyId: String
|
||||
public let secretAccessKey: String
|
||||
public let sessionToken: String?
|
||||
|
||||
public init?() {
|
||||
guard let accessKeyId = ProcessInfo.processInfo.environment["AWS_ACCESS_KEY_ID"] else {
|
||||
return nil
|
||||
}
|
||||
guard let secretAccessKey = ProcessInfo.processInfo.environment["AWS_SECRET_ACCESS_KEY"] else {
|
||||
return nil
|
||||
}
|
||||
self.accessKeyId = accessKeyId
|
||||
self.secretAccessKey = secretAccessKey
|
||||
self.sessionToken = ProcessInfo.processInfo.environment["AWS_SESSION_TOKEN"]
|
||||
}
|
||||
}
|
||||
|
||||
/// Protocol for parsing AWS credential configs
|
||||
protocol SharedCredentialsConfigParser {
|
||||
/// Parse a specified file
|
||||
///
|
||||
/// - Parameter filename: The path to the file
|
||||
/// - Returns: A dictionary of dictionaries where the key is each profile
|
||||
/// and the value is the fields and values within that profile
|
||||
/// - Throws: If the file cannot be parsed
|
||||
func parse(filename: String) throws -> [String: [String:String]]
|
||||
}
|
||||
|
||||
/// An implementation of SharedCredentialsConfigParser that uses INIParser
|
||||
class IniConfigParser: SharedCredentialsConfigParser {
|
||||
func parse(filename: String) throws -> [String : [String : String]] {
|
||||
return try INIParser(filename).sections
|
||||
}
|
||||
}
|
||||
|
||||
public struct SharedCredential: CredentialProvider {
|
||||
|
||||
public let accessKeyId: String
|
||||
public let secretAccessKey: String
|
||||
public let sessionToken: String?
|
||||
public let expiration: Date? = nil
|
||||
|
||||
public init?(filename: String = "~/.aws/credentials",
|
||||
profile: String = "default") {
|
||||
self.init(
|
||||
filename: filename,
|
||||
profile: profile,
|
||||
parser: IniConfigParser()
|
||||
)
|
||||
}
|
||||
|
||||
init?(filename: String, profile: String, parser: SharedCredentialsConfigParser) {
|
||||
// Expand tilde before parsing the file
|
||||
let filename = NSString(string: filename).expandingTildeInPath
|
||||
guard let contents = try? parser.parse(filename: filename) else { return nil }
|
||||
guard let config = contents[profile] else { return nil }
|
||||
guard let accessKeyId = config["aws_access_key_id"] else { return nil }
|
||||
guard let secretAccessKey = config["aws_secret_access_key"] else { return nil }
|
||||
|
||||
self.accessKeyId = accessKeyId
|
||||
self.secretAccessKey = secretAccessKey
|
||||
self.sessionToken = config["aws_session_token"]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
//
|
||||
// hash.swift
|
||||
// AsyncHTTPClient
|
||||
//
|
||||
// Created by Adam Fowler on 29/08/2019.
|
||||
//
|
||||
|
||||
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)
|
||||
|
||||
import CommonCrypto
|
||||
|
||||
public func sha256(_ string: String) -> [UInt8] {
|
||||
let bytes = Array(string.utf8)
|
||||
return sha256(bytes)
|
||||
}
|
||||
|
||||
public func sha256(_ bytes: [UInt8]) -> [UInt8] {
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
CC_SHA256(bytes, CC_LONG(bytes.count), &hash)
|
||||
return hash
|
||||
}
|
||||
|
||||
public func sha256(_ buffer: UnsafeBufferPointer<UInt8>) -> [UInt8] {
|
||||
var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
CC_SHA256(buffer.baseAddress, CC_LONG(buffer.count), &hash)
|
||||
return hash
|
||||
}
|
||||
|
||||
public func hmac(string: String, key: [UInt8]) -> [UInt8] {
|
||||
var context = CCHmacContext()
|
||||
CCHmacInit(&context, CCHmacAlgorithm(kCCHmacAlgSHA256), key, key.count)
|
||||
|
||||
let bytes = Array(string.utf8)
|
||||
CCHmacUpdate(&context, bytes, bytes.count)
|
||||
var digest = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))
|
||||
CCHmacFinal(&context, &digest)
|
||||
|
||||
return digest
|
||||
}
|
||||
|
||||
#endif
|
|
@ -0,0 +1,190 @@
|
|||
//
|
||||
// signer.swift
|
||||
// AWSSigner
|
||||
//
|
||||
// Created by Adam Fowler on 2019/08/29.
|
||||
// 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 Foundation
|
||||
import NIO
|
||||
import AsyncHTTPClient
|
||||
|
||||
/// Amazon Web Services V4 Signer
|
||||
public class AWSSigner {
|
||||
static let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1)
|
||||
|
||||
let credentials: CredentialProvider
|
||||
let service: String
|
||||
let region: String
|
||||
|
||||
/// Initialise the Signer class with AWS credentials
|
||||
public init(credentials: CredentialProvider, service: String, region: String) {
|
||||
self.credentials = credentials
|
||||
self.service = service
|
||||
self.region = region
|
||||
}
|
||||
|
||||
/// sign a HTTP Request and place in "Authorization" header
|
||||
public func signInHeader(request: HTTPClient.Request) -> HTTPClient.Request {
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// construct signing data. Do this after adding the headers as it uses data from the headers
|
||||
let signingData = SigningData(request: request, signer: self)
|
||||
|
||||
// construct authorization string
|
||||
let authorization = "AWS4-HMAC-SHA256 " +
|
||||
"Credential=\(credentials.accessKeyId)/\(signingData.date)/\(region)/\(service)/aws4_request, " +
|
||||
"SignedHeaders=\(signingData.signedHeaders), " +
|
||||
"Signature=\(signature(signingData: signingData))"
|
||||
|
||||
// add Authorization header
|
||||
request.headers.add(name: "Authorization", value: authorization)
|
||||
return request
|
||||
}
|
||||
|
||||
/// 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")
|
||||
}
|
||||
|
||||
// Create signing data
|
||||
var signingData = SigningData(request: request, signer: self)
|
||||
|
||||
// Construct query string. Start with original query strings and append all the signing info.
|
||||
var query = request.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-Date=\(signingData.datetime)"
|
||||
query += "&X-Amz-Expires=\(expires)"
|
||||
query += "&X-Amz-SignedHeaders=\(signingData.signedHeaders)"
|
||||
if let sessionToken = credentials.sessionToken {
|
||||
query += "&X-Amz-Security-Token=\(sessionToken)"
|
||||
}
|
||||
// Split the string and sort to ensure the order of query strings is the same as AWS
|
||||
query = query.split(separator: "&")
|
||||
.sorted()
|
||||
.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
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 kSigning = hmac(string: "aws4_request", key: kService)
|
||||
let kSignature = hmac(string: stringToSign(signingData: signingData), key: kSigning)
|
||||
return AWSSigner.hexEncoded(kSignature)
|
||||
}
|
||||
|
||||
/// Create the string to sign as in https://docs.aws.amazon.com/general/latest/gr/sigv4-create-string-to-sign.html
|
||||
func stringToSign(signingData: SigningData) -> String {
|
||||
let stringToSign = "AWS4-HMAC-SHA256\n" +
|
||||
"\(signingData.datetime)\n" +
|
||||
"\(signingData.date)/\(region)/\(service)/aws4_request\n" +
|
||||
AWSSigner.hexEncoded(sha256(canonicalRequest(signingData: signingData)))
|
||||
return stringToSign
|
||||
}
|
||||
|
||||
/// Create the canonical request as in https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||
func canonicalRequest(signingData: SigningData) -> String {
|
||||
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" +
|
||||
"\(signingData.unsignedURL.path)\n" +
|
||||
"\(signingData.unsignedURL.query ?? "")\n" +
|
||||
"\(canonicalHeaders)\n\n" +
|
||||
"\(signingData.signedHeaders)\n" +
|
||||
signingData.hashedPayload
|
||||
return canonicalRequest
|
||||
}
|
||||
|
||||
/// Create a SHA256 hash of the Requests body
|
||||
static func hashedPayload(_ payload: HTTPClient.Body?) -> 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)
|
||||
}
|
||||
|
||||
/// return a hexEncoded string buffer from an array of bytes
|
||||
static func hexEncoded(_ buffer: [UInt8]) -> String {
|
||||
return buffer.map{String(format: "%02x", $0)}.joined(separator: "")
|
||||
}
|
||||
|
||||
/// return a timestamp formatted for signing requests
|
||||
static func timestamp(_ date: Date) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
|
||||
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
return formatter.string(from: date)
|
||||
}
|
||||
|
||||
static let queryAllowedCharacters = CharacterSet(charactersIn:"/;+").inverted
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
import XCTest
|
||||
import AsyncHTTPClient
|
||||
@testable import AWSSigner
|
||||
|
||||
final class aws_signTests: XCTestCase {
|
||||
let credentials : CredentialProvider = (SharedCredential() ?? EnvironmentCredential()) ?? Credential(accessKeyId: "", secretAccessKey: "")
|
||||
|
||||
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 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 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)
|
||||
}
|
||||
}
|
||||
static var allTests = [
|
||||
("testSignS3URL", testSignS3URL),
|
||||
("testSignSNSHeaders", testSignSNSHeaders),
|
||||
("testSignS3Headers", testSignS3Headers),
|
||||
]
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
import XCTest
|
||||
|
||||
#if !canImport(ObjectiveC)
|
||||
public func allTests() -> [XCTestCaseEntry] {
|
||||
return [
|
||||
testCase(aws_signTests.allTests),
|
||||
]
|
||||
}
|
||||
#endif
|
|
@ -0,0 +1,7 @@
|
|||
import XCTest
|
||||
|
||||
import aws_signTests
|
||||
|
||||
var tests = [XCTestCaseEntry]()
|
||||
tests += aws_signTests.allTests()
|
||||
XCTMain(tests)
|
Loading…
Reference in New Issue