Skip to content

Commit 1cc6775

Browse files
committed
chore: verify signatures when applying updates (wip)
1 parent 73d3752 commit 1cc6775

File tree

3 files changed

+81
-16
lines changed

3 files changed

+81
-16
lines changed

packages/hypergraph-react/src/HypergraphAppContext.tsx

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import * as automerge from '@automerge/automerge';
44
import { uuid } from '@automerge/automerge';
55
import { RepoContext } from '@automerge/automerge-repo-react-hooks';
66
import { Identity, Key, Messages, SpaceEvents, type SpaceStorageEntry, Utils, store } from '@graphprotocol/hypergraph';
7-
import { canonicalize } from '@graphprotocol/hypergraph/utils/jsc';
87
import { useSelector as useSelectorStore } from '@xstate/store/react';
98
import { Effect, Exit } from 'effect';
109
import * as Schema from 'effect/Schema';
@@ -536,18 +535,41 @@ export function HypergraphAppProvider({
536535
}
537536

538537
if (response.updates) {
539-
const updates = response.updates?.updates.map((update) => {
540-
return Messages.decryptMessage({
541-
nonceAndCiphertext: update,
542-
secretKey: Utils.hexToBytes(keys[0].key),
538+
const updates = response.updates?.updates.map(async (update) => {
539+
// TODO verify the update signature and that the signing key
540+
// belongs to the reported accountId
541+
const signer = Messages.recoverUpdateMessageSigner({
542+
update: update.update,
543+
spaceId: response.id,
544+
ephemeralId: update.ephemeralId,
545+
signature: update.signature,
546+
accountId: update.accountId,
543547
});
548+
const authorIdentity = await getUserIdentity(update.accountId);
549+
if (authorIdentity.signaturePublicKey !== signer) {
550+
console.error(
551+
`Received invalid signature, recovered signer is ${signer},
552+
expected ${authorIdentity.signaturePublicKey}`,
553+
);
554+
return { valid: false, update: new Uint8Array([]) };
555+
}
556+
return {
557+
valid: true,
558+
update: Messages.decryptMessage({
559+
nonceAndCiphertext: update.update,
560+
secretKey: Utils.hexToBytes(keys[0].key),
561+
}),
562+
};
544563
});
545564

546-
for (const update of updates) {
547-
automergeDocHandle.update((existingDoc) => {
548-
const [newDoc] = automerge.applyChanges(existingDoc, [update]);
549-
return newDoc;
550-
});
565+
for (const updatePromise of updates) {
566+
const update = await updatePromise;
567+
if (update.valid) {
568+
automergeDocHandle.update((existingDoc) => {
569+
const [newDoc] = automerge.applyChanges(existingDoc, [update.update]);
570+
return newDoc;
571+
});
572+
}
551573
}
552574

553575
store.send({

packages/hypergraph/src/messages/signed-update-message.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { secp256k1 } from '@noble/curves/secp256k1';
2-
import { canonicalize, hexToBytes, stringToUint8Array } from '../utils/index.js';
2+
import { sha256 } from '@noble/hashes/sha256';
3+
import { bytesToHex, canonicalize, hexToBytes, stringToUint8Array } from '../utils/index.js';
34
import { encryptMessage } from './encrypt-message.js';
45
import type { RequestCreateUpdate } from './types.js';
56

6-
interface Params {
7+
interface SignedMessageParams {
78
accountId: string;
89
ephemeralId: string;
910
spaceId: string;
@@ -12,14 +13,25 @@ interface Params {
1213
signaturePrivateKey: string;
1314
}
1415

16+
interface RecoverParams {
17+
update: Uint8Array;
18+
spaceId: string;
19+
ephemeralId: string;
20+
signature: {
21+
hex: string;
22+
recovery: number;
23+
};
24+
accountId: string;
25+
}
26+
1527
export const signedUpdateMessage = ({
1628
accountId,
1729
ephemeralId,
1830
spaceId,
1931
message,
2032
secretKey,
2133
signaturePrivateKey,
22-
}: Params): RequestCreateUpdate => {
34+
}: SignedMessageParams): RequestCreateUpdate => {
2335
const update = encryptMessage({
2436
message,
2537
secretKey: hexToBytes(secretKey),
@@ -34,7 +46,12 @@ export const signedUpdateMessage = ({
3446
}),
3547
);
3648

37-
const signature = secp256k1.sign(messageToSign, hexToBytes(signaturePrivateKey), { prehash: true }).toCompactHex();
49+
const recoverySignature = secp256k1.sign(messageToSign, hexToBytes(signaturePrivateKey), { prehash: true });
50+
51+
const signature = {
52+
hex: recoverySignature.toCompactHex(),
53+
recovery: recoverySignature.recovery,
54+
};
3855

3956
return {
4057
type: 'create-update',
@@ -45,3 +62,17 @@ export const signedUpdateMessage = ({
4562
signature,
4663
};
4764
};
65+
66+
export const recoverUpdateMessageSigner = ({ update, spaceId, ephemeralId, signature, accountId }: RecoverParams) => {
67+
const recoveredSignature = secp256k1.Signature.fromCompact(signature.hex).addRecoveryBit(signature.recovery);
68+
const signedMessage = stringToUint8Array(
69+
canonicalize({
70+
accountId,
71+
ephemeralId,
72+
update,
73+
spaceId,
74+
}),
75+
);
76+
const signedMessageHash = sha256(signedMessage);
77+
return bytesToHex(recoveredSignature.recoverPublicKey(signedMessageHash).toRawBytes(true));
78+
};

packages/hypergraph/src/messages/types.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,20 @@ import * as Schema from 'effect/Schema';
22

33
import { AcceptInvitationEvent, CreateInvitationEvent, CreateSpaceEvent, SpaceEvent } from '../space-events/index.js';
44

5+
export const SignatureWithRecovery = Schema.Struct({
6+
hex: Schema.String,
7+
recovery: Schema.Number,
8+
});
9+
10+
export const SignedUpdate = Schema.Struct({
11+
update: Schema.Uint8Array,
12+
accountId: Schema.String,
13+
signature: SignatureWithRecovery,
14+
ephemeralId: Schema.String,
15+
});
16+
517
export const Updates = Schema.Struct({
6-
updates: Schema.Array(Schema.Uint8Array),
18+
updates: Schema.Array(SignedUpdate),
719
firstUpdateClock: Schema.Number,
820
lastUpdateClock: Schema.Number,
921
});
@@ -87,7 +99,7 @@ export const RequestCreateUpdate = Schema.Struct({
8799
update: Schema.Uint8Array,
88100
spaceId: Schema.String,
89101
ephemeralId: Schema.String, // used to identify the confirmation message
90-
signature: Schema.String,
102+
signature: SignatureWithRecovery,
91103
});
92104

93105
export const RequestMessage = Schema.Union(

0 commit comments

Comments
 (0)