#if canImport(CryptoKit)import CryptoKit#elseimport Crypto#endifimport Foundation#if canImport(FoundationNetworking)import FoundationNetworking#endif enum BedrockAWSSigner { struct Credentials { let accessKeyID: String let secretAccessKey: String let sessionToken: String? } static func sign( request: inout URLRequest, credentials: Credentials, region: String, service: String, date: Date = Date()) { let dateFormatter = Self.dateFormatter() let dateStamp = Self.dateStamp(date: date) let amzDate = dateFormatter.string(from: date) request.setValue(amzDate, forHTTPHeaderField: "X-Amz-Date") if let sessionToken = credentials.sessionToken { request.setValue(sessionToken, forHTTPHeaderField: "X-Amz-Security-Token") } let host = request.url?.host ?? "" request.setValue(host, forHTTPHeaderField: "Host") let bodyHash = Self.sha256Hex(request.httpBody ?? Data()) request.setValue(bodyHash, forHTTPHeaderField: "x-amz-content-sha256") let signedHeaders = Self.signedHeaders(request: request) let canonicalRequest = Self.canonicalRequest( request: request, signedHeaders: signedHeaders, bodyHash: bodyHash) let credentialScope = "\(dateStamp)/\(region)/\(service)/aws4_request" let stringToSign = [ "AWS4-HMAC-SHA256", amzDate, credentialScope, Self.sha256Hex(Data(canonicalRequest.utf8)), ].joined(separator: "\n") let signature = Self.calculateSignature( secretKey: credentials.secretAccessKey, dateStamp: dateStamp, region: region, service: service, stringToSign: stringToSign) let authorization = "AWS4-HMAC-SHA256 " + "Credential=\(credentials.accessKeyID)/\(credentialScope), " + "SignedHeaders=\(signedHeaders.keys), " + "Signature=\(signature)" request.setValue(authorization, forHTTPHeaderField: "Authorization") } private struct SignedHeadersInfo { let keys: String let canonical: String } private static func signedHeaders(request: URLRequest) -> SignedHeadersInfo { var headers: [(String, String)] = [] if let allHeaders = request.allHTTPHeaderFields { for (key, value) in allHeaders { headers.append((key.lowercased(), value.trimmingCharacters(in: .whitespaces))) } } headers.sort { $0.0 < $1.0 } let keys = headers.map(\.0).joined(separator: ";") let canonical = headers.map { "\($0.0):\($0.1)" }.joined(separator: "\n") return SignedHeadersInfo(keys: keys, canonical: canonical) } private static func canonicalRequest( request: URLRequest, signedHeaders: SignedHeadersInfo, bodyHash: String) -> String { let method = request.httpMethod ?? "GET" let url = request.url! let path = url.path.isEmpty ? "/" : url.path let query = Self.canonicalQueryString(url: url) return [ method, Self.uriEncodePath(path), query, signedHeaders.canonical + "\n", signedHeaders.keys, bodyHash, ].joined(separator: "\n") } private static func canonicalQueryString(url: URL) -> String { guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false), let queryItems = components.queryItems, !queryItems.isEmpty else { return "" } return queryItems .map { item in let key = Self.uriEncode(item.name) let value = Self.uriEncode(item.value ?? "") return "\(key)=\(value)" } .sorted() .joined(separator: "&") } private static func calculateSignature( secretKey: String, dateStamp: String, region: String, service: String, stringToSign: String) -> String { let kDate = Self.hmacSHA256(key: Data("AWS4\(secretKey)".utf8), data: Data(dateStamp.utf8)) let kRegion = Self.hmacSHA256(key: kDate, data: Data(region.utf8)) let kService = Self.hmacSHA256(key: kRegion, data: Data(service.utf8)) let kSigning = Self.hmacSHA256(key: kService, data: Data("aws4_request".utf8)) let signature = Self.hmacSHA256(key: kSigning, data: Data(stringToSign.utf8)) return signature.map { String(format: "%02x", $0) }.joined() } private static func hmacSHA256(key: Data, data: Data) -> Data { let symmetricKey = SymmetricKey(data: key) let mac = HMAC<SHA256>.authenticationCode(for: data, using: symmetricKey) return Data(mac) } private static func sha256Hex(_ data: Data) -> String { let digest = SHA256.hash(data: data) return digest.map { String(format: "%02x", $0) }.joined() } private static func dateFormatter() -> DateFormatter { let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'" formatter.timeZone = TimeZone(identifier: "UTC") formatter.locale = Locale(identifier: "en_US_POSIX") return formatter } private static func dateStamp(date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyyMMdd" formatter.timeZone = TimeZone(identifier: "UTC") formatter.locale = Locale(identifier: "en_US_POSIX") return formatter.string(from: date) } private static func uriEncode(_ string: String) -> String { var allowed = CharacterSet.alphanumerics allowed.insert(charactersIn: "-._~") return string.addingPercentEncoding(withAllowedCharacters: allowed) ?? string } private static func uriEncodePath(_ path: String) -> String { path.split(separator: "/", omittingEmptySubsequences: false) .map { self.uriEncode(String($0)) } .joined(separator: "/") }}
#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif
import Foundation
#if canImport(FoundationNetworking)
import FoundationNetworking
#endif
enum BedrockAWSSigner {
struct Credentials {
let accessKeyID: String
let secretAccessKey: String
let sessionToken: String?
}
static func sign(
request: inout URLRequest,
credentials: Credentials,
region: String,
service: String,
date: Date = Date())
{
let dateFormatter = Self.dateFormatter()
let dateStamp = Self.dateStamp(date: date)
let amzDate = dateFormatter.string(from: date)
request.setValue(amzDate, forHTTPHeaderField: "X-Amz-Date")
if let sessionToken = credentials.sessionToken {
request.setValue(sessionToken, forHTTPHeaderField: "X-Amz-Security-Token")
}
let host = request.url?.host ?? ""
request.setValue(host, forHTTPHeaderField: "Host")
let bodyHash = Self.sha256Hex(request.httpBody ?? Data())
request.setValue(bodyHash, forHTTPHeaderField: "x-amz-content-sha256")
let signedHeaders = Self.signedHeaders(request: request)
let canonicalRequest = Self.canonicalRequest(
request: request,
signedHeaders: signedHeaders,
bodyHash: bodyHash)
let credentialScope = "\(dateStamp)/\(region)/\(service)/aws4_request"
let stringToSign = [
"AWS4-HMAC-SHA256",
amzDate,
credentialScope,
Self.sha256Hex(Data(canonicalRequest.utf8)),
].joined(separator: "\n")
let signature = Self.calculateSignature(
secretKey: credentials.secretAccessKey,
dateStamp: dateStamp,
region: region,
service: service,
stringToSign: stringToSign)
let authorization = "AWS4-HMAC-SHA256 "
+ "Credential=\(credentials.accessKeyID)/\(credentialScope), "
+ "SignedHeaders=\(signedHeaders.keys), "
+ "Signature=\(signature)"
request.setValue(authorization, forHTTPHeaderField: "Authorization")
}
private struct SignedHeadersInfo {
let keys: String
let canonical: String
}
private static func signedHeaders(request: URLRequest) -> SignedHeadersInfo {
var headers: [(String, String)] = []
if let allHeaders = request.allHTTPHeaderFields {
for (key, value) in allHeaders {
headers.append((key.lowercased(), value.trimmingCharacters(in: .whitespaces)))
}
}
headers.sort { $0.0 < $1.0 }
let keys = headers.map(\.0).joined(separator: ";")
let canonical = headers.map { "\($0.0):\($0.1)" }.joined(separator: "\n")
return SignedHeadersInfo(keys: keys, canonical: canonical)
}
private static func canonicalRequest(
request: URLRequest,
signedHeaders: SignedHeadersInfo,
bodyHash: String) -> String
{
let method = request.httpMethod ?? "GET"
let url = request.url!
let path = url.path.isEmpty ? "/" : url.path
let query = Self.canonicalQueryString(url: url)
return [
method,
Self.uriEncodePath(path),
query,
signedHeaders.canonical + "\n",
signedHeaders.keys,
bodyHash,
].joined(separator: "\n")
}
private static func canonicalQueryString(url: URL) -> String {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let queryItems = components.queryItems,
!queryItems.isEmpty
else {
return ""
}
return queryItems
.map { item in
let key = Self.uriEncode(item.name)
let value = Self.uriEncode(item.value ?? "")
return "\(key)=\(value)"
}
.sorted()
.joined(separator: "&")
}
private static func calculateSignature(
secretKey: String,
dateStamp: String,
region: String,
service: String,
stringToSign: String) -> String
{
let kDate = Self.hmacSHA256(key: Data("AWS4\(secretKey)".utf8), data: Data(dateStamp.utf8))
let kRegion = Self.hmacSHA256(key: kDate, data: Data(region.utf8))
let kService = Self.hmacSHA256(key: kRegion, data: Data(service.utf8))
let kSigning = Self.hmacSHA256(key: kService, data: Data("aws4_request".utf8))
let signature = Self.hmacSHA256(key: kSigning, data: Data(stringToSign.utf8))
return signature.map { String(format: "%02x", $0) }.joined()
}
private static func hmacSHA256(key: Data, data: Data) -> Data {
let symmetricKey = SymmetricKey(data: key)
let mac = HMAC<SHA256>.authenticationCode(for: data, using: symmetricKey)
return Data(mac)
}
private static func sha256Hex(_ data: Data) -> String {
let digest = SHA256.hash(data: data)
return digest.map { String(format: "%02x", $0) }.joined()
}
private static func dateFormatter() -> DateFormatter {
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
formatter.timeZone = TimeZone(identifier: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter
}
private static func dateStamp(date: Date) -> String {
let formatter = DateFormatter()
formatter.dateFormat = "yyyyMMdd"
formatter.timeZone = TimeZone(identifier: "UTC")
formatter.locale = Locale(identifier: "en_US_POSIX")
return formatter.string(from: date)
}
private static func uriEncode(_ string: String) -> String {
var allowed = CharacterSet.alphanumerics
allowed.insert(charactersIn: "-._~")
return string.addingPercentEncoding(withAllowedCharacters: allowed) ?? string
}
private static func uriEncodePath(_ path: String) -> String {
path.split(separator: "/", omittingEmptySubsequences: false)
.map { self.uriEncode(String($0)) }
.joined(separator: "/")
}
}