UNPKG

ts-mls

Version:

[![CI](https://github.com/LukaJCB/ts-mls/actions/workflows/ci.yml/badge.svg)](https://github.com/LukaJCB/ts-mls/actions/workflows/ci.yml) [![npm version](https://badge.fury.io/js/ts-mls.svg)](https://badge.fury.io/js/ts-mls) [![Coverage Status](https://co

231 lines 11.8 kB
import { createGroup, joinGroup } from "../../src/clientState"; import { createCommit } from "../../src/createCommit"; import { emptyPskIndex } from "../../src/pskIndex"; import { ciphersuites, getCiphersuiteFromName, getCiphersuiteImpl } from "../../src/crypto/ciphersuite"; import { generateKeyPackage } from "../../src/keyPackage"; import { defaultLifetime } from "../../src/lifetime"; import { defaultCapabilities } from "../../src/defaultCapabilities"; import { CodecError, ValidationError } from "../../src/mlsError"; import { encodeRequiredCapabilities } from "../../src/requiredCapabilities"; import { encodeExternalSender } from "../../src/externalSender"; import { constantTimeEqual } from "../../src/util/constantTimeCompare"; import { createCustomCredential } from "../../src/customCredential"; for (const cs of Object.keys(ciphersuites)) { test(`Proposal Validation ${cs}`, async () => { await remove(cs); }); } async function remove(cipherSuite) { const impl = await getCiphersuiteImpl(getCiphersuiteFromName(cipherSuite)); const aliceCredential = { credentialType: "basic", identity: new TextEncoder().encode("alice") }; const alice = await generateKeyPackage(aliceCredential, defaultCapabilities(), defaultLifetime, [], impl); const groupId = new TextEncoder().encode("group1"); let aliceGroup = await createGroup(groupId, alice.publicPackage, alice.privatePackage, [], impl); const bobCredential = { credentialType: "basic", identity: new TextEncoder().encode("bob") }; const bob = await generateKeyPackage(bobCredential, defaultCapabilities(), defaultLifetime, [], impl); const charlieCredential = { credentialType: "basic", identity: new TextEncoder().encode("charlie") }; const charlie = await generateKeyPackage(charlieCredential, defaultCapabilities(), defaultLifetime, [], impl); const addBobProposal = { proposalType: "add", add: { keyPackage: bob.publicPackage, }, }; const addCharlieProposal = { proposalType: "add", add: { keyPackage: charlie.publicPackage, }, }; const addBobAndCharlieCommitResult = await createCommit(aliceGroup, emptyPskIndex, false, [addBobProposal, addCharlieProposal], impl); aliceGroup = addBobAndCharlieCommitResult.newState; let bobGroup = await joinGroup(addBobAndCharlieCommitResult.welcome, bob.publicPackage, bob.privatePackage, emptyPskIndex, impl, aliceGroup.ratchetTree); expect(bobGroup.keySchedule.epochAuthenticator).toStrictEqual(aliceGroup.keySchedule.epochAuthenticator); let charlieGroup = await joinGroup(addBobAndCharlieCommitResult.welcome, charlie.publicPackage, charlie.privatePackage, emptyPskIndex, impl, aliceGroup.ratchetTree); expect(charlieGroup.keySchedule.epochAuthenticator).toStrictEqual(aliceGroup.keySchedule.epochAuthenticator); const removeBobProposal = { proposalType: "remove", remove: { removed: bobGroup.privatePath.leafIndex, }, }; const removeBobProposal2 = { proposalType: "remove", remove: { removed: bobGroup.privatePath.leafIndex, }, }; // can't remove same leaf node twice await expect(createCommit(aliceGroup, emptyPskIndex, false, [removeBobProposal, removeBobProposal2], impl)).rejects.toThrow(ValidationError); // can't add someone already in the group await expect(createCommit(aliceGroup, emptyPskIndex, false, [addBobProposal], impl)).rejects.toThrow(ValidationError); const proposalInvalidRequiredCapabilities = { proposalType: "group_context_extensions", groupContextExtensions: { extensions: [{ extensionType: "required_capabilities", extensionData: new Uint8Array([1, 2]) }], }, }; //can't add groupContextExtensions with invalid requiredCapabilities await expect(createCommit(aliceGroup, emptyPskIndex, false, [proposalInvalidRequiredCapabilities], impl)).rejects.toThrow(CodecError); const proposalRequiredCapabilities = { proposalType: "group_context_extensions", groupContextExtensions: { extensions: [ { extensionType: "required_capabilities", extensionData: encodeRequiredCapabilities({ extensionTypes: [], proposalTypes: [99], credentialTypes: [] }), }, ], }, }; //can't add groupContextExtensions with requiredCapabilities that members don't support await expect(createCommit(aliceGroup, emptyPskIndex, false, [proposalRequiredCapabilities], impl)).rejects.toThrow(ValidationError); const dianaCredential = { credentialType: "basic", identity: new TextEncoder().encode("diana") }; const diana = await generateKeyPackage(dianaCredential, { ...defaultCapabilities(), credentials: ["basic"] }, defaultLifetime, [], impl); const addDiana = { proposalType: "add", add: { keyPackage: diana.publicPackage, }, }; const proposalRequiredCapabilitiesX509 = { proposalType: "group_context_extensions", groupContextExtensions: { extensions: [ { extensionType: "required_capabilities", extensionData: encodeRequiredCapabilities({ extensionTypes: [], proposalTypes: [], credentialTypes: ["x509"], }), }, ], }, }; //can't add groupContextExtensions with requiredCapabilities that newly added member doesn't support await expect(createCommit(aliceGroup, emptyPskIndex, false, [addDiana, proposalRequiredCapabilitiesX509], impl)).rejects.toThrow(ValidationError); const proposalInvalidExternalSenders = { proposalType: "group_context_extensions", groupContextExtensions: { extensions: [{ extensionType: "external_senders", extensionData: new Uint8Array([1, 2]) }], }, }; //can't add groupContextExtensions with invalid requiredCapabilities await expect(createCommit(aliceGroup, emptyPskIndex, false, [proposalInvalidExternalSenders], impl)).rejects.toThrow(CodecError); const badCredential = { credentialType: "basic", identity: new TextEncoder().encode("NOT GOOD") }; const proposalUnauthenticatedExternalSenders = { proposalType: "group_context_extensions", groupContextExtensions: { extensions: [ { extensionType: "external_senders", extensionData: encodeExternalSender({ credential: badCredential, signaturePublicKey: new Uint8Array() }), }, ], }, }; const authService = { async validateCredential(c, _pk) { if (c.credentialType === "basic" && constantTimeEqual(c.identity, badCredential.identity)) return false; return true; }, }; //can't add groupContextExtensions with external senders that can't be auth'd await expect(createCommit(withAuthService(aliceGroup, authService), emptyPskIndex, false, [proposalUnauthenticatedExternalSenders], impl)).rejects.toThrow(ValidationError); const edwardCredential = { credentialType: "basic", identity: new TextEncoder().encode("edward") }; const edward = await generateKeyPackage(edwardCredential, { ...defaultCapabilities(), credentials: ["basic"] }, defaultLifetime, [], impl); const addEdward = { proposalType: "add", add: { keyPackage: edward.publicPackage, }, }; const authServiceEdward = { async validateCredential(c, _pk) { if (c.credentialType === "basic" && constantTimeEqual(c.identity, edwardCredential.identity)) return false; return true; }, }; //can't add a member with invalid credentials await expect(createCommit(withAuthService(aliceGroup, authServiceEdward), emptyPskIndex, false, [addEdward], impl)).rejects.toThrow(ValidationError); const frankCredential = createCustomCredential(5, new Uint8Array([1, 2])); const frank = await generateKeyPackage(frankCredential, defaultCapabilities(), defaultLifetime, [], impl); const addFrank = { proposalType: "add", add: { keyPackage: frank.publicPackage }, }; //can't add leafNode with an unsupported credentialType await expect(createCommit(aliceGroup, emptyPskIndex, false, [addFrank], impl)).rejects.toThrow(ValidationError); const georgeCredential = { credentialType: "basic", identity: new TextEncoder().encode("george") }; const georgeExtension = { extensionType: 8545, extensionData: new Uint8Array() }; const george = await generateKeyPackage(georgeCredential, defaultCapabilities(), defaultLifetime, [georgeExtension], impl); const addGeorge = { proposalType: "add", add: { keyPackage: george.publicPackage }, }; //can't add leafNode with an unsupported extension await expect(createCommit(aliceGroup, emptyPskIndex, false, [addGeorge], impl)).rejects.toThrow(ValidationError); const updateLeafNode = { leafNodeSource: "update", signaturePublicKey: alice.publicPackage.leafNode.signaturePublicKey, hpkePublicKey: alice.publicPackage.leafNode.hpkePublicKey, credential: alice.publicPackage.leafNode.credential, capabilities: alice.publicPackage.leafNode.capabilities, extensions: alice.publicPackage.leafNode.extensions, signature: new Uint8Array(), }; const updateProposal = { proposalType: "update", update: { leafNode: updateLeafNode, }, }; // commiter can't update themselves await expect(createCommit(aliceGroup, emptyPskIndex, false, [updateProposal], impl)).rejects.toThrow(ValidationError); const removeProposal = { proposalType: "remove", remove: { removed: 0, }, }; // committer can't remove themselves await expect(createCommit(aliceGroup, emptyPskIndex, false, [removeProposal], impl)).rejects.toThrow(ValidationError); const hannahCredential = { credentialType: "basic", identity: new TextEncoder().encode("bob") }; const hannah = await generateKeyPackage(hannahCredential, defaultCapabilities(), defaultLifetime, [], impl); const addHannahProposal = { proposalType: "add", add: { keyPackage: hannah.publicPackage, }, }; // can't add the same keypackage twice await expect(createCommit(aliceGroup, emptyPskIndex, false, [addHannahProposal, addHannahProposal], impl)).rejects.toThrow(ValidationError); const pskId = new Uint8Array([1, 2, 3, 4]); const pskProposal = { proposalType: "psk", psk: { preSharedKeyId: { psktype: "external", pskId, pskNonce: new Uint8Array([5, 6, 7, 8]), }, }, }; // can't reference the same psk in multiple proposals await expect(createCommit(aliceGroup, emptyPskIndex, false, [pskProposal, pskProposal], impl)).rejects.toThrow(ValidationError); const groupContextExtensionsProposal = { proposalType: "group_context_extensions", groupContextExtensions: { extensions: [], }, }; // can't use multiple group_context_extensions proposals await expect(createCommit(aliceGroup, emptyPskIndex, false, [groupContextExtensionsProposal, groupContextExtensionsProposal], impl)).rejects.toThrow(ValidationError); } function withAuthService(state, authService) { return { ...state, clientConfig: { ...state.clientConfig, authService: authService } }; } //# sourceMappingURL=proposalValidation.test.js.map