diff --git a/crypto-ffi/bindings/js/packages/browser/shared/utils.ts b/crypto-ffi/bindings/js/packages/browser/shared/utils.ts index 5d793b6c07..db016da2db 100644 --- a/crypto-ffi/bindings/js/packages/browser/shared/utils.ts +++ b/crypto-ffi/bindings/js/packages/browser/shared/utils.ts @@ -136,7 +136,8 @@ export async function sharedSetup() { ): Promise { const clientId = options.clientId ?? window.helpers.newClientId(); - const db = await window.helpers.newDatabase(); + const db = + options.database ?? (await window.helpers.newDatabase()); const cc = window.ccModule.CoreCrypto.new(db); // this also sets the default if undefined @@ -503,11 +504,13 @@ type CcInitOptions = | { withBasicCredential: false; clientId?: ClientId; + database?: Database; } | { withBasicCredential?: true; cipherSuite?: CipherSuite; clientId?: ClientId; + database?: Database; }; export interface Helpers { diff --git a/crypto-ffi/bindings/js/packages/browser/test/e2ei.test.ts b/crypto-ffi/bindings/js/packages/browser/test/e2ei.test.ts index 25851f0b90..37c4e08ee8 100644 --- a/crypto-ffi/bindings/js/packages/browser/test/e2ei.test.ts +++ b/crypto-ffi/bindings/js/packages/browser/test/e2ei.test.ts @@ -167,6 +167,53 @@ describe("end to end identity", () => { await expect(acquisitionCreated).toBe(true); }); + it("should instantiate an x509 credential acquisition object from credential ref", async () => { + const acquisitionCreated = await browser.execute(async () => { + const database = await window.helpers.newDatabase(); + const pkiEnvironment = await window.ccModule.PkiEnvironment.create( + window.pkiEnvironmentHooks, + database + ); + + const clientId = window.helpers.newClientId( + "LcksJb74Tm6N12cDjFy7lQ:8e6424430d3b28be@world.com" + ); + const config = + window.ccModule.X509CredentialAcquisitionConfiguration.new({ + acmeUrl: "acme.example.com", + idpUrl: "https://idp.example.com", + ciphersuite: window.defaultCipherSuite, + displayName: "Alice Smith", + clientId, + handle: "alice_wire", + domain: "world.com", + team: undefined, + validityPeriodSecs: BigInt(3600), + }); + + const cc = await window.helpers.ccInit({ + withBasicCredential: true, + clientId, + database, + }); + + const [credentialRef] = await cc.transaction( + async (ctx) => await ctx.findCredentials({ clientId }) + ); + + const acquisition = + await window.ccModule.X509CredentialAcquisition.newFromCredentialRef( + pkiEnvironment, + config, + credentialRef! + ); + + return acquisition !== undefined; + }); + + await expect(acquisitionCreated).toBe(true); + }); + it("should not be enabled on conversation with basic credential", async () => { const conversationState = await browser.execute(async () => { const cc = await window.helpers.ccInit(); diff --git a/crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/MlsModel.kt b/crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/MlsModel.kt index f24651305b..e3d592be5c 100644 --- a/crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/MlsModel.kt +++ b/crypto-ffi/bindings/shared/src/commonMain/kotlin/com/wire/crypto/MlsModel.kt @@ -43,3 +43,17 @@ suspend fun PkiEnvironment.Companion.new( hooks: PkiEnvironmentHooks, database: Database ) = createPkiEnvironment(hooks, database) + +/** + * Create a new credential acquisition from an existing credential. + * This API is temporary until our system decouples client identities from a client's public signature key. + * + * Provide [coreCryptoDatabase] if you're using distinct DB instances for [PkiEnvironment] and [CoreCrypto]. + * Otherwise, the [PkiEnvironment]'s DB will be used to load the full credential. + */ +suspend fun X509CredentialAcquisition.Companion.newFromCredentialRef( + pkiEnvironment: PkiEnvironment, + config: X509CredentialAcquisitionConfiguration, + credentialRef: CredentialRef, + coreCryptoDatabase: Database? = null, +) = x509CredentialAcquisitionNewFromCredentialRef(pkiEnvironment, config, credentialRef, coreCryptoDatabase) diff --git a/crypto-ffi/bindings/shared/src/commonTest/kotlin/com/wire/crypto/E2EITest.kt b/crypto-ffi/bindings/shared/src/commonTest/kotlin/com/wire/crypto/E2EITest.kt index b26619ce34..9ee6938747 100644 --- a/crypto-ffi/bindings/shared/src/commonTest/kotlin/com/wire/crypto/E2EITest.kt +++ b/crypto-ffi/bindings/shared/src/commonTest/kotlin/com/wire/crypto/E2EITest.kt @@ -89,6 +89,34 @@ internal class E2EITest { assertThat(acquisition).isNotNull } + @Test + fun testInstantiateX509CredentialAcquisitionFromCredentialRef() = runTest { + val db = newDatabase() + val pkiEnv = PkiEnvironment.new(MockPkiEnvironmentHooks(), db) + val clientId = ClientId("LcksJb74Tm6N12cDjFy7lQ:8e6424430d3b28be@world.com".encodeToByteArray()) + val config = X509CredentialAcquisitionConfiguration( + acmeUrl = "acme.example.com", + idpUrl = "https://idp.example.com", + ciphersuite = CIPHERSUITE_DEFAULT, + displayName = "Alice Smith", + clientId = clientId, + handle = "alice_wire", + domain = "world.com", + team = null, + validityPeriodSecs = 3600uL + ) + + val cc = ccInit(CcInitOptions.WithBasicCredential(clientId = clientId, database = db)) + + val credentialRef = cc.transaction { ctx -> + ctx.findCredentials(clientId = clientId).first() + } + + val acquisition = X509CredentialAcquisition.newFromCredentialRef(pkiEnv, config, credentialRef) + + assertThat(acquisition).isNotNull + } + @Test fun conversation_should_be_not_verified_when_at_least_1_of_the_members_uses_a_Basic_credential() = runTest { diff --git a/crypto-ffi/bindings/shared/src/commonTest/kotlin/com/wire/crypto/testutils/TestUtils.kt b/crypto-ffi/bindings/shared/src/commonTest/kotlin/com/wire/crypto/testutils/TestUtils.kt index 45022cd474..41b31bf30a 100644 --- a/crypto-ffi/bindings/shared/src/commonTest/kotlin/com/wire/crypto/testutils/TestUtils.kt +++ b/crypto-ffi/bindings/shared/src/commonTest/kotlin/com/wire/crypto/testutils/TestUtils.kt @@ -104,21 +104,24 @@ class MockPkiEnvironmentHooks : PkiEnvironmentHooks { sealed interface CcInitOptions { val clientId: ClientId? + val database: Database? data class WithoutBasicCredential( - override val clientId: ClientId? = null + override val clientId: ClientId? = null, + override val database: Database? = null, ) : CcInitOptions data class WithBasicCredential( val cipherSuite: CipherSuite = CIPHERSUITE_DEFAULT, - override val clientId: ClientId? = null + override val clientId: ClientId? = null, + override val database: Database? = null, ) : CcInitOptions } suspend fun ccInit( options: CcInitOptions = CcInitOptions.WithBasicCredential() ): CoreCrypto { - val db = newDatabase() + val db = options.database ?: newDatabase() val cc = CoreCrypto(db) val clientId = options.clientId ?: genClientId() diff --git a/crypto-ffi/bindings/swift/WireCoreCrypto/WireCoreCryptoTests/WireCoreCryptoTests.swift b/crypto-ffi/bindings/swift/WireCoreCrypto/WireCoreCryptoTests/WireCoreCryptoTests.swift index e5bcaeaf2e..f60f81d965 100644 --- a/crypto-ffi/bindings/swift/WireCoreCrypto/WireCoreCryptoTests/WireCoreCryptoTests.swift +++ b/crypto-ffi/bindings/swift/WireCoreCrypto/WireCoreCryptoTests/WireCoreCryptoTests.swift @@ -31,7 +31,8 @@ final class WireCoreCryptoTests: XCTestCase { } func testSetClientDataPersists() async throws { - let coreCrypto = try await createCoreCrypto() + let database = try await newDatabase() + let coreCrypto = try CoreCrypto(database: database) let data = Data("my message processing checkpoint".utf8) try await coreCrypto.transaction { context in @@ -53,9 +54,9 @@ final class WireCoreCryptoTests: XCTestCase { let key = genDatabaseKey() - let database = try await openDatabase(location: keystore.path, key: key) + let database = try await Database.open(location: keystore.path, key: key) - let database2 = try await openDatabase(location: keystore.path, key: key) + let database2 = try await Database.open(location: keystore.path, key: key) XCTAssertNotNil(database) XCTAssertNotNil(database2) @@ -80,24 +81,19 @@ final class WireCoreCryptoTests: XCTestCase { let key = genDatabaseKey() - try await _ = openDatabase(location: keystore.path, key: key) + try await _ = Database.open(location: keystore.path, key: key) let invalidKey = genDatabaseKey() await XCTAssertThrowsErrorAsync { - try await openDatabase( + try await Database.open( location: keystore.path, key: invalidKey ) } } func testUpdatingDatabaseKeyWorks() async throws { - let root = FileManager.default.temporaryDirectory.appending(path: "mls") - let keystore = root.appending(path: "keystore-\(UUID().uuidString)") - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - - let key1 = genDatabaseKey() - let database = try await Database.open(location: keystore.path, key: key1) + let database = try await newDatabase() var coreCrypto = try CoreCrypto(database: database) let clientId = ClientId(bytes: UUID().uuidString.data(using: .utf8)!) @@ -112,19 +108,14 @@ final class WireCoreCryptoTests: XCTestCase { return credentialRef.publicKeyHash() } - let key2 = genDatabaseKey() - XCTAssertNotEqual(key1, key2) - - try await database.updateKey(key: key2) + let key = genDatabaseKey() + try await database.updateKey(key: key) coreCrypto = try CoreCrypto(database: database) let pubkey2 = try await coreCrypto.transaction { ctx in try await ctx.mlsInit( clientId: clientId, transport: self.mockMlsTransport) - return try await ctx.findCredentials( - clientId: clientId, publicKey: nil, ciphersuite: nil, credentialType: nil, - earliestValidity: nil - ).first?.publicKeyHash() + return try await ctx.getCredentials().first?.publicKeyHash() } XCTAssertEqual(pubkey1, pubkey2) @@ -194,7 +185,8 @@ final class WireCoreCryptoTests: XCTestCase { func testInteractionWithInvalidContextThrowsError() async throws { let aliceId = ClientId(bytes: Data("alice1".utf8)) - let coreCrypto = try await createCoreCrypto() + let database = try await newDatabase() + let coreCrypto = try CoreCrypto(database: database) var context: CoreCryptoContextProtocol? try await coreCrypto.transaction { context = $0 } @@ -210,7 +202,8 @@ final class WireCoreCryptoTests: XCTestCase { func testErrorIsPropagatedByTransaction() async throws { struct MyError: Error, Equatable {} - let coreCrypto = try await createCoreCrypto() + let database = try await newDatabase() + let coreCrypto = try CoreCrypto(database: database) let expectedError = MyError() await XCTAssertThrowsErrorAsync( @@ -226,21 +219,15 @@ final class WireCoreCryptoTests: XCTestCase { func testTransactionRollsBackOnError() async throws { struct MyError: Error, Equatable {} - let coreCrypto = try await createClients("alice1")[0] - let conversationId = ConversationId(bytes: Data("conversation1".utf8)) - let ciphersuite = try ciphersuiteFromU16(discriminant: 2) + let coreCrypto = try await ccInit() let expectedError = MyError() let credentialRef = try await coreCrypto.transaction { ctx in - return try await ctx.findCredentials( - clientId: nil, - publicKey: nil, - ciphersuite: ciphersuite, - credentialType: nil, - earliestValidity: nil - ).first! + return try await ctx.getCredentials().first! } + let conversationId = genConversationId() + await XCTAssertThrowsErrorAsync( expectedError, when: { @@ -256,14 +243,15 @@ final class WireCoreCryptoTests: XCTestCase { // This would fail with a "Conversation already exists" exception, if the above // transaction hadn't been rolled back. try await coreCrypto.transaction { ctx in - await ctx.createConversationShort( - conversationId: conversationId - ) + try await ctx.createConversation( + conversationId: conversationId, credentialRef: credentialRef, + externalSender: nil) } } func testParallelTransactionsArePerformedSerially() async throws { - let coreCrypto = try await createCoreCrypto() + let database = try await newDatabase() + let coreCrypto = try CoreCrypto(database: database) let token = "t" let transactionCount = 3 @@ -357,21 +345,8 @@ final class WireCoreCryptoTests: XCTestCase { } func testErrorTypeMappingShouldWork() async throws { - let conversationId = ConversationId(bytes: Data("conversation1".utf8)) - let alice = try await createClients("alice1")[0] - let ciphersuite = try ciphersuiteFromU16(discriminant: 2) - let credentialRef = try await alice.transaction { ctx in - await ctx.createConversationShort( - conversationId: conversationId - ) - return try await ctx.findCredentials( - clientId: nil, - publicKey: nil, - ciphersuite: ciphersuite, - credentialType: nil, - earliestValidity: nil - ).first! - } + let alice = try await ccInit() + let conversationId = try await createConversation(coreCrypto: alice) let expectedError = CoreCryptoError.Mls( mlsError: MlsError.ConversationAlreadyExists( @@ -379,6 +354,7 @@ final class WireCoreCryptoTests: XCTestCase { )) try await alice.transaction { ctx in + let credentialRef = try await ctx.getCredentials().first! await self.XCTAssertThrowsErrorAsync(expectedError) { try await ctx.createConversation( conversationId: conversationId, @@ -390,13 +366,10 @@ final class WireCoreCryptoTests: XCTestCase { } func testFindCredentialsShouldReturnNonEmptyResult() async throws { - let alice = try await createClients("alice1")[0] + let alice = try await ccInit() let publicKey = try await alice.transaction { ctx in - try await ctx.findCredentials( - clientId: nil, publicKey: nil, ciphersuite: nil, - credentialType: .basic, earliestValidity: nil - ) - }.first!.publicKeyHash() + try await ctx.getCredentials().first!.publicKeyHash() + } XCTAssertNotNil(publicKey) } @@ -409,39 +382,25 @@ final class WireCoreCryptoTests: XCTestCase { } func testCanAddBasicCredential() async throws { - let clientId = genClientId() - let credential = try Credential.basic(ciphersuite: ciphersuiteDefault(), clientId: clientId) + let alice = try await ccInit() - let alice = try await createCoreCrypto() - let ref = try await alice.transaction { ctx in - try await ctx.mlsInit( - clientId: clientId, - transport: self.mockMlsTransport) - return try await ctx.addCredential(credential: credential) + let allCredentials = try await alice.transaction { ctx in + try await ctx.getCredentials() } + let ref = allCredentials.first! + XCTAssertEqual(ref.type(), CredentialType.basic) XCTAssertNotEqual(ref.earliestValidity(), 0) - let allCredentials = try await alice.transaction { ctx in - try await ctx.getCredentials() - } XCTAssertEqual(allCredentials.count, 1) } func testCanRemoveBasicCredential() async throws { - let clientId = genClientId() - let credential = try Credential.basic(ciphersuite: ciphersuiteDefault(), clientId: clientId) - - let alice = try await createCoreCrypto() - let ref = try await alice.transaction { ctx in - try await ctx.mlsInit( - clientId: clientId, - transport: self.mockMlsTransport) - return try await ctx.addCredential(credential: credential) - } + let alice = try await ccInit() try await alice.transaction { ctx in + let ref = try await ctx.getCredentials().first! try await ctx.removeCredential(credentialRef: ref) } @@ -453,19 +412,16 @@ final class WireCoreCryptoTests: XCTestCase { func testCanSearchCredentialsByCiphersuite() async throws { let clientId = genClientId() + let ciphersuite1 = CipherSuite.mls128Dhkemp256Aes128gcmSha256P256 - let credential1 = try Credential.basic(ciphersuite: ciphersuite1, clientId: clientId) + let alice = try await ccInit( + options: CcInitOptions.withBasicCredential( + cipherSuite: ciphersuite1, clientId: clientId)) let ciphersuite2 = CipherSuite.mls128Dhkemx25519Chacha20poly1305Sha256Ed25519 let credential2 = try Credential.basic(ciphersuite: ciphersuite2, clientId: clientId) - let alice = try await createCoreCrypto() try await alice.transaction { ctx in - try await ctx.mlsInit( - clientId: clientId, - transport: self.mockMlsTransport - ) - _ = try await ctx.addCredential(credential: credential1) _ = try await ctx.addCredential(credential: credential2) } @@ -494,18 +450,11 @@ final class WireCoreCryptoTests: XCTestCase { } func testConversationExistsShouldReturnTrue() async throws { - let conversationId = ConversationId(bytes: Data("conversation1".utf8)) - let ciphersuite = try ciphersuiteFromU16(discriminant: 2) - let alice = try await createClients("alice1")[0] + let conversationId = genConversationId() + let alice = try await ccInit() let credentialRef = try await alice.transaction { ctx in - return try await ctx.findCredentials( - clientId: nil, - publicKey: nil, - ciphersuite: ciphersuite, - credentialType: nil, - earliestValidity: nil - ).first! + return try await ctx.getCredentials().first! } let resultBefore = try await alice.transaction { ctx in @@ -522,42 +471,12 @@ final class WireCoreCryptoTests: XCTestCase { } func testUpdateKeyingMaterialShouldProcessTheCommitMessage() async throws { - let conversationId = ConversationId(bytes: Data("conversation1".utf8)) - let ciphersuite = try ciphersuiteFromU16(discriminant: 2) - let clients = try await createClients("alice1", "bob1") - let alice = clients[0] - let bob = clients[1] + let alice = try await ccInit() + let bob = try await ccInit() - try await bob.transaction { ctx in - await ctx.createConversationShort( - conversationId: conversationId - ) - } - let aliceKp = try await alice.transaction { ctx in - let credential = try await ctx.findCredentials( - clientId: nil, - publicKey: nil, - ciphersuite: ciphersuite, - credentialType: .basic, - earliestValidity: nil - ).first! + let conversationId = try await createConversation(coreCrypto: alice) + _ = try await invite(cc1: bob, cc2: alice, conversationId: conversationId) - return try await ctx.generateKeyPackage( - credentialRef: credential, - lifetime: nil - ) - } - try await bob.transaction { ctx in - _ = try await ctx.addClientsToConversation( - conversationId: conversationId, keyPackages: [aliceKp] - ) - } - let welcome = await mockMlsTransport.lastCommitBundle?.welcome - _ = try await alice.transaction { ctx in - try await ctx.processWelcomeMessage( - welcomeMessage: welcome! - ) - } try await bob.transaction { ctx in try await ctx.updateKeyingMaterial(conversationId: conversationId) } @@ -570,45 +489,13 @@ final class WireCoreCryptoTests: XCTestCase { XCTAssertNil(decrypted.senderClientId) } - // swiftlint:disable:next function_body_length func testEncryptMessageCanBeDecryptedByReceiver() async throws { - let conversationId = ConversationId(bytes: Data("conversation1".utf8)) - let ciphersuite = try ciphersuiteFromU16(discriminant: 2) - let clients = try await createClients("alice1", "bob1") - let alice = clients[0] - let bob = clients[1] - - try await bob.transaction { ctx in - await ctx.createConversationShort( - conversationId: conversationId - ) - } + let alice = try await ccInit() + let bob = try await ccInit() - let aliceKp = try await alice.transaction { ctx in - let credential = try await ctx.findCredentials( - clientId: nil, - publicKey: nil, - ciphersuite: ciphersuite, - credentialType: .basic, - earliestValidity: nil - ).first! + let conversationId = try await createConversation(coreCrypto: bob) + _ = try await invite(cc1: bob, cc2: alice, conversationId: conversationId) - return try await ctx.generateKeyPackage( - credentialRef: credential, - lifetime: nil - ) - } - try await bob.transaction { ctx in - _ = try await ctx.addClientsToConversation( - conversationId: conversationId, keyPackages: [aliceKp] - ) - } - let welcome = await mockMlsTransport.lastCommitBundle?.welcome - _ = try await alice.transaction { ctx in - try await ctx.processWelcomeMessage( - welcomeMessage: welcome! - ) - } let message = Data("Hello World !".utf8) let ciphertext = try await alice.transaction { ctx in try await ctx.encryptMessage( @@ -646,15 +533,10 @@ final class WireCoreCryptoTests: XCTestCase { } } - let coreCrypto = try await createClients("alice")[0] - let conversationId = ConversationId(bytes: Data("conversation1".utf8)) + let coreCrypto = try await ccInit() // set up the conversation in one transaction - try await coreCrypto.transaction { context in - await context.createConversationShort( - conversationId: conversationId - ) - } + let conversationId = try await createConversation(coreCrypto: coreCrypto) // register the observer let epochRecorder = EpochRecorder() @@ -685,15 +567,10 @@ final class WireCoreCryptoTests: XCTestCase { } } - let conversationId = ConversationId(bytes: Data("conversation1".utf8)) - let coreCrypto = try await createClients("alice")[0] + let coreCrypto = try await ccInit() // set up the conversation in one transaction - try await coreCrypto.transaction { ctx in - await ctx.createConversationShort( - conversationId: conversationId - ) - } + let conversationId = try await createConversation(coreCrypto: coreCrypto) // register the observer let historyRecorder = HistoryRecorder() @@ -711,43 +588,16 @@ final class WireCoreCryptoTests: XCTestCase { } func testCanCreateKeypackage() async throws { - let clientId = genClientId() - let credential = try Credential.basic(ciphersuite: ciphersuiteDefault(), clientId: clientId) - - let alice = try await createCoreCrypto() - let credentialRef = try await alice.transaction { ctx in - try await ctx.mlsInit( - clientId: clientId, - transport: self.mockMlsTransport - - ) - return try await ctx.addCredential(credential: credential) - } - - let keyPackage = try await alice.transaction { ctx in - try await ctx.generateKeyPackage(credentialRef: credentialRef, lifetime: nil) - } - + let alice = try await ccInit() + let keyPackage = try await generateKeyPackage(coreCrypto: alice) XCTAssertNotNil(keyPackage) } func testCanSerializeKeypackage() async throws { - let clientId = genClientId() - let credential = try Credential.basic(ciphersuite: ciphersuiteDefault(), clientId: clientId) - - let alice = try await createCoreCrypto() - let credentialRef = try await alice.transaction { ctx in - try await ctx.mlsInit( - clientId: clientId, - transport: self.mockMlsTransport - ) - return try await ctx.addCredential(credential: credential) - } + let alice = try await ccInit() - let keyPackage = try await alice.transaction { ctx in - try await ctx.generateKeyPackage(credentialRef: credentialRef, lifetime: nil) - } + let keyPackage = try await generateKeyPackage(coreCrypto: alice) let bytes = try keyPackage.serialize() XCTAssertFalse(bytes.isEmpty) @@ -760,22 +610,8 @@ final class WireCoreCryptoTests: XCTestCase { } func testCanRetrieveKeypackagesInBulk() async throws { - let clientId = genClientId() - let credential = try Credential.basic(ciphersuite: ciphersuiteDefault(), clientId: clientId) - - let alice = try await createCoreCrypto() - let credentialRef = try await alice.transaction { ctx in - try await ctx.mlsInit( - clientId: clientId, - transport: self.mockMlsTransport - - ) - return try await ctx.addCredential(credential: credential) - } - - _ = try await alice.transaction { ctx in - try await ctx.generateKeyPackage(credentialRef: credentialRef, lifetime: nil) - } + let alice = try await ccInit() + _ = try await generateKeyPackage(coreCrypto: alice) let keyPackages = try await alice.transaction { ctx in try await ctx.getKeyPackages() @@ -786,27 +622,13 @@ final class WireCoreCryptoTests: XCTestCase { } func testCanRemoveKeypackage() async throws { - let clientId = genClientId() - let credential = try Credential.basic(ciphersuite: ciphersuiteDefault(), clientId: clientId) - - let alice = try await createCoreCrypto() - let credentialRef = try await alice.transaction { ctx in - try await ctx.mlsInit( - clientId: clientId, - transport: self.mockMlsTransport - ) - return try await ctx.addCredential(credential: credential) - } + let alice = try await ccInit() // add a kp which will not be removed - _ = try await alice.transaction { ctx in - try await ctx.generateKeyPackage(credentialRef: credentialRef, lifetime: nil) - } + _ = try await generateKeyPackage(coreCrypto: alice) // add a kp which will be removed - let keyPackage = try await alice.transaction { ctx in - try await ctx.generateKeyPackage(credentialRef: credentialRef, lifetime: nil) - } + let keyPackage = try await generateKeyPackage(coreCrypto: alice) // remove the keypackage try await alice.transaction { ctx in @@ -831,14 +653,10 @@ final class WireCoreCryptoTests: XCTestCase { clientId: clientId ) - let alice = try await createCoreCrypto() + let alice = try await ccInit( + options: CcInitOptions.withoutBasicCredential(clientId: clientId)) try await alice.transaction { ctx in - try await ctx.mlsInit( - clientId: clientId, - transport: self.mockMlsTransport - - ) let cref1 = try await ctx.addCredential(credential: credential1) let cref2 = try await ctx.addCredential(credential: credential2) @@ -861,29 +679,22 @@ final class WireCoreCryptoTests: XCTestCase { } func testCanSetPkiEnvironment() async throws { - let root = FileManager.default.temporaryDirectory.appending(path: "mls") - let keystore = root.appending(path: "pki-\(UUID().uuidString)") - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - let database = try await Database.open(location: keystore.path, key: genDatabaseKey()) + let database = try await newDatabase() let pkiEnvironment = try await PkiEnvironment( hooks: MockPkiEnvironmentHooks(), database: database) let coreCrypto = try CoreCrypto(database: database) - try await coreCrypto.setPkiEnvironment(pkiEnvironment: pkiEnvironment) + await coreCrypto.setPkiEnvironment(pkiEnvironment: pkiEnvironment) let pkiEnvironment2 = await coreCrypto.getPkiEnvironment() XCTAssertNotNil(pkiEnvironment2) - try await coreCrypto.setPkiEnvironment(pkiEnvironment: nil) + await coreCrypto.setPkiEnvironment(pkiEnvironment: nil) let pkiEnvironment3 = await coreCrypto.getPkiEnvironment() XCTAssertNil(pkiEnvironment3) } func testCanAddTrustAnchor() async throws { - let root = FileManager.default.temporaryDirectory.appending(path: "mls") - let keystore = root.appending(path: "pki-trust-anchor-\(UUID().uuidString)") - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - let database = try await Database.open(location: keystore.path, key: genDatabaseKey()) - + let database = try await newDatabase() let pkiEnvironment = try await PkiEnvironment( hooks: MockPkiEnvironmentHooks(), database: database) @@ -895,11 +706,7 @@ final class WireCoreCryptoTests: XCTestCase { } func testCanAddIntermediateCert() async throws { - let root = FileManager.default.temporaryDirectory.appending(path: "mls") - let keystore = root.appending(path: "pki-intermediate-cert-\(UUID().uuidString)") - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - let database = try await Database.open(location: keystore.path, key: genDatabaseKey()) - + let database = try await newDatabase() let pkiEnvironment = try await PkiEnvironment( hooks: MockPkiEnvironmentHooks(), database: database) @@ -911,10 +718,7 @@ final class WireCoreCryptoTests: XCTestCase { } func testCanInstantiateX509CredentialAcquisition() async throws { - let root = FileManager.default.temporaryDirectory.appending(path: "mls") - let keystore = root.appending(path: "pki-\(UUID().uuidString)") - try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true) - let database = try await Database.open(location: keystore.path, key: genDatabaseKey()) + let database = try await newDatabase() let pkiEnvironment = try await PkiEnvironment( hooks: MockPkiEnvironmentHooks(), database: database) @@ -941,14 +745,11 @@ final class WireCoreCryptoTests: XCTestCase { func testParseJwkProducesSenderUsableInCreateConversation() async throws { let jwk = generateEd25519Jwk() let externalSender = try ExternalSender.parseJwk(jwk: jwk) - let alice = try await createClients("alice1")[0] + let alice = try await ccInit() let conversationId = ConversationId(bytes: Data("ext-sender-jwk".utf8)) let retrievedKey = try await alice.transaction { ctx -> ExternalSenderKey in - let credentialRef = try await ctx.findCredentials( - clientId: nil, publicKey: nil, ciphersuite: nil, - credentialType: nil, earliestValidity: nil - ).first! + let credentialRef = try await ctx.getCredentials().first! try await ctx.createConversation( conversationId: conversationId, credentialRef: credentialRef, diff --git a/crypto-ffi/src/database/mod.rs b/crypto-ffi/src/database/mod.rs index 30c12ccbbf..09d46ef376 100644 --- a/crypto-ffi/src/database/mod.rs +++ b/crypto-ffi/src/database/mod.rs @@ -12,11 +12,11 @@ use crate::{CoreCryptoError, CoreCryptoResult}; #[derive(Debug, derive_more::From, derive_more::Into, Clone, derive_more::Deref, uniffi::Object)] pub struct Database(core_crypto_keystore::Database); -#[cfg(any(feature = "wasm", feature = "napi"))] +// Note: no uniffi::export when not using ubrn, because static functions are not supported yet by uniffi version 0.29. #[cfg_attr(any(feature = "wasm", feature = "napi"), uniffi::export)] impl Database { /// Open or create a database. - #[uniffi::constructor(name = "open")] + #[cfg_attr(any(feature = "wasm", feature = "napi"), uniffi::constructor)] pub async fn open(location: &str, key: Arc) -> CoreCryptoResult { core_crypto_keystore::Database::open(core_crypto_keystore::ConnectionType::Persistent(location), key.as_ref()) .await @@ -25,7 +25,7 @@ impl Database { } /// Create an in-memory database whose data will be lost when the instance is dropped. - #[uniffi::constructor(name = "inMemory")] + #[cfg_attr(any(feature = "wasm", feature = "napi"), uniffi::constructor)] pub async fn in_memory(key: Arc) -> CoreCryptoResult { core_crypto_keystore::Database::open(core_crypto_keystore::ConnectionType::InMemory, key.as_ref()) .await @@ -34,24 +34,19 @@ impl Database { } } -// Note: no uniffi::export, because static functions are not supported yet by uniffi version 0.29. -#[cfg(not(any(feature = "wasm", feature = "napi")))] -impl Database { - /// Open or create a database. - pub async fn open(location: &str, key: Arc) -> CoreCryptoResult { - core_crypto_keystore::Database::open(core_crypto_keystore::ConnectionType::Persistent(location), key.as_ref()) - .await - .map(Database) - .map_err(CoreCryptoError::generic()) - } +// Note: free functions when not using ubrn. +/// Open or create a database. +#[cfg(not(any(feature = "wasm", feature = "napi", target_os = "unknown")))] +#[uniffi::export] +pub async fn open_database(location: &str, key: Arc) -> CoreCryptoResult { + Database::open(location, key).await +} - /// Create an in-memory database whose data will be lost when the instance is dropped. - pub async fn in_memory(key: Arc) -> CoreCryptoResult { - core_crypto_keystore::Database::open(core_crypto_keystore::ConnectionType::InMemory, key.as_ref()) - .await - .map(Database) - .map_err(CoreCryptoError::generic()) - } +/// Create an in-memory database whose data will be lost when the instance is dropped. +#[cfg(not(any(feature = "wasm", feature = "napi", target_os = "unknown")))] +#[uniffi::export] +pub async fn in_memory_database(key: Arc) -> CoreCryptoResult { + Database::in_memory(key).await } #[uniffi::export] @@ -82,20 +77,6 @@ impl Database { } } -/// Open or create a database. -#[cfg(not(any(feature = "wasm", target_os = "unknown")))] -#[uniffi::export] -pub async fn open_database(location: &str, key: Arc) -> CoreCryptoResult { - Database::open(location, key).await -} - -/// Create an in-memory database whose data will be lost when the instance is dropped. -#[cfg(not(any(feature = "wasm", target_os = "unknown")))] -#[uniffi::export] -pub async fn in_memory_database(key: Arc) -> CoreCryptoResult { - Database::in_memory(key).await -} - /// Export a fully vacuumed and optimized copy of the database to the specified path. /// /// The copy is created using SQLite's VACUUM INTO command and is encrypted with the same key diff --git a/crypto-ffi/src/e2ei/mod.rs b/crypto-ffi/src/e2ei/mod.rs index 8321fbeb05..41fb51f364 100644 --- a/crypto-ffi/src/e2ei/mod.rs +++ b/crypto-ffi/src/e2ei/mod.rs @@ -1,3 +1,5 @@ +pub(crate) mod x509_credential_acquisition_from_credential_ref; + use std::sync::Arc; use async_lock::Mutex; diff --git a/crypto-ffi/src/e2ei/x509_credential_acquisition_from_credential_ref.rs b/crypto-ffi/src/e2ei/x509_credential_acquisition_from_credential_ref.rs new file mode 100644 index 0000000000..2a73b0b078 --- /dev/null +++ b/crypto-ffi/src/e2ei/x509_credential_acquisition_from_credential_ref.rs @@ -0,0 +1,81 @@ +//! This entire module is temporary until our system decouples client identities from a client's public signature key. +//! See . + +use std::sync::Arc; + +use async_lock::Mutex; +use core_crypto::RecursiveError; + +use crate::{ + CoreCryptoError, CoreCryptoResult, CredentialRef, Database, PkiEnvironment, X509CredentialAcquisition, + X509CredentialAcquisitionConfiguration, e2ei::AcquisitionState, +}; + +#[cfg_attr(any(feature = "wasm", feature = "napi"), uniffi::export)] +impl X509CredentialAcquisition { + /// Create a new credential acquisition from an existing credential. This API is temporary until our system + /// decouples client identities from a client's public signature key. + /// See . + /// + /// Provide `core_crypto_database` if you're using distinct DB instances for `PkiEnvironment` and `CoreCrypto`. + /// Otherwise, the `PkiEnvironment`'s DB will be used to load the full credential. + #[cfg_attr(any(feature = "wasm", feature = "napi"), uniffi::constructor)] + pub async fn new_from_credential_ref( + pki_environment: Arc, + config: X509CredentialAcquisitionConfiguration, + credential_ref: &CredentialRef, + core_crypto_database: Option>, + ) -> CoreCryptoResult { + let ciphersuite = config.ciphersuite; + if ciphersuite != credential_ref.ciphersuite() { + return Err(CoreCryptoError::ad_hoc( + "config cipher suite doesn't match credential cipher suite", + )); + } + + let ffi_database = core_crypto_database + .map(|db| db.as_ref().clone()) + .unwrap_or(pki_environment.database()); + let database = core_crypto_keystore::Database::from(ffi_database); + let credential = credential_ref + .0 + .load(&database) + .await + .map_err(RecursiveError::mls_credential_ref("loading credential from ref"))?; + + let key_bytes = credential.signature_key_bytes(); + let algorithm = ciphersuite.try_into()?; + let pem = wire_e2e_identity::utils::pem_from_bytes(key_bytes, algorithm)?; + + let inner = wire_e2e_identity::X509CredentialAcquisition::try_new_from_pem( + pki_environment.clone_inner(), + config.try_into_core()?, + pem, + )?; + + Ok(Self { + state: Mutex::new(AcquisitionState::Initialized(inner.into())), + ciphersuite, + }) + } +} + +// Note: free function when not using ubrn. + +/// Create a new credential acquisition from an existing credential. This API is temporary until our system +/// decouples client identities from a client's public signature key. +/// See . +/// +/// Provide `core_crypto_database` if you're using distinct DB instances for `PkiEnvironment` and `CoreCrypto`. +/// Otherwise, the `PkiEnvironment`'s DB will be used to load the full credential. +#[cfg(not(any(feature = "wasm", feature = "napi", target_os = "unknown")))] +#[uniffi::export] +pub async fn x509_credential_acquisition_new_from_credential_ref( + pki_environment: Arc, + config: X509CredentialAcquisitionConfiguration, + credential_ref: &CredentialRef, + core_crypto_database: Option>, +) -> CoreCryptoResult { + X509CredentialAcquisition::new_from_credential_ref(pki_environment, config, credential_ref, core_crypto_database) + .await +} diff --git a/crypto-ffi/src/lib.rs b/crypto-ffi/src/lib.rs index e413e195bc..b19360ba4f 100644 --- a/crypto-ffi/src/lib.rs +++ b/crypto-ffi/src/lib.rs @@ -53,10 +53,14 @@ pub use core_crypto_context::CoreCryptoContext; pub use credential::Credential; pub use credential_ref::CredentialRef; pub use credential_type::CredentialType; -pub use database::{Database, DatabaseKey, migrate_database_key_type_to_bytes}; #[cfg(not(any(feature = "wasm", target_os = "unknown")))] -pub use database::{export_database_copy, in_memory_database, open_database}; +pub use database::export_database_copy; +pub use database::{Database, DatabaseKey, migrate_database_key_type_to_bytes}; +#[cfg(not(any(feature = "wasm", feature = "napi", target_os = "unknown")))] +pub use database::{in_memory_database, open_database}; pub use decrypted_message::{BufferedDecryptedMessage, DecryptedMessage}; +#[cfg(not(any(feature = "wasm", feature = "napi", target_os = "unknown")))] +pub use e2ei::x509_credential_acquisition_from_credential_ref::x509_credential_acquisition_new_from_credential_ref; pub use e2ei::{E2eiConversationState, X509CredentialAcquisition, X509CredentialAcquisitionConfiguration}; pub use ephemeral::{HistorySecret, core_crypto_history_client}; #[cfg(feature = "proteus")] diff --git a/crypto-ffi/src/pki_env.rs b/crypto-ffi/src/pki_env.rs index f80e7f3cdb..0278e03268 100644 --- a/crypto-ffi/src/pki_env.rs +++ b/crypto-ffi/src/pki_env.rs @@ -235,6 +235,10 @@ impl PkiEnvironment { pub(crate) fn clone_inner(&self) -> Arc { self.0.clone() } + + pub(crate) fn database(&self) -> Database { + self.0.database().clone().into() + } } #[cfg_attr(feature = "wasm", uniffi::export)] diff --git a/crypto/src/mls/credential/credential_ref/persistence.rs b/crypto/src/mls/credential/credential_ref/persistence.rs index baf7d8194d..570ce8e2ad 100644 --- a/crypto/src/mls/credential/credential_ref/persistence.rs +++ b/crypto/src/mls/credential/credential_ref/persistence.rs @@ -13,7 +13,7 @@ impl CredentialRef { /// Load the credential which matches this ref from the database. /// /// Note that this does not attach the credential to any Session; it just does the data manipulation. - pub(crate) async fn load(&self, database: &impl FetchFromDatabase) -> Result { + pub async fn load(&self, database: &impl FetchFromDatabase) -> Result { database .get::(&self.public_key_hash()) .await diff --git a/crypto/src/mls/credential/mod.rs b/crypto/src/mls/credential/mod.rs index 204e8f1899..2dd1d52b76 100644 --- a/crypto/src/mls/credential/mod.rs +++ b/crypto/src/mls/credential/mod.rs @@ -119,6 +119,12 @@ impl Credential { &self.signature_key_pair } + /// The signature key bytes. + // TODO temporary. Remove when https://wearezeta.atlassian.net/wiki/x/RABtrQ is resolved. + pub fn signature_key_bytes(&self) -> &[u8] { + self.signature_key_pair.private() + } + /// Get the signature scheme pub fn signature_scheme(&self) -> SignatureScheme { self.signature_key_pair.signature_scheme() diff --git a/e2e-identity/src/acquisition/initial.rs b/e2e-identity/src/acquisition/initial.rs index 3b374cd6d4..1d464f2238 100644 --- a/e2e-identity/src/acquisition/initial.rs +++ b/e2e-identity/src/acquisition/initial.rs @@ -1,5 +1,7 @@ use std::sync::Arc; +use rusty_jwt_tools::prelude::Pem; + use super::{X509CredentialAcquisition, X509CredentialConfiguration, states}; use crate::{ error::E2eIdentityResult, @@ -26,4 +28,27 @@ impl X509CredentialAcquisition { data: states::Initialized, }) } + + /// Create the acquisition object using an existing sign keypair. This API is temporary until our system decouples + /// client identities from a client's public signature key. + /// See . + // + // We're intentionally not factoring this into `try_new()`, so that this can be removed more cleanly. + pub fn try_new_from_pem( + pki_env: Arc, + config: X509CredentialConfiguration, + sign_kp: Pem, + ) -> E2eIdentityResult { + let acme_kp = generate_key(config.sign_alg)?; + let acme_jwk = public_jwk_from_pem_keypair(config.sign_alg, &acme_kp)?; + + Ok(Self { + pki_env, + config, + sign_kp, + acme_kp, + acme_jwk, + data: states::Initialized, + }) + } } diff --git a/e2e-identity/src/utils.rs b/e2e-identity/src/utils.rs index 696acb383e..10a03dfe5d 100644 --- a/e2e-identity/src/utils.rs +++ b/e2e-identity/src/utils.rs @@ -20,6 +20,16 @@ pub fn generate_key(sign_alg: JwsAlgorithm) -> E2eIdentityResult { Ok(pem.into()) } +pub fn pem_from_bytes(bytes: &[u8], sign_alg: JwsAlgorithm) -> E2eIdentityResult { + let pem = match sign_alg { + JwsAlgorithm::P256 => ES256KeyPair::from_bytes(bytes)?.to_pem()?, + JwsAlgorithm::P384 => ES384KeyPair::from_bytes(bytes)?.to_pem()?, + JwsAlgorithm::P521 => ES512KeyPair::from_bytes(bytes)?.to_pem()?, + JwsAlgorithm::Ed25519 => Ed25519KeyPair::from_bytes(bytes)?.to_pem(), + }; + Ok(pem.into()) +} + pub fn public_jwk_from_pem_keypair(alg: JwsAlgorithm, keypair: &Pem) -> E2eIdentityResult { let jwk = match alg { JwsAlgorithm::P256 => ES256KeyPair::from_pem(keypair)?.public_key().try_into_jwk()?,