Add extra notary signature checks to zkgroup
This commit is contained in:
parent
944d60f40b
commit
feef67da5a
3 changed files with 86 additions and 23 deletions
80
ts/groups.ts
80
ts/groups.ts
|
@ -48,6 +48,7 @@ import {
|
||||||
getClientZkAuthOperations,
|
getClientZkAuthOperations,
|
||||||
getClientZkGroupCipher,
|
getClientZkGroupCipher,
|
||||||
getClientZkProfileOperations,
|
getClientZkProfileOperations,
|
||||||
|
verifyNotarySignature,
|
||||||
} from './util/zkgroup';
|
} from './util/zkgroup';
|
||||||
import {
|
import {
|
||||||
computeHash,
|
computeHash,
|
||||||
|
@ -71,6 +72,7 @@ import * as Bytes from './Bytes';
|
||||||
import type { AvatarDataType } from './types/Avatar';
|
import type { AvatarDataType } from './types/Avatar';
|
||||||
import { UUID, isValidUuid } from './types/UUID';
|
import { UUID, isValidUuid } from './types/UUID';
|
||||||
import type { UUIDStringType } from './types/UUID';
|
import type { UUIDStringType } from './types/UUID';
|
||||||
|
import * as Errors from './types/errors';
|
||||||
import { SignalService as Proto } from './protobuf';
|
import { SignalService as Proto } from './protobuf';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -1274,7 +1276,10 @@ export async function modifyGroupV2({
|
||||||
// change conversation state and add change notifications to the timeline.
|
// change conversation state and add change notifications to the timeline.
|
||||||
await window.Signal.Groups.maybeUpdateGroup({
|
await window.Signal.Groups.maybeUpdateGroup({
|
||||||
conversation,
|
conversation,
|
||||||
groupChangeBase64,
|
groupChange: {
|
||||||
|
base64: groupChangeBase64,
|
||||||
|
isTrusted: true,
|
||||||
|
},
|
||||||
newRevision,
|
newRevision,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1711,13 +1716,18 @@ export function maybeDeriveGroupV2Id(conversation: ConversationModel): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MigratePropsType = {
|
type WrappedGroupChangeType = Readonly<{
|
||||||
|
base64: string;
|
||||||
|
isTrusted: boolean;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type MigratePropsType = Readonly<{
|
||||||
conversation: ConversationModel;
|
conversation: ConversationModel;
|
||||||
groupChangeBase64?: string;
|
|
||||||
newRevision?: number;
|
newRevision?: number;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
sentAt?: number;
|
sentAt?: number;
|
||||||
};
|
groupChange?: WrappedGroupChangeType;
|
||||||
|
}>;
|
||||||
|
|
||||||
export async function isGroupEligibleToMigrate(
|
export async function isGroupEligibleToMigrate(
|
||||||
conversation: ConversationModel
|
conversation: ConversationModel
|
||||||
|
@ -2286,7 +2296,7 @@ export async function joinGroupV2ViaLinkAndMigrate({
|
||||||
// the log endpoint - the parameters beyond conversation are needed in that scenario.
|
// the log endpoint - the parameters beyond conversation are needed in that scenario.
|
||||||
export async function respondToGroupV2Migration({
|
export async function respondToGroupV2Migration({
|
||||||
conversation,
|
conversation,
|
||||||
groupChangeBase64,
|
groupChange,
|
||||||
newRevision,
|
newRevision,
|
||||||
receivedAt,
|
receivedAt,
|
||||||
sentAt,
|
sentAt,
|
||||||
|
@ -2522,7 +2532,7 @@ export async function respondToGroupV2Migration({
|
||||||
// group update codepaths.
|
// group update codepaths.
|
||||||
await maybeUpdateGroup({
|
await maybeUpdateGroup({
|
||||||
conversation,
|
conversation,
|
||||||
groupChangeBase64,
|
groupChange,
|
||||||
newRevision,
|
newRevision,
|
||||||
receivedAt,
|
receivedAt,
|
||||||
sentAt,
|
sentAt,
|
||||||
|
@ -2531,15 +2541,15 @@ export async function respondToGroupV2Migration({
|
||||||
|
|
||||||
// Fetching and applying group changes
|
// Fetching and applying group changes
|
||||||
|
|
||||||
type MaybeUpdatePropsType = {
|
type MaybeUpdatePropsType = Readonly<{
|
||||||
conversation: ConversationModel;
|
conversation: ConversationModel;
|
||||||
groupChangeBase64?: string;
|
|
||||||
newRevision?: number;
|
newRevision?: number;
|
||||||
receivedAt?: number;
|
receivedAt?: number;
|
||||||
sentAt?: number;
|
sentAt?: number;
|
||||||
dropInitialJoinMessage?: boolean;
|
dropInitialJoinMessage?: boolean;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
};
|
groupChange?: WrappedGroupChangeType;
|
||||||
|
}>;
|
||||||
|
|
||||||
const FIVE_MINUTES = 5 * durations.MINUTE;
|
const FIVE_MINUTES = 5 * durations.MINUTE;
|
||||||
|
|
||||||
|
@ -2593,7 +2603,7 @@ export async function maybeUpdateGroup(
|
||||||
{
|
{
|
||||||
conversation,
|
conversation,
|
||||||
dropInitialJoinMessage,
|
dropInitialJoinMessage,
|
||||||
groupChangeBase64,
|
groupChange,
|
||||||
newRevision,
|
newRevision,
|
||||||
receivedAt,
|
receivedAt,
|
||||||
sentAt,
|
sentAt,
|
||||||
|
@ -2610,7 +2620,7 @@ export async function maybeUpdateGroup(
|
||||||
group: conversation.attributes,
|
group: conversation.attributes,
|
||||||
serverPublicParamsBase64: window.getServerPublicParams(),
|
serverPublicParamsBase64: window.getServerPublicParams(),
|
||||||
newRevision,
|
newRevision,
|
||||||
groupChangeBase64,
|
groupChange,
|
||||||
dropInitialJoinMessage,
|
dropInitialJoinMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2762,19 +2772,21 @@ async function updateGroup(
|
||||||
// No need for convo.updateLastMessage(), 'newmessage' handler does that
|
// No need for convo.updateLastMessage(), 'newmessage' handler does that
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GetGroupUpdatesType = Readonly<{
|
||||||
|
dropInitialJoinMessage?: boolean;
|
||||||
|
group: ConversationAttributesType;
|
||||||
|
serverPublicParamsBase64: string;
|
||||||
|
newRevision?: number;
|
||||||
|
groupChange?: WrappedGroupChangeType;
|
||||||
|
}>;
|
||||||
|
|
||||||
async function getGroupUpdates({
|
async function getGroupUpdates({
|
||||||
dropInitialJoinMessage,
|
dropInitialJoinMessage,
|
||||||
group,
|
group,
|
||||||
serverPublicParamsBase64,
|
serverPublicParamsBase64,
|
||||||
newRevision,
|
newRevision,
|
||||||
groupChangeBase64,
|
groupChange: wrappedGroupChange,
|
||||||
}: {
|
}: GetGroupUpdatesType): Promise<UpdatesResultType> {
|
||||||
dropInitialJoinMessage?: boolean;
|
|
||||||
group: ConversationAttributesType;
|
|
||||||
groupChangeBase64?: string;
|
|
||||||
newRevision?: number;
|
|
||||||
serverPublicParamsBase64: string;
|
|
||||||
}): Promise<UpdatesResultType> {
|
|
||||||
const logId = idForLogging(group.groupId);
|
const logId = idForLogging(group.groupId);
|
||||||
|
|
||||||
log.info(`getGroupUpdates/${logId}: Starting...`);
|
log.info(`getGroupUpdates/${logId}: Starting...`);
|
||||||
|
@ -2794,18 +2806,44 @@ async function getGroupUpdates({
|
||||||
|
|
||||||
if (
|
if (
|
||||||
window.GV2_ENABLE_SINGLE_CHANGE_PROCESSING &&
|
window.GV2_ENABLE_SINGLE_CHANGE_PROCESSING &&
|
||||||
groupChangeBase64 &&
|
wrappedGroupChange &&
|
||||||
isNumber(newRevision) &&
|
isNumber(newRevision) &&
|
||||||
(isInitialCreationMessage || weAreAwaitingApproval || isOneVersionUp)
|
(isInitialCreationMessage || weAreAwaitingApproval || isOneVersionUp)
|
||||||
) {
|
) {
|
||||||
log.info(`getGroupUpdates/${logId}: Processing just one change`);
|
log.info(`getGroupUpdates/${logId}: Processing just one change`);
|
||||||
const groupChangeBuffer = Bytes.fromBase64(groupChangeBase64);
|
|
||||||
|
const groupChangeBuffer = Bytes.fromBase64(wrappedGroupChange.base64);
|
||||||
const groupChange = Proto.GroupChange.decode(groupChangeBuffer);
|
const groupChange = Proto.GroupChange.decode(groupChangeBuffer);
|
||||||
const isChangeSupported =
|
const isChangeSupported =
|
||||||
!isNumber(groupChange.changeEpoch) ||
|
!isNumber(groupChange.changeEpoch) ||
|
||||||
groupChange.changeEpoch <= SUPPORTED_CHANGE_EPOCH;
|
groupChange.changeEpoch <= SUPPORTED_CHANGE_EPOCH;
|
||||||
|
|
||||||
if (isChangeSupported) {
|
if (isChangeSupported) {
|
||||||
|
if (!wrappedGroupChange.isTrusted) {
|
||||||
|
strictAssert(
|
||||||
|
groupChange.serverSignature && groupChange.actions,
|
||||||
|
'Server signature must be present in untrusted group change'
|
||||||
|
);
|
||||||
|
try {
|
||||||
|
verifyNotarySignature(
|
||||||
|
serverPublicParamsBase64,
|
||||||
|
groupChange.actions,
|
||||||
|
groupChange.serverSignature
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
log.warn(
|
||||||
|
`getGroupUpdates/${logId}: verifyNotarySignature failed, ` +
|
||||||
|
'dropping the message',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
newAttributes: group,
|
||||||
|
groupChangeMessages: [],
|
||||||
|
members: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return updateGroupViaSingleChange({
|
return updateGroupViaSingleChange({
|
||||||
group,
|
group,
|
||||||
newRevision,
|
newRevision,
|
||||||
|
|
|
@ -2053,7 +2053,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
const { revision, groupChange } = initialMessage.groupV2;
|
const { revision, groupChange } = initialMessage.groupV2;
|
||||||
await window.Signal.Groups.respondToGroupV2Migration({
|
await window.Signal.Groups.respondToGroupV2Migration({
|
||||||
conversation,
|
conversation,
|
||||||
groupChangeBase64: groupChange,
|
groupChange: groupChange
|
||||||
|
? {
|
||||||
|
base64: groupChange,
|
||||||
|
isTrusted: false,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
newRevision: revision,
|
newRevision: revision,
|
||||||
receivedAt: message.get('received_at'),
|
receivedAt: message.get('received_at'),
|
||||||
sentAt: message.get('sent_at'),
|
sentAt: message.get('sent_at'),
|
||||||
|
@ -2083,7 +2088,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
|
||||||
try {
|
try {
|
||||||
await window.Signal.Groups.maybeUpdateGroup({
|
await window.Signal.Groups.maybeUpdateGroup({
|
||||||
conversation,
|
conversation,
|
||||||
groupChangeBase64: groupChange,
|
groupChange: groupChange
|
||||||
|
? {
|
||||||
|
base64: groupChange,
|
||||||
|
isTrusted: false,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
newRevision: revision,
|
newRevision: revision,
|
||||||
receivedAt: message.get('received_at'),
|
receivedAt: message.get('received_at'),
|
||||||
sentAt: message.get('sent_at'),
|
sentAt: message.get('sent_at'),
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
ProfileKeyCredentialResponse,
|
ProfileKeyCredentialResponse,
|
||||||
ServerPublicParams,
|
ServerPublicParams,
|
||||||
UuidCiphertext,
|
UuidCiphertext,
|
||||||
|
NotarySignature,
|
||||||
} from '@signalapp/signal-client/zkgroup';
|
} from '@signalapp/signal-client/zkgroup';
|
||||||
import { UUID } from '../types/UUID';
|
import { UUID } from '../types/UUID';
|
||||||
import type { UUIDStringType } from '../types/UUID';
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
|
@ -256,3 +257,17 @@ export function deriveProfileKeyCommitment(
|
||||||
|
|
||||||
return profileKey.getCommitment(uuid).contents.toString('base64');
|
return profileKey.getCommitment(uuid).contents.toString('base64');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function verifyNotarySignature(
|
||||||
|
serverPublicParamsBase64: string,
|
||||||
|
message: Uint8Array,
|
||||||
|
signature: Uint8Array
|
||||||
|
): void {
|
||||||
|
const serverPublicParams = new ServerPublicParams(
|
||||||
|
Buffer.from(serverPublicParamsBase64, 'base64')
|
||||||
|
);
|
||||||
|
|
||||||
|
const notarySignature = new NotarySignature(Buffer.from(signature));
|
||||||
|
|
||||||
|
serverPublicParams.verifySignature(Buffer.from(message), notarySignature);
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue