#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.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: "/") } }