Skip to content

Commit f9a5b5d

Browse files
authored
Merge branch 'main' into trigger-4255
2 parents 852823e + b29b8b8 commit f9a5b5d

8 files changed

Lines changed: 169 additions & 28 deletions

File tree

AmplifyFoundation/Sources/AmplifyMetadata.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ import Foundation
1010
/// Central metadata for the Amplify Swift SDK.
1111
/// CI updates the `version` value during the release process.
1212
public enum AmplifyMetadata {
13-
public static let version = "2.58.4"
13+
public static let version = "2.58.5"
1414
public static let platformName = "amplify-swift"
1515
}

AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthAWSCredentialsProvider.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ public protocol AWSCredentialsProvider {
4444
- accessKeyId: A unique identifier.
4545
- secretAccessKey: A secret key used to sign requests cryptographically.
4646
*/
47-
public protocol AWSCredentials {
47+
public protocol AWSCredentials: Sendable {
4848

4949
/// A unique identifier.
5050
var accessKeyId: String { get }

AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSServiceConfiguration.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import Foundation
1515
public class AmplifyAWSServiceConfiguration {
1616

1717
/// - Tag: AmplifyAWSServiceConfiguration.amplifyVersion
18-
public static let amplifyVersion = "2.58.4"
18+
public static let amplifyVersion = "2.58.5"
1919

2020
/// - Tag: AmplifyAWSServiceConfiguration.platformName
2121
public static let platformName = "amplify-swift"

AmplifyPlugins/Notifications/Push/Tests/PushNotificationHostApp/LocalServer/package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

AmplifyPlugins/Predictions/AWSPredictionsPlugin/Signing/SigV4Signer.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,13 @@ struct SigV4Signer {
202202
serviceName: serviceName
203203
)
204204

205+
let existingQuery = URLComponents(url: url, resolvingAgainstBaseURL: false)?
206+
.queryItems?
207+
.map { "\($0.name)=\($0.value ?? "")" }
208+
.joined(separator: "&")
209+
205210
let canonicalQueryString = _canonicalQueryString(
206-
query: url.query,
211+
query: existingQuery,
207212
signedHeaders: signedHeaders,
208213
timestamp: timestamp,
209214
credentialScope: credentialScope,
@@ -325,7 +330,7 @@ struct SigV4Signer {
325330

326331
let sorted = canonicalQueryString.split(separator: "&")
327332
.map {
328-
String($0).split(separator: "=")
333+
String($0).split(separator: "=", maxSplits: 1)
329334
.map(String.init)
330335
.map(PercentEncoding.uri.encode)
331336
.joined(separator: "=")

AmplifyPlugins/Predictions/Tests/AWSPredictionsPluginUnitTests/SigV4SignerTests/SigV4URLSigningTestCase.swift

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,132 @@ class SigV4URLSigningTestCase: XCTestCase {
157157
XCTAssertEqual(try queryValue(for: "X-Amz-Expires", from: queryItems), String(expiration))
158158
XCTAssertEqual(try queryValue(for: "X-Amz-Signature", from: queryItems), "eb2d084e14a165e42c47d1ad0369b1ea91d31561e6a57d939b071a8f1c3fc18f")
159159
}
160+
161+
// MARK: - Session token with base64 padding (contains '=' characters)
162+
163+
func testSignWithSessionTokenContainingEquals() throws {
164+
let url = try url()
165+
166+
let credential = SigV4Signer.Credential(
167+
accessKey: "AKIAIOSFODNN7EXAMPLE",
168+
secretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
169+
sessionToken: "FwoGZXIvYXdzEBYaDHQa7IU/xL+SomeBase64Token+With/Slashes==")
170+
171+
let signer = SigV4Signer(
172+
credential: credential,
173+
serviceName: "rekognition",
174+
region: "us-east-1"
175+
)
176+
177+
let signedURL = signer.sign(
178+
url: url,
179+
method: .get,
180+
date: { date }
181+
)
182+
183+
let components = URLComponents(url: signedURL, resolvingAgainstBaseURL: false)
184+
let queryItems = try XCTUnwrap(components?.queryItems)
185+
186+
let token = queryItems.first(where: { $0.name == "X-Amz-Security-Token" })?.value
187+
XCTAssertEqual(token, "FwoGZXIvYXdzEBYaDHQa7IU/xL+SomeBase64Token+With/Slashes==",
188+
"Session token with '=' padding must be preserved intact")
189+
190+
// Verify signature is present (not nil/empty) — proves the signing completed without error
191+
let signature = queryItems.first(where: { $0.name == "X-Amz-Signature" })?.value
192+
XCTAssertNotNil(signature)
193+
XCTAssertFalse(signature!.isEmpty)
194+
}
195+
196+
func testSignWithSessionTokenContainingEqualsProducesDeterministicSignature() throws {
197+
let url = try url()
198+
199+
let credential = SigV4Signer.Credential(
200+
accessKey: "AKIAIOSFODNN7EXAMPLE",
201+
secretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
202+
sessionToken: "IQoJb3JpZ2luX2VjEKz//////////wEaCXVzLWVhc3QtMSJHMEUCIQC+base64padding==")
203+
204+
let signer = SigV4Signer(
205+
credential: credential,
206+
serviceName: "rekognition",
207+
region: "us-east-1"
208+
)
209+
210+
let signedURL1 = signer.sign(url: url, method: .get, date: { date })
211+
let signer2 = SigV4Signer(credential: credential, serviceName: "rekognition", region: "us-east-1")
212+
let signedURL2 = signer2.sign(url: url, method: .get, date: { date })
213+
214+
let sig1 = URLComponents(url: signedURL1, resolvingAgainstBaseURL: false)?
215+
.queryItems?.first(where: { $0.name == "X-Amz-Signature" })?.value
216+
let sig2 = URLComponents(url: signedURL2, resolvingAgainstBaseURL: false)?
217+
.queryItems?.first(where: { $0.name == "X-Amz-Signature" })?.value
218+
219+
XCTAssertEqual(sig1, sig2, "Signing must be deterministic for session tokens with '=' characters")
220+
}
221+
222+
// MARK: - URL with query parameters containing special characters
223+
224+
func testSignURLWithSpecialCharactersInQueryParams() throws {
225+
let baseURL = try XCTUnwrap(
226+
URL(string: "wss://streaming-rekognition.us-east-1.amazon.com/start-face-liveness-session-websocket")
227+
)
228+
229+
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
230+
components.queryItems = [
231+
URLQueryItem(name: "session-id", value: "abc-123"),
232+
URLQueryItem(name: "x-amz-user-agent", value: "amplify-swift/2.53.2 api/rekognition os/iOS/26.0")
233+
]
234+
let url = try XCTUnwrap(components.url)
235+
236+
let signer = SigV4Signer(
237+
credential: temporaryCredential,
238+
serviceName: "rekognition",
239+
region: "us-east-1"
240+
)
241+
242+
let signedURL = signer.sign(url: url, method: .get, date: { date })
243+
244+
let signedComponents = URLComponents(url: signedURL, resolvingAgainstBaseURL: false)
245+
let queryItems = try XCTUnwrap(signedComponents?.queryItems)
246+
247+
// Verify the user-agent value survived encoding round-trip intact
248+
let userAgent = queryItems.first(where: { $0.name == "x-amz-user-agent" })?.value
249+
XCTAssertEqual(userAgent, "amplify-swift/2.53.2 api/rekognition os/iOS/26.0")
250+
251+
// Verify signature exists
252+
let signature = queryItems.first(where: { $0.name == "X-Amz-Signature" })?.value
253+
XCTAssertNotNil(signature)
254+
XCTAssertFalse(signature!.isEmpty)
255+
}
256+
257+
func testSignURLWithQueryParamsProducesDeterministicSignature() throws {
258+
let baseURL = try XCTUnwrap(
259+
URL(string: "wss://streaming-rekognition.us-east-1.amazon.com/start-face-liveness-session-websocket")
260+
)
261+
262+
var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false)!
263+
components.queryItems = [
264+
URLQueryItem(name: "session-id", value: "test-session-id"),
265+
URLQueryItem(name: "x-amz-user-agent", value: "amplify-swift/2.53.2 os/iOS/26.0 lang/swift/6.x")
266+
]
267+
let url = try XCTUnwrap(components.url)
268+
269+
let credential = SigV4Signer.Credential(
270+
accessKey: "AKIAIOSFODNN7EXAMPLE",
271+
secretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
272+
sessionToken: "TokenWith+Plus/Slash==")
273+
274+
let signer1 = SigV4Signer(credential: credential, serviceName: "rekognition", region: "us-east-1")
275+
let signer2 = SigV4Signer(credential: credential, serviceName: "rekognition", region: "us-east-1")
276+
277+
let signedURL1 = signer1.sign(url: url, method: .get, date: { date })
278+
let signedURL2 = signer2.sign(url: url, method: .get, date: { date })
279+
280+
let sig1 = URLComponents(url: signedURL1, resolvingAgainstBaseURL: false)?
281+
.queryItems?.first(where: { $0.name == "X-Amz-Signature" })?.value
282+
let sig2 = URLComponents(url: signedURL2, resolvingAgainstBaseURL: false)?
283+
.queryItems?.first(where: { $0.name == "X-Amz-Signature" })?.value
284+
285+
XCTAssertEqual(sig1, sig2,
286+
"Signing must produce identical signatures regardless of how Foundation encodes the URL internally")
287+
}
160288
}

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 2.58.5 (2026-07-27)
4+
5+
### Bug Fixes
6+
7+
- **predictions**: fix Face Liveness with temporary credentials on iOS 26 (URL signing + Sendable credentials) (#4248)
8+
- **auth,push**: prevent command injection in WebAuthn/Push test LocalServers (#4252)
9+
- **auth**: fix visionOS build failure in WebAuthn credential registration (#4250)
10+
311
## 2.58.4 (2026-06-30)
412

513
### Bug Fixes

Package.resolved

Lines changed: 20 additions & 20 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)