From 023fc2341a93b4e70f0db8cd0aa9dc040d1550f8 Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Tue, 27 Jan 2026 07:50:21 -0300 Subject: [PATCH 1/2] feat: make media_integrity required - Remove optional mediaIntegrity from Sidecar struct - Remove legacy generate methods without mediaIntegrity - Require Secure Enclave for photo capture in ExampleApp --- ExampleApp/ExampleApp/ContentView.swift | 33 +++++++++++------------- Sources/SignedShotSDK/Sidecar.swift | 34 +++---------------------- 2 files changed, 19 insertions(+), 48 deletions(-) diff --git a/ExampleApp/ExampleApp/ContentView.swift b/ExampleApp/ExampleApp/ContentView.swift index 63ec9c8..c6d0c40 100644 --- a/ExampleApp/ExampleApp/ContentView.swift +++ b/ExampleApp/ExampleApp/ContentView.swift @@ -401,7 +401,7 @@ struct ContentView: View { } private var canCapture: Bool { - isSetup && hasActiveSession && !captureService.isCaptureInProgress + isSetup && hasActiveSession && isEnclaveReady && !captureService.isCaptureInProgress } // MARK: - Actions @@ -475,6 +475,11 @@ struct ContentView: View { return } + guard isEnclaveReady else { + errorMessage = "Secure Enclave not available. Cannot capture without media integrity." + return + } + do { // 1. Capture photo → JPEG bytes in memory let photo = try await captureService.capturePhoto() @@ -482,14 +487,11 @@ struct ContentView: View { let capturedAt = photo.capturedAt // 2. Generate media integrity (hash + sign) BEFORE saving to disk - var mediaIntegrity: MediaIntegrity? - if isEnclaveReady { - mediaIntegrity = try integrityService.generateIntegrity( - for: photo.jpegData, - captureId: session.captureId, - capturedAt: capturedAt - ) - } + let mediaIntegrity = try integrityService.generateIntegrity( + for: photo.jpegData, + captureId: session.captureId, + capturedAt: capturedAt + ) // 3. Exchange nonce for trust token isExchangingToken = true @@ -498,15 +500,10 @@ struct ContentView: View { isExchangingToken = false // 4. Generate sidecar (JWT + media_integrity) - let sidecarData: Data - if let integrity = mediaIntegrity { - sidecarData = try sidecarGenerator.generate( - jwt: response.trustToken, - mediaIntegrity: integrity - ) - } else { - sidecarData = try sidecarGenerator.generate(jwt: response.trustToken) - } + let sidecarData = try sidecarGenerator.generate( + jwt: response.trustToken, + mediaIntegrity: mediaIntegrity + ) // 5. Save photo + sidecar together let url = try storage.save(photo) diff --git a/Sources/SignedShotSDK/Sidecar.swift b/Sources/SignedShotSDK/Sidecar.swift index 535f967..99a7259 100644 --- a/Sources/SignedShotSDK/Sidecar.swift +++ b/Sources/SignedShotSDK/Sidecar.swift @@ -8,15 +8,8 @@ public struct Sidecar: Codable, Sendable { /// Capture trust information from the backend (ES256 signed JWT) public let captureTrust: CaptureTrust - /// Media integrity proof from the device (optional, requires Secure Enclave) - public let mediaIntegrity: MediaIntegrity? - - /// Initialize with JWT only (legacy, no media integrity) - public init(version: String = "1.0", jwt: String) { - self.version = version - self.captureTrust = CaptureTrust(jwt: jwt) - self.mediaIntegrity = nil - } + /// Media integrity proof from the device's Secure Enclave + public let mediaIntegrity: MediaIntegrity /// Initialize with JWT and media integrity public init(version: String = "1.0", jwt: String, mediaIntegrity: MediaIntegrity) { @@ -51,15 +44,7 @@ public struct SidecarGenerator { encoder.outputFormatting = [.prettyPrinted, .sortedKeys] } - /// Generate sidecar JSON data (legacy, no media integrity) - /// - Parameter jwt: The trust token from the backend - /// - Returns: JSON data for the sidecar file - public func generate(jwt: String) throws -> Data { - let sidecar = Sidecar(jwt: jwt) - return try encoder.encode(sidecar) - } - - /// Generate sidecar JSON data with media integrity + /// Generate sidecar JSON data /// - Parameters: /// - jwt: The trust token from the backend /// - mediaIntegrity: The media integrity proof from Secure Enclave @@ -69,18 +54,7 @@ public struct SidecarGenerator { return try encoder.encode(sidecar) } - /// Generate sidecar and return as string (legacy, no media integrity) - /// - Parameter jwt: The trust token from the backend - /// - Returns: JSON string for the sidecar file - public func generateString(jwt: String) throws -> String { - let data = try generate(jwt: jwt) - guard let string = String(data: data, encoding: .utf8) else { - throw SidecarError.encodingFailed - } - return string - } - - /// Generate sidecar and return as string with media integrity + /// Generate sidecar and return as string /// - Parameters: /// - jwt: The trust token from the backend /// - mediaIntegrity: The media integrity proof from Secure Enclave From 714ba62d74dca13d8ade64f4d1a1cdc380dbcb4a Mon Sep 17 00:00:00 2001 From: Felippe Costa Date: Tue, 27 Jan 2026 07:57:10 -0300 Subject: [PATCH 2/2] fix: update tests for required media_integrity --- Tests/SignedShotSDKTests/APIModelsTests.swift | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/Tests/SignedShotSDKTests/APIModelsTests.swift b/Tests/SignedShotSDKTests/APIModelsTests.swift index 3f06ffb..39e5ebb 100644 --- a/Tests/SignedShotSDKTests/APIModelsTests.swift +++ b/Tests/SignedShotSDKTests/APIModelsTests.swift @@ -140,7 +140,14 @@ final class APIModelsTests: XCTestCase { // MARK: - Sidecar Tests func testSidecarEncoding() throws { - let sidecar = Sidecar(jwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.test.sig") + let mediaIntegrity = MediaIntegrity( + contentHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + signature: "c2lnbmF0dXJl", + publicKey: "cHVibGljS2V5", + captureId: "550e8400-e29b-41d4-a716-446655440000", + capturedAt: "2026-01-26T15:30:00Z" + ) + let sidecar = Sidecar(jwt: "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.test.sig", mediaIntegrity: mediaIntegrity) let encoder = JSONEncoder() let data = try encoder.encode(sidecar) @@ -150,6 +157,10 @@ final class APIModelsTests: XCTestCase { let captureTrust = json?["capture_trust"] as? [String: Any] XCTAssertEqual(captureTrust?["jwt"] as? String, "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.test.sig") + + let integrity = json?["media_integrity"] as? [String: Any] + XCTAssertNotNil(integrity) + XCTAssertEqual(integrity?["capture_id"] as? String, "550e8400-e29b-41d4-a716-446655440000") } func testSidecarDecoding() throws { @@ -158,6 +169,13 @@ final class APIModelsTests: XCTestCase { "version": "1.0", "capture_trust": { "jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature" + }, + "media_integrity": { + "content_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "signature": "c2lnbmF0dXJl", + "public_key": "cHVibGljS2V5", + "capture_id": "550e8400-e29b-41d4-a716-446655440000", + "captured_at": "2026-01-26T15:30:00Z" } } """ @@ -167,17 +185,26 @@ final class APIModelsTests: XCTestCase { XCTAssertEqual(sidecar.version, "1.0") XCTAssertEqual(sidecar.captureTrust.jwt, "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.payload.signature") + XCTAssertEqual(sidecar.mediaIntegrity.captureId, "550e8400-e29b-41d4-a716-446655440000") } func testSidecarGenerator() throws { let generator = SidecarGenerator() let jwt = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.test.signature" + let mediaIntegrity = MediaIntegrity( + contentHash: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + signature: "c2lnbmF0dXJl", + publicKey: "cHVibGljS2V5", + captureId: "550e8400-e29b-41d4-a716-446655440000", + capturedAt: "2026-01-26T15:30:00Z" + ) - let jsonString = try generator.generateString(jwt: jwt) + let jsonString = try generator.generateString(jwt: jwt, mediaIntegrity: mediaIntegrity) XCTAssertTrue(jsonString.contains("\"version\" : \"1.0\"")) XCTAssertTrue(jsonString.contains("\"capture_trust\"")) XCTAssertTrue(jsonString.contains(jwt)) + XCTAssertTrue(jsonString.contains("\"media_integrity\"")) } // MARK: - KeychainError Tests