Skip to content

Commit

Permalink
Add support for existingConfigId (#49)
Browse files Browse the repository at this point in the history
  • Loading branch information
fboemer authored Nov 12, 2024
1 parent 07e7c3a commit e220657
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 11 deletions.
26 changes: 21 additions & 5 deletions Sources/PIRService/Controllers/PIRServiceController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,27 @@ struct PIRServiceController {
await usecases.get(names: configRequest.usecases)
}

let configs = try requestedUsecases.mapValues { usecase in
var config = try usecase.config()
if let platform = context.platform {
config.makeCompatible(with: platform)
guard configRequest.existingConfigIds.isEmpty ||
configRequest.existingConfigIds.count == requestedUsecases.count
else {
throw HTTPError(.badRequest, message: """
Invalid existingConfigIds count \(configRequest.existingConfigIds.count). \
Expected 0 or \(requestedUsecases.count).
""")
}

let existingConfigIds = configRequest.existingConfigIds.isEmpty ? Array(
repeating: Data(),
count: requestedUsecases.count) : configRequest.existingConfigIds
var configs = [String: Apple_SwiftHomomorphicEncryption_Api_Pir_V1_Config]()
for (usecaseName, configId) in zip(requestedUsecases.keys, existingConfigIds) {
if let usecase = requestedUsecases[usecaseName] {
var config = try usecase.config(existingConfigId: Array(configId))
if let platform = context.platform {
config.makeCompatible(with: platform)
}
configs[usecaseName] = config
}
return config
}

let keyConfigs = try requestedUsecases.values.map { try $0.evaluationKeyConfig() }
Expand All @@ -81,6 +96,7 @@ struct PIRServiceController {
key: key,
as: Protobuf<Apple_SwiftHomomorphicEncryption_Api_Shared_V1_EvaluationKey>.self)
return Apple_SwiftHomomorphicEncryption_Api_Shared_V1_KeyStatus.with { keyStatus in
// A timestamp of 0 indicates the evaluation key does not exist on the server
keyStatus.timestamp = storedEvaluationKey?.message.metadata.timestamp ?? 0
keyStatus.keyConfig = keyConfig
}
Expand Down
12 changes: 12 additions & 0 deletions Sources/PIRService/Controllers/Usecase.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,15 @@ protocol Usecase: Sendable {
evaluationKey: Apple_SwiftHomomorphicEncryption_Api_Shared_V1_EvaluationKey) async throws
-> Apple_SwiftHomomorphicEncryption_Api_Pir_V1_Response
}

extension Usecase {
func config(existingConfigId: [UInt8]) throws -> Apple_SwiftHomomorphicEncryption_Api_Pir_V1_Config {
let config = try config()
if Array(config.configID) == existingConfigId {
return Apple_SwiftHomomorphicEncryption_Api_Pir_V1_Config.with { apiConfig in
apiConfig.reuseExistingConfig = true
}
}
return config
}
}
3 changes: 3 additions & 0 deletions Sources/PIRService/PIRService.docc/HTTPEndpoints.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ Header | `Authorization` | The value will contain a private access to
Header | `User-Agent` | Identifier for the user's OS type and version.
Header | `User-Identifier` | Pseudorandom identifier tied to a user.
Request Body | `ConfigRequest` | Serialized Protobuf message that list the use-cases that the system is interested in.
As of iOS 18.2, the client will set the `existing_config_ids` field.
Response | `ConfigResponse` | Serialized Protobuf message. The `ConfigResponse` contains the `configs` and `key_info` response fields.
As of iOS 18.2, the message may set `reuse_existing_config: true` instead of the `pirConfig` field, reducing the message size.
This indicates the client should use the config with id specified in `existing_config_ids`.
Response field | `configs` | Map from use case names to the corresponding configuration.
Response field | `key_info` | List of `KeyStatus` objects.

Expand Down
2 changes: 2 additions & 0 deletions Sources/PIRService/PIRService.docc/TestingInstructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,8 @@ After introduction in iOS 18.0, `Live Caller ID Lookup` introduced further featu

* `Fixed PIR Shard Config` (iOS 18.2). When all shard configurations are identical, `PIR Fixed Shard Config` allows for a more compact PIR config, saving bandwidth and client-side memory usage. To enable, set the `pirShardConfigs` field in the PIR config. iOS clients prior to iOS 18.2 will still require the `shardConfigs` field to be set. See [Reusing PIR Parameters]( https://swiftpackageindex.com/apple/swift-homomorphic-encryption/main/documentation/privateinformationretrieval/reusingpirparameters) for how to process the database such that all shard configurations are identical.

* `Reusing existing config id` (iOS 18.2). During the `config` request, if a client has a cached configuration, it will send the config id of that cached configuration. Then, if the configuration is unchanged, the server may respond with a config setting `reuseExistingConfig = true` and omit any other fields. This helps reduce the response size for the config fetch.

## Writing the application extension

> Important: Please see [Getting up-to-date calling and blocking information for your
Expand Down
70 changes: 64 additions & 6 deletions Tests/PIRServiceTests/PIRServiceControllerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import HummingbirdTesting
@testable import PIRService
import PrivateInformationRetrieval
import PrivateInformationRetrievalProtobuf
import Util
import XCTest

class PIRServiceControllerTests: XCTestCase {
Expand Down Expand Up @@ -94,10 +95,65 @@ class PIRServiceControllerTests: XCTestCase {
}
}

func testCachedConfigFetch() async throws {
let usecaseStore = UsecaseStore()
let exampleUsecase = ExampleUsecase.repeatedShardConfig
try await usecaseStore.set(name: "test", usecase: exampleUsecase)
let app = try await buildApplication(usecaseStore: usecaseStore)
let user = UserIdentifier()

try await app.test(.live) { client in // swiftlint:disable:this closure_body_length
for platform: Platform in [.macOS15, .macOS15_2, .iOS18, .iOS18_2] {
// No or wrong existing configId
for existingConfigId in [Data(), Data([UInt8(1), 2])] {
let configRequest = Apple_SwiftHomomorphicEncryption_Api_Pir_V1_ConfigRequest.with { configReq in
configReq.usecases = ["test"]
configReq.existingConfigIds = [existingConfigId]
}
try await client.execute(
uri: "/config",
userIdentifier: user,
message: configRequest,
platform: platform)
{ response in
XCTAssertEqual(response.status, .ok)
var expectedConfig = try exampleUsecase.config()
expectedConfig.makeCompatible(with: platform)
let configResponse = try response
.message(as: Apple_SwiftHomomorphicEncryption_Api_Pir_V1_ConfigResponse.self)
XCTAssertEqual(configResponse.configs["test"], expectedConfig)
try XCTAssertEqual(configResponse.keyInfo[0].keyConfig, exampleUsecase.evaluationKeyConfig())
}
}
// Existing configId
let configRequestWithConfigId = try Apple_SwiftHomomorphicEncryption_Api_Pir_V1_ConfigRequest
.with { configReq in
configReq.usecases = ["test"]
configReq.existingConfigIds = try [exampleUsecase.config().configID]
}
try await client.execute(
uri: "/config",
userIdentifier: user,
message: configRequestWithConfigId,
platform: platform)
{ response in
XCTAssertEqual(response.status, .ok)
let configResponse = try response
.message(as: Apple_SwiftHomomorphicEncryption_Api_Pir_V1_ConfigResponse.self)
XCTAssertEqual(configResponse.configs["test"]?.reuseExistingConfig, true)
XCTAssertEqual(
configResponse.configs["test"]?.pirConfig,
Apple_SwiftHomomorphicEncryption_Api_Pir_V1_PIRConfig())
try XCTAssertEqual(configResponse.keyInfo[0].keyConfig, exampleUsecase.evaluationKeyConfig())
}
}
}
}

func testCompressedConfigFetch() async throws {
// Mock usecase that has a large config with 10K randomized shardConfigs.
struct TestUseCaseWithLargeConfig: Usecase {
init() {
init() throws {
let shardConfigs = (0..<10000).map { _ in
Apple_SwiftHomomorphicEncryption_Api_Pir_V1_PIRShardConfig.with { shardConfig in
shardConfig.numEntries = UInt64.random(in: 0..<1000)
Expand All @@ -106,10 +162,12 @@ class PIRServiceControllerTests: XCTestCase {
}
}

self.randomConfig = Apple_SwiftHomomorphicEncryption_Api_Pir_V1_Config.with { config in
config.pirConfig = .with { pirConfig in
pirConfig.shardConfigs = shardConfigs
}
let pirConfg = Apple_SwiftHomomorphicEncryption_Api_Pir_V1_PIRConfig.with { pirConfig in
pirConfig.shardConfigs = shardConfigs
}
self.randomConfig = try Apple_SwiftHomomorphicEncryption_Api_Pir_V1_Config.with { config in
config.pirConfig = pirConfg
config.configID = try pirConfg.sha256()
}
}

Expand All @@ -133,7 +191,7 @@ class PIRServiceControllerTests: XCTestCase {
}

let usecaseStore = UsecaseStore()
let exampleUsecase = TestUseCaseWithLargeConfig()
let exampleUsecase = try TestUseCaseWithLargeConfig()
try await usecaseStore.set(name: "test", usecase: exampleUsecase)

let app = try await buildApplication(usecaseStore: usecaseStore)
Expand Down

0 comments on commit e220657

Please sign in to comment.