Initial commit of code

This commit is contained in:
Adam Fowler 2019-08-30 08:01:35 +01:00
parent cacaf4e437
commit 0747cc22e5
9 changed files with 514 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
.DS_Store
/.build
/Packages
/*.xcodeproj

43
Package.resolved Normal file
View File

@ -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
}

19
Package.swift Normal file
View File

@ -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"]),
]
)

View File

@ -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"]
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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),
]
}

View File

@ -0,0 +1,9 @@
import XCTest
#if !canImport(ObjectiveC)
public func allTests() -> [XCTestCaseEntry] {
return [
testCase(aws_signTests.allTests),
]
}
#endif

7
Tests/LinuxMain.swift Normal file
View File

@ -0,0 +1,7 @@
import XCTest
import aws_signTests
var tests = [XCTestCaseEntry]()
tests += aws_signTests.allTests()
XCTMain(tests)