TeraStateMigrationTests.swift (25035B)
1 import Foundation 2 import RadrootsKit 3 @testable import TeraApp 4 import XCTest 5 6 final class TeraStateMigrationTests: XCTestCase { 7 func testRelayValidationMatchesRuntimeProfiles() throws { 8 XCTAssertEqual( 9 try TeraNetworkValidator.relays( 10 ["wss://radroots.org", "WSS://WRITE.EXAMPLE:443/"], 11 profile: .publicNetwork 12 ), 13 ["wss://write.example"] 14 ) 15 XCTAssertEqual( 16 try TeraNetworkValidator.relays( 17 ["ws://127.0.0.1:7447"], 18 profile: .simulator 19 ), 20 ["ws://127.0.0.1:7447"] 21 ) 22 for accepted in [ 23 "ws://10.0.0.5:7447", 24 "ws://172.16.0.5:7447", 25 "ws://172.31.255.254:7447", 26 "ws://192.168.0.5:7447", 27 "ws://[fc00::5]:7447", 28 "ws://[fd00::5]:7447", 29 "wss://10.0.0.5:7447", 30 ] { 31 XCTAssertNoThrow( 32 try TeraNetworkValidator.relays([accepted], profile: .device), 33 "Expected device policy to admit \(accepted)" 34 ) 35 } 36 for denied in [ 37 "ws://8.8.8.8:7447", 38 "ws://device.example:7447", 39 "wss://device.example:7447", 40 "ws://0.0.0.0:7447", 41 "ws://127.0.0.1:7447", 42 "ws://169.254.1.1:7447", 43 "ws://172.32.0.5:7447", 44 "ws://100.64.0.5:7447", 45 "ws://224.0.0.1:7447", 46 "ws://[::]:7447", 47 "ws://[::1]:7447", 48 "ws://[fe80::1]:7447", 49 "ws://[ff02::1]:7447", 50 "ws://[2001:4860:4860::8888]:7447", 51 ] { 52 XCTAssertThrowsError( 53 try TeraNetworkValidator.relays([denied], profile: .device), 54 "Expected device policy to deny \(denied)" 55 ) 56 } 57 for denied in [ 58 "ws://public.example", 59 "wss://localhost", 60 "wss://10.0.0.1", 61 "wss://user@example.com", 62 "wss://relay.example?token=value", 63 ] { 64 XCTAssertThrowsError( 65 try TeraNetworkValidator.relays([denied], profile: .publicNetwork), 66 "Expected public policy to deny \(denied)" 67 ) 68 } 69 } 70 71 func testLegacyRelayMigrationIsIdempotentAndCorruptionIsNotAbsence() async throws { 72 let fixture = try StateFixture() 73 defer { fixture.remove() } 74 let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots) 75 try fileAccess.write( 76 .inline( 77 Data( 78 """ 79 {"format":"radroots_field_ios_relay_settings_v1","relays":["ws://127.0.0.1:7447"]} 80 """.utf8 81 ) 82 ), 83 to: RadrootsFileReference( 84 scope: .data, 85 relativePath: "settings/relay_settings.json" 86 ) 87 ) 88 let store = TeraConfigurationStore( 89 bootstrap: fixture.bootstrap, 90 roots: fixture.roots 91 ) 92 let first = try await store.load() 93 let second = try await store.load() 94 XCTAssertEqual(first, second) 95 XCTAssertEqual(first.writableRelays, ["ws://127.0.0.1:7447"]) 96 97 try fileAccess.write( 98 .inline(Data("not-json".utf8)), 99 to: RadrootsFileReference( 100 scope: .data, 101 relativePath: "settings/radroots_configuration_v3.json" 102 ) 103 ) 104 do { 105 _ = try await store.load() 106 XCTFail("Corrupt stored configuration must fail closed") 107 } catch { 108 XCTAssertEqual(error as? TeraConfigurationError, .corruptStoredConfiguration) 109 } 110 } 111 112 func testV2ConfigurationMigratesOnceToExplicitV3BlossomAuthority() async throws { 113 let fixture = try StateFixture() 114 defer { fixture.remove() } 115 let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots) 116 try fileAccess.write( 117 .inline( 118 Data( 119 """ 120 {"format":"radroots_ios_configuration_v2","profile":"simulator","writableRelays":["ws://127.0.0.1:7447"],"blossomOrigins":["http://127.0.0.1:3000","http://localhost:3001"]} 121 """.utf8 122 ) 123 ), 124 to: RadrootsFileReference( 125 scope: .data, 126 relativePath: "settings/radroots_configuration_v2.json" 127 ) 128 ) 129 let store = TeraConfigurationStore(bootstrap: fixture.bootstrap, roots: fixture.roots) 130 131 let first = try await store.load() 132 let second = try await store.load() 133 134 XCTAssertEqual(first, second) 135 XCTAssertEqual( 136 first.blossom, 137 TeraBlossomEndpointConfiguration( 138 hostKind: .simulator, 139 endpointAuthority: .loopbackDevelopment, 140 primaryOrigin: "http://127.0.0.1:3000", 141 fallbackOrigins: ["http://localhost:3001"] 142 ) 143 ) 144 let persisted = try configurationObject(fileAccess) 145 XCTAssertEqual(persisted["format"] as? String, "radroots_ios_configuration_v3") 146 let blossom = try XCTUnwrap(persisted["blossom"] as? [String: Any]) 147 XCTAssertEqual(blossom["hostKind"] as? String, "simulator") 148 XCTAssertEqual(blossom["endpointAuthority"] as? String, "loopback_development") 149 XCTAssertEqual(persisted["activationState"] as? String, "current") 150 XCTAssertEqual(persisted["generation"] as? UInt64, 1) 151 152 let fingerprint = String(repeating: "a", count: 64) 153 try await store.confirmCanonicalBlossomConfiguration( 154 canonicalBlossomConfiguration( 155 primaryOrigin: "http://127.0.0.1:3000", 156 fallbackOrigins: ["http://localhost:3001"], 157 fingerprint: fingerprint 158 ), 159 expectedGeneration: first.generation 160 ) 161 let confirmed = try configurationObject(fileAccess) 162 XCTAssertEqual(confirmed["canonicalBlossomConfigFingerprint"] as? String, fingerprint) 163 XCTAssertEqual(confirmed["activationState"] as? String, "current") 164 } 165 166 func testStoredProfileDriftRecoversFromCurrentBootstrap() async throws { 167 let fixture = try StateFixture() 168 defer { fixture.remove() } 169 let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots) 170 _ = try await TeraConfigurationStore( 171 bootstrap: fixture.bootstrap, 172 roots: fixture.roots 173 ).load() 174 let originalFingerprint = String(repeating: "b", count: 64) 175 let originalStore = TeraConfigurationStore( 176 bootstrap: fixture.bootstrap, 177 roots: fixture.roots 178 ) 179 try await originalStore.confirmCanonicalBlossomConfiguration( 180 canonicalBlossomConfiguration(fingerprint: originalFingerprint), 181 expectedGeneration: 1 182 ) 183 let production = TeraConfigurationBootstrap( 184 runtimeMode: "production", 185 relayURLs: ["wss://write.example"], 186 blossomOrigins: ["https://blossom.example"], 187 keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, 188 bundleIdentifier: fixture.bootstrap.bundleIdentifier, 189 appMetadata: fixture.bootstrap.appMetadata 190 ) 191 let store = TeraConfigurationStore(bootstrap: production, roots: fixture.roots) 192 193 let recovered = try await store.load() 194 195 XCTAssertEqual(recovered.profile, .publicNetwork) 196 XCTAssertEqual(recovered.activationState, .reconfigurationRequired) 197 XCTAssertEqual(recovered.generation, 2) 198 XCTAssertEqual(recovered.previousBlossomConfigFingerprint, originalFingerprint) 199 XCTAssertEqual(recovered.writableRelays, ["wss://write.example"]) 200 XCTAssertEqual( 201 recovered.blossom, 202 TeraBlossomEndpointConfiguration( 203 hostKind: .physicalDevice, 204 endpointAuthority: .publicWebPKI, 205 primaryOrigin: "https://blossom.example", 206 fallbackOrigins: [] 207 ) 208 ) 209 let repeated = try await store.load() 210 XCTAssertEqual(repeated, recovered) 211 212 let persisted = try configurationObject(fileAccess) 213 XCTAssertEqual(persisted["profile"] as? String, "public") 214 XCTAssertEqual(persisted["activationState"] as? String, "reconfiguration_required") 215 XCTAssertNil(persisted["canonicalBlossomConfigFingerprint"]) 216 XCTAssertEqual( 217 persisted["previousBlossomConfigFingerprint"] as? String, 218 originalFingerprint 219 ) 220 } 221 222 func testBootstrapInputDriftRequiresExplicitGenerationAwareActivation() async throws { 223 let fixture = try StateFixture() 224 defer { fixture.remove() } 225 let original = TeraConfigurationStore( 226 bootstrap: fixture.bootstrap, 227 roots: fixture.roots 228 ) 229 let first = try await original.load() 230 let originalFingerprint = String(repeating: "c", count: 64) 231 try await original.confirmCanonicalBlossomConfiguration( 232 canonicalBlossomConfiguration(fingerprint: originalFingerprint), 233 expectedGeneration: first.generation 234 ) 235 let changedBootstrap = TeraConfigurationBootstrap( 236 runtimeMode: fixture.bootstrap.runtimeMode, 237 relayURLs: ["ws://127.0.0.1:7448"], 238 blossomOrigins: ["http://127.0.0.1:3001"], 239 keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, 240 bundleIdentifier: fixture.bootstrap.bundleIdentifier, 241 appMetadata: fixture.bootstrap.appMetadata 242 ) 243 let changed = TeraConfigurationStore( 244 bootstrap: changedBootstrap, 245 roots: fixture.roots 246 ) 247 248 let pending = try await changed.load() 249 250 XCTAssertEqual(pending.activationState, .reconfigurationRequired) 251 XCTAssertEqual(pending.generation, first.generation + 1) 252 XCTAssertEqual(pending.previousBlossomConfigFingerprint, originalFingerprint) 253 XCTAssertEqual(pending.writableRelays, ["ws://127.0.0.1:7448"]) 254 XCTAssertEqual(pending.blossom?.primaryOrigin, "http://127.0.0.1:3001") 255 256 let replacementFingerprint = String(repeating: "d", count: 64) 257 try await changed.confirmCanonicalBlossomConfiguration( 258 canonicalBlossomConfiguration( 259 primaryOrigin: "http://127.0.0.1:3001", 260 fingerprint: replacementFingerprint 261 ), 262 expectedGeneration: pending.generation 263 ) 264 let active = try await changed.load() 265 XCTAssertEqual(active.activationState, .current) 266 XCTAssertEqual(active.generation, pending.generation) 267 XCTAssertNil(active.previousBlossomConfigFingerprint) 268 let persisted = try configurationObject( 269 RadrootsAppleFileAccess(roots: fixture.roots) 270 ) 271 XCTAssertEqual( 272 persisted["canonicalBlossomConfigFingerprint"] as? String, 273 replacementFingerprint 274 ) 275 } 276 277 func testMaximumStoredGenerationFailsAsCorruptionWithoutWrapping() async throws { 278 let fixture = try StateFixture() 279 defer { fixture.remove() } 280 let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots) 281 _ = try await TeraConfigurationStore( 282 bootstrap: fixture.bootstrap, 283 roots: fixture.roots 284 ).load() 285 var persisted = try configurationObject(fileAccess) 286 persisted["generation"] = NSNumber(value: UInt64.max) 287 try fileAccess.write( 288 .inline(JSONSerialization.data(withJSONObject: persisted, options: [.sortedKeys])), 289 to: RadrootsFileReference( 290 scope: .data, 291 relativePath: "settings/radroots_configuration_v3.json" 292 ) 293 ) 294 let changedBootstrap = TeraConfigurationBootstrap( 295 runtimeMode: fixture.bootstrap.runtimeMode, 296 relayURLs: ["ws://127.0.0.1:7448"], 297 blossomOrigins: fixture.bootstrap.blossomOrigins, 298 keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, 299 bundleIdentifier: fixture.bootstrap.bundleIdentifier, 300 appMetadata: fixture.bootstrap.appMetadata 301 ) 302 let changed = TeraConfigurationStore( 303 bootstrap: changedBootstrap, 304 roots: fixture.roots 305 ) 306 307 do { 308 _ = try await changed.load() 309 XCTFail("Maximum persisted generation must fail closed") 310 } catch { 311 XCTAssertEqual(error as? TeraConfigurationError, .corruptStoredConfiguration) 312 } 313 XCTAssertEqual(try configurationObject(fileAccess)["generation"] as? UInt64, .max) 314 } 315 316 func testBootstrapActivationRetainsSelectedNetworkAndClearsPendingRollbackState() async throws { 317 let fixture = try StateFixture() 318 defer { fixture.remove() } 319 let original = TeraConfigurationStore( 320 bootstrap: fixture.bootstrap, 321 roots: fixture.roots 322 ) 323 let first = try await original.load() 324 try await original.confirmCanonicalBlossomConfiguration( 325 canonicalBlossomConfiguration(fingerprint: String(repeating: "f", count: 64)), 326 expectedGeneration: first.generation 327 ) 328 let changedBootstrap = TeraConfigurationBootstrap( 329 runtimeMode: fixture.bootstrap.runtimeMode, 330 relayURLs: ["ws://127.0.0.1:7448"], 331 blossomOrigins: ["http://127.0.0.1:3001"], 332 keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, 333 bundleIdentifier: fixture.bootstrap.bundleIdentifier, 334 appMetadata: fixture.bootstrap.appMetadata 335 ) 336 let changed = TeraConfigurationStore( 337 bootstrap: changedBootstrap, 338 roots: fixture.roots 339 ) 340 let pending = try await changed.load() 341 342 try await changed.confirmBootstrapActivation(expectedGeneration: pending.generation) 343 344 let active = try await changed.load() 345 XCTAssertEqual(active.activationState, .current) 346 XCTAssertEqual(active.generation, pending.generation) 347 XCTAssertEqual(active.writableRelays, ["ws://127.0.0.1:7448"]) 348 XCTAssertEqual(active.blossom?.primaryOrigin, "http://127.0.0.1:3001") 349 XCTAssertNil(active.previousBlossomConfigFingerprint) 350 let persisted = try configurationObject( 351 RadrootsAppleFileAccess(roots: fixture.roots) 352 ) 353 XCTAssertNil(persisted["canonicalBlossomConfigFingerprint"]) 354 XCTAssertNil(persisted["previousBlossomConfigFingerprint"]) 355 } 356 357 func testCanonicalPublicWebPkiRuntimeLabelPreservesStoredFormat() async throws { 358 let fixture = try StateFixture() 359 defer { fixture.remove() } 360 let bootstrap = TeraConfigurationBootstrap( 361 runtimeMode: "production", 362 relayURLs: [], 363 blossomOrigins: ["https://media.example"], 364 keychainServicePrefix: fixture.bootstrap.keychainServicePrefix, 365 bundleIdentifier: fixture.bootstrap.bundleIdentifier, 366 appMetadata: fixture.bootstrap.appMetadata 367 ) 368 let store = TeraConfigurationStore(bootstrap: bootstrap, roots: fixture.roots) 369 let selected = try await store.load() 370 371 try await store.confirmCanonicalBlossomConfiguration( 372 TeraBlossomConfigurationStatus( 373 schemaVersion: 1, 374 hostKind: "physical_device", 375 endpointAuthority: "public_webpki", 376 primaryOrigin: "https://media.example", 377 fallbackOrigins: [], 378 configFingerprint: String(repeating: "e", count: 64) 379 ), 380 expectedGeneration: selected.generation 381 ) 382 383 let persisted = try configurationObject( 384 RadrootsAppleFileAccess(roots: fixture.roots) 385 ) 386 let blossom = try XCTUnwrap(persisted["blossom"] as? [String: Any]) 387 XCTAssertEqual(blossom["endpointAuthority"] as? String, "public_web_pki") 388 } 389 390 func testStoredBlossomAuthorityDriftRecoversWithinProfile() async throws { 391 let fixture = try StateFixture() 392 defer { fixture.remove() } 393 let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots) 394 try fileAccess.write( 395 .inline( 396 Data( 397 """ 398 {"format":"radroots_ios_configuration_v3","profile":"simulator","writableRelays":["ws://127.0.0.1:7447"],"blossom":{"hostKind":"physical_device","endpointAuthority":"public_web_pki","primaryOrigin":"https://wrong.example","fallbackOrigins":[]}} 399 """.utf8 400 ) 401 ), 402 to: RadrootsFileReference( 403 scope: .data, 404 relativePath: "settings/radroots_configuration_v3.json" 405 ) 406 ) 407 let store = TeraConfigurationStore(bootstrap: fixture.bootstrap, roots: fixture.roots) 408 409 let recovered = try await store.load() 410 411 XCTAssertEqual( 412 recovered.blossom, 413 TeraBlossomEndpointConfiguration( 414 hostKind: .simulator, 415 endpointAuthority: .loopbackDevelopment, 416 primaryOrigin: "http://127.0.0.1:3000", 417 fallbackOrigins: [] 418 ) 419 ) 420 } 421 422 private func configurationObject( 423 _ fileAccess: RadrootsAppleFileAccess 424 ) throws -> [String: Any] { 425 let source = try fileAccess.read( 426 RadrootsFileReference( 427 scope: .data, 428 relativePath: "settings/radroots_configuration_v3.json" 429 ), 430 mode: .inline(maxBytes: TeraConfigurationStore.maximumStoredConfigurationBytes) 431 ) 432 guard case let .inline(data) = source, 433 let object = try JSONSerialization.jsonObject(with: data) as? [String: Any] 434 else { 435 throw TeraConfigurationError.persistenceFailed 436 } 437 return object 438 } 439 440 private func canonicalBlossomConfiguration( 441 primaryOrigin: String = "http://127.0.0.1:3000", 442 fallbackOrigins: [String] = [], 443 fingerprint: String 444 ) -> TeraBlossomConfigurationStatus { 445 TeraBlossomConfigurationStatus( 446 schemaVersion: 1, 447 hostKind: "simulator", 448 endpointAuthority: "loopback_development", 449 primaryOrigin: primaryOrigin, 450 fallbackOrigins: fallbackOrigins, 451 configFingerprint: fingerprint 452 ) 453 } 454 455 func testSourceGenerationAndVisualIdentitySurviveStoreRecreation() async throws { 456 let fixture = try StateFixture() 457 defer { fixture.remove() } 458 let firstStore = TeraConfigurationStore( 459 bootstrap: fixture.bootstrap, 460 roots: fixture.roots 461 ) 462 let first = try await firstStore.sourceGeneration() 463 let secondStore = TeraConfigurationStore( 464 bootstrap: fixture.bootstrap, 465 roots: fixture.roots 466 ) 467 let second = try await secondStore.sourceGeneration() 468 XCTAssertEqual(first, second) 469 470 let key = String(repeating: "ab", count: 32) 471 XCTAssertEqual( 472 TeraStableVisualIdentity(publicKeyHex: key), 473 TeraStableVisualIdentity(publicKeyHex: key) 474 ) 475 XCTAssertNotEqual( 476 TeraStableVisualIdentity(publicKeyHex: key).digestHex, 477 TeraStableVisualIdentity(publicKeyHex: String(repeating: "cd", count: 32)).digestHex 478 ) 479 } 480 481 func testSourceGenerationRejectsInvalidInjectedClockWithoutPersistence() async throws { 482 let fixture = try StateFixture() 483 defer { fixture.remove() } 484 for value in [TimeInterval.nan, -1, TimeInterval.greatestFiniteMagnitude, 0] { 485 let store = TeraConfigurationStore( 486 bootstrap: fixture.bootstrap, 487 roots: fixture.roots, 488 clock: TeraClock(now: { Date(timeIntervalSince1970: value) }) 489 ) 490 do { 491 _ = try await store.sourceGeneration() 492 XCTFail("Invalid clock value must fail closed") 493 } catch { 494 XCTAssertEqual(error as? TeraConfigurationError, .persistenceFailed) 495 } 496 } 497 let fileAccess = RadrootsAppleFileAccess(roots: fixture.roots) 498 XCTAssertThrowsError( 499 try fileAccess.read( 500 RadrootsFileReference( 501 scope: .data, 502 relativePath: "state/source_generation_v1.json" 503 ), 504 mode: .inline(maxBytes: TeraConfigurationStore.maximumStoredConfigurationBytes) 505 ) 506 ) 507 } 508 509 func testLegacyIdentityMigrationIsTransactionalAndIdempotent() async throws { 510 let secureStore = InMemorySecureStore() 511 let metadataStore = InMemoryIdentityMetadataStore() 512 let servicePrefix = "org.radroots.tests.identity.\(UUID().uuidString.lowercased())" 513 let defaults = try XCTUnwrap(UserDefaults(suiteName: servicePrefix)) 514 defer { 515 UserDefaults(suiteName: servicePrefix)?.removePersistentDomain(forName: servicePrefix) 516 } 517 let legacyKey = RadrootsSecureStoreKey( 518 namespace: "nostr_identity", 519 name: "selected_secret_hex" 520 ) 521 try secureStore.put( 522 Data(String(repeating: "01", count: 32).utf8), 523 for: legacyKey, 524 policy: .secureLocalSecret 525 ) 526 let custody = try RadrootsIdentityCustody( 527 configuration: RadrootsIdentityCustodyConfiguration( 528 namespace: "radroots_identity_v1", 529 secretPolicy: .secureLocalSecret 530 ), 531 secureStore: secureStore, 532 metadataStore: metadataStore, 533 userPresence: AllowingUserPresence() 534 ) 535 let store = TeraIdentityStore( 536 custody: custody, 537 secureStore: secureStore, 538 servicePrefix: servicePrefix, 539 userDefaults: defaults 540 ) 541 let first = try await store.loadAndMigrate() 542 XCTAssertEqual(first.state, .recoveryRequired) 543 XCTAssertTrue(try secureStore.contains(legacyKey)) 544 let migrated = try await store.recover() 545 let second = try await store.loadAndMigrate() 546 XCTAssertEqual(migrated.publicKeyHex, second.publicKeyHex) 547 XCTAssertEqual(migrated.state, .unlocked) 548 XCTAssertFalse(try secureStore.contains(legacyKey)) 549 } 550 551 func testMalformedLegacyIdentityMetadataIsNotTreatedAsMissing() async throws { 552 let secureStore = InMemorySecureStore() 553 let servicePrefix = "org.radroots.tests.corrupt.\(UUID().uuidString.lowercased())" 554 let defaults = try XCTUnwrap(UserDefaults(suiteName: servicePrefix)) 555 defer { 556 UserDefaults(suiteName: servicePrefix)?.removePersistentDomain(forName: servicePrefix) 557 } 558 defaults.set( 559 Data("not-json".utf8), 560 forKey: "field_ios.identity.public_metadata.\(servicePrefix)" 561 ) 562 let custody = try RadrootsIdentityCustody( 563 configuration: RadrootsIdentityCustodyConfiguration( 564 namespace: "radroots_identity_v1", 565 secretPolicy: .secureLocalSecret 566 ), 567 secureStore: secureStore, 568 metadataStore: InMemoryIdentityMetadataStore(), 569 userPresence: AllowingUserPresence() 570 ) 571 let store = TeraIdentityStore( 572 custody: custody, 573 secureStore: secureStore, 574 servicePrefix: servicePrefix, 575 userDefaults: defaults 576 ) 577 do { 578 _ = try await store.loadAndMigrate() 579 XCTFail("Malformed legacy metadata must be classified as corrupt") 580 } catch { 581 XCTAssertEqual(error as? TeraIdentityStoreError, .corruptLegacyMetadata) 582 } 583 } 584 585 func testRuntimeSignerUsesCanonicalOperationIDInsteadOfSignerRequestDigest() async throws { 586 let secureStore = InMemorySecureStore() 587 let custody = try RadrootsIdentityCustody( 588 configuration: RadrootsIdentityCustodyConfiguration( 589 namespace: "radroots_identity_v1", 590 secretPolicy: .secureLocalSecret 591 ), 592 secureStore: secureStore, 593 metadataStore: InMemoryIdentityMetadataStore(), 594 userPresence: AllowingUserPresence() 595 ) 596 let servicePrefix = "org.radroots.tests.signer.\(UUID().uuidString.lowercased())" 597 let store = TeraIdentityStore( 598 custody: custody, 599 secureStore: secureStore, 600 servicePrefix: servicePrefix 601 ) 602 let identity = try await store.create() 603 let signer = try await store.signer(for: identity) 604 let operationID = UUID().uuidString.lowercased() 605 606 let outcome = try await signer.sign( 607 TeraRuntimeSigningRequest( 608 operationID: operationID, 609 signerRequestID: String(repeating: "ab", count: 32), 610 publicKeyHex: XCTUnwrap(identity.publicKeyHex), 611 purpose: .blossomUpload, 612 deadlineUnixMilliseconds: UInt64(Date().timeIntervalSince1970 * 1000) + 60000, 613 digest: Data(repeating: 0xCD, count: 32) 614 ) 615 ) 616 617 guard case let .signed(signatureHex) = outcome else { 618 return XCTFail("The custody signer rejected the canonical runtime operation ID") 619 } 620 XCTAssertEqual(signatureHex.count, 128) 621 } 622 }