Add extra notary signature checks to zkgroup

Co-authored-by: Fedor Indutny <79877362+indutny-signal@users.noreply.github.com>
This commit is contained in:
automated-signal 2022-03-08 16:06:16 -08:00 committed by GitHub
parent ec8824caa8
commit 2e58e77bd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 86 additions and 23 deletions

View File

@ -48,6 +48,7 @@ import {
getClientZkAuthOperations,
getClientZkGroupCipher,
getClientZkProfileOperations,
verifyNotarySignature,
} from './util/zkgroup';
import {
computeHash,
@ -71,6 +72,7 @@ import * as Bytes from './Bytes';
import type { AvatarDataType } from './types/Avatar';
import { UUID, isValidUuid } from './types/UUID';
import type { UUIDStringType } from './types/UUID';
import * as Errors from './types/errors';
import { SignalService as Proto } from './protobuf';
import {
@ -1294,7 +1296,10 @@ export async function modifyGroupV2({
// change conversation state and add change notifications to the timeline.
await window.Signal.Groups.maybeUpdateGroup({
conversation,
groupChangeBase64,
groupChange: {
base64: groupChangeBase64,
isTrusted: true,
},
newRevision,
});
@ -1743,13 +1748,18 @@ export function maybeDeriveGroupV2Id(conversation: ConversationModel): boolean {
return true;
}
type MigratePropsType = {
type WrappedGroupChangeType = Readonly<{
base64: string;
isTrusted: boolean;
}>;
type MigratePropsType = Readonly<{
conversation: ConversationModel;
groupChangeBase64?: string;
newRevision?: number;
receivedAt?: number;
sentAt?: number;
};
groupChange?: WrappedGroupChangeType;
}>;
export async function isGroupEligibleToMigrate(
conversation: ConversationModel
@ -2333,7 +2343,7 @@ export async function joinGroupV2ViaLinkAndMigrate({
// the log endpoint - the parameters beyond conversation are needed in that scenario.
export async function respondToGroupV2Migration({
conversation,
groupChangeBase64,
groupChange,
newRevision,
receivedAt,
sentAt,
@ -2569,7 +2579,7 @@ export async function respondToGroupV2Migration({
// group update codepaths.
await maybeUpdateGroup({
conversation,
groupChangeBase64,
groupChange,
newRevision,
receivedAt,
sentAt,
@ -2578,15 +2588,15 @@ export async function respondToGroupV2Migration({
// Fetching and applying group changes
type MaybeUpdatePropsType = {
type MaybeUpdatePropsType = Readonly<{
conversation: ConversationModel;
groupChangeBase64?: string;
newRevision?: number;
receivedAt?: number;
sentAt?: number;
dropInitialJoinMessage?: boolean;
force?: boolean;
};
groupChange?: WrappedGroupChangeType;
}>;
const FIVE_MINUTES = 5 * durations.MINUTE;
@ -2640,7 +2650,7 @@ export async function maybeUpdateGroup(
{
conversation,
dropInitialJoinMessage,
groupChangeBase64,
groupChange,
newRevision,
receivedAt,
sentAt,
@ -2657,7 +2667,7 @@ export async function maybeUpdateGroup(
group: conversation.attributes,
serverPublicParamsBase64: window.getServerPublicParams(),
newRevision,
groupChangeBase64,
groupChange,
dropInitialJoinMessage,
});
@ -2809,19 +2819,21 @@ async function updateGroup(
// 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({
dropInitialJoinMessage,
group,
serverPublicParamsBase64,
newRevision,
groupChangeBase64,
}: {
dropInitialJoinMessage?: boolean;
group: ConversationAttributesType;
groupChangeBase64?: string;
newRevision?: number;
serverPublicParamsBase64: string;
}): Promise<UpdatesResultType> {
groupChange: wrappedGroupChange,
}: GetGroupUpdatesType): Promise<UpdatesResultType> {
const logId = idForLogging(group.groupId);
log.info(`getGroupUpdates/${logId}: Starting...`);
@ -2841,18 +2853,44 @@ async function getGroupUpdates({
if (
window.GV2_ENABLE_SINGLE_CHANGE_PROCESSING &&
groupChangeBase64 &&
wrappedGroupChange &&
isNumber(newRevision) &&
(isInitialCreationMessage || weAreAwaitingApproval || isOneVersionUp)
) {
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 isChangeSupported =
!isNumber(groupChange.changeEpoch) ||
groupChange.changeEpoch <= SUPPORTED_CHANGE_EPOCH;
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({
group,
newRevision,

View File

@ -2333,7 +2333,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
const { revision, groupChange } = initialMessage.groupV2;
await window.Signal.Groups.respondToGroupV2Migration({
conversation,
groupChangeBase64: groupChange,
groupChange: groupChange
? {
base64: groupChange,
isTrusted: false,
}
: undefined,
newRevision: revision,
receivedAt: message.get('received_at'),
sentAt: message.get('sent_at'),
@ -2363,7 +2368,12 @@ export class MessageModel extends window.Backbone.Model<MessageAttributesType> {
try {
await window.Signal.Groups.maybeUpdateGroup({
conversation,
groupChangeBase64: groupChange,
groupChange: groupChange
? {
base64: groupChange,
isTrusted: false,
}
: undefined,
newRevision: revision,
receivedAt: message.get('received_at'),
sentAt: message.get('sent_at'),

View File

@ -16,6 +16,7 @@ import {
ProfileKeyCredentialResponse,
ServerPublicParams,
UuidCiphertext,
NotarySignature,
} from '@signalapp/signal-client/zkgroup';
import { UUID } from '../types/UUID';
import type { UUIDStringType } from '../types/UUID';
@ -256,3 +257,17 @@ export function deriveProfileKeyCommitment(
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);
}