Skip to content

Commit a75fa47

Browse files
committed
[seal] handle key server v2 with server mode, committee mode goes to aggregator URL
1 parent e2a800f commit a75fa47

File tree

9 files changed

+401
-38
lines changed

9 files changed

+401
-38
lines changed

.changeset/dull-baboons-deny.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@mysten/seal': minor
3+
---
4+
5+
Handle key server v2 and aggregator for a committee of key servers

packages/seal/src/bcs.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,45 @@ export const KeyServerMoveV1 = bcs.struct('KeyServerV1', {
5151
pk: bcs.byteVector(),
5252
});
5353

54+
/**
55+
* The Move struct for PartialKeyServer.
56+
*/
57+
export const PartialKeyServer = bcs.struct('PartialKeyServer', {
58+
name: bcs.string(),
59+
url: bcs.string(),
60+
partialPk: bcs.byteVector(),
61+
partyId: bcs.u16(),
62+
});
63+
64+
/**
65+
* The Move enum for ServerType (V2).
66+
*/
67+
export const ServerType = bcs.enum('ServerType', {
68+
Independent: bcs.struct('Independent', {
69+
url: bcs.string(),
70+
}),
71+
Committee: bcs.struct('Committee', {
72+
version: bcs.u32(),
73+
threshold: bcs.u16(),
74+
partialKeyServers: bcs.vector(
75+
bcs.struct('VecMapEntry', {
76+
key: bcs.Address,
77+
value: PartialKeyServer,
78+
}),
79+
),
80+
}),
81+
});
82+
83+
/**
84+
* The Move struct for the KeyServerV2 object.
85+
*/
86+
export const KeyServerMoveV2 = bcs.struct('KeyServerV2', {
87+
name: bcs.string(),
88+
keyType: bcs.u8(),
89+
pk: bcs.byteVector(),
90+
serverType: ServerType,
91+
});
92+
5493
/**
5594
* The Move struct for the parent object.
5695
*/

packages/seal/src/client.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ export class SealClient {
262262
await retrieveKeyServers({
263263
objectIds: missingKeyServers,
264264
client: this.#suiClient,
265+
configs: this.#configs,
265266
})
266267
).forEach((keyServer) =>
267268
this.#cachedPublicKeys.set(keyServer.objectId, G2Element.fromBytes(keyServer.pk)),
@@ -295,8 +296,9 @@ export class SealClient {
295296

296297
async #loadKeyServers(): Promise<Map<string, KeyServer>> {
297298
const keyServers = await retrieveKeyServers({
298-
objectIds: [...this.#configs].map(([objectId]) => objectId),
299+
objectIds: [...this.#configs.keys()],
299300
client: this.#suiClient,
301+
configs: this.#configs,
300302
});
301303

302304
if (keyServers.length === 0) {
@@ -306,6 +308,10 @@ export class SealClient {
306308
if (this.#verifyKeyServers) {
307309
await Promise.all(
308310
keyServers.map(async (server) => {
311+
// Skip /service verification for committee key server type since the request goes through an aggregator.
312+
if (server.serverType === 'Committee') {
313+
return;
314+
}
309315
const config = this.#configs.get(server.objectId);
310316
if (!(await verifyKeyServer(server, this.#timeout, config?.apiKeyName, config?.apiKey))) {
311317
throw new InvalidKeyServerError(`Key server ${server.objectId} is not valid`);

packages/seal/src/key-server.ts

Lines changed: 91 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,35 @@
33
import { bcs, fromBase64, fromHex, toBase64, toHex } from '@mysten/bcs';
44
import { bls12_381 } from '@noble/curves/bls12-381';
55

6-
import { KeyServerMove, KeyServerMoveV1 } from './bcs.js';
7-
import { InvalidKeyServerError, InvalidKeyServerVersionError, SealAPIError } from './error.js';
6+
import { KeyServerMove, KeyServerMoveV1, KeyServerMoveV2 } from './bcs.js';
7+
import {
8+
InvalidClientOptionsError,
9+
InvalidKeyServerError,
10+
InvalidKeyServerVersionError,
11+
SealAPIError,
12+
} from './error.js';
813
import { DST_POP } from './ibe.js';
914
import { PACKAGE_VERSION } from './version.js';
10-
import type { SealCompatibleClient } from './types.js';
15+
import type { KeyServerConfig, SealCompatibleClient } from './types.js';
1116
import type { G1Element } from './bls12381.js';
1217
import { flatten, Version } from './utils.js';
1318
import { elgamalDecrypt } from './elgamal.js';
1419
import type { Certificate } from './session-key.js';
1520

16-
const EXPECTED_SERVER_VERSION = 1;
21+
const SUPPORTED_SERVER_VERSIONS = [2, 1]; // Must be configured in descending order.
22+
23+
export type ServerType = 'Independent' | 'Committee';
1724

1825
export type KeyServer = {
1926
objectId: string;
2027
name: string;
2128
url: string;
22-
keyType: KeyServerType;
29+
keyType: KeyType;
2330
pk: Uint8Array<ArrayBuffer>;
31+
serverType: ServerType;
2432
};
2533

26-
export enum KeyServerType {
34+
export enum KeyType {
2735
BonehFranklinBLS12381 = 0,
2836
}
2937

@@ -33,16 +41,22 @@ export const SERVER_VERSION_REQUIREMENT = new Version('0.4.1');
3341
* Given a list of key server object IDs, returns a list of SealKeyServer
3442
* from onchain state containing name, objectId, URL and pk.
3543
*
44+
* Supports both V1 (independent servers) and V2 (independent + committee servers).
45+
* For V2 committee servers, returns the aggregator URL from the config.
46+
*
3647
* @param objectIds - The key server object IDs.
3748
* @param client - The SuiClient to use.
49+
* @param configs - The key server configurations containing aggregator URLs.
3850
* @returns - An array of SealKeyServer.
3951
*/
4052
export async function retrieveKeyServers({
4153
objectIds,
4254
client,
55+
configs,
4356
}: {
4457
objectIds: string[];
4558
client: SealCompatibleClient;
59+
configs: Map<string, KeyServerConfig>;
4660
}): Promise<KeyServer[]> {
4761
return await Promise.all(
4862
objectIds.map(async (objectId) => {
@@ -51,39 +65,89 @@ export async function retrieveKeyServers({
5165
objectId,
5266
});
5367
const ks = KeyServerMove.parse(await res.object.content);
54-
if (
55-
EXPECTED_SERVER_VERSION < Number(ks.firstVersion) ||
56-
EXPECTED_SERVER_VERSION > Number(ks.lastVersion)
57-
) {
68+
69+
// Find the highest supported version.
70+
const firstVersion = Number(ks.firstVersion);
71+
const lastVersion = Number(ks.lastVersion);
72+
const version = SUPPORTED_SERVER_VERSIONS.find((v) => v >= firstVersion && v <= lastVersion);
73+
74+
if (version === undefined) {
5875
throw new InvalidKeyServerVersionError(
59-
`Key server ${objectId} supports versions between ${ks.firstVersion} and ${ks.lastVersion} (inclusive), but SDK expects version ${EXPECTED_SERVER_VERSION}`,
76+
`Key server ${objectId} supports versions between ${ks.firstVersion} and ${ks.lastVersion} (inclusive), but SDK expects one of ${SUPPORTED_SERVER_VERSIONS.join(', ')}`,
6077
);
6178
}
6279

63-
// Then fetch the expected versioned object and parse it.
64-
const resVersionedKs = await client.core.getDynamicField({
80+
// Fetch the versioned object.
81+
const versionedKeyServer = await client.core.getDynamicField({
6582
parentId: objectId,
6683
name: {
6784
type: 'u64',
68-
bcs: bcs.u64().serialize(EXPECTED_SERVER_VERSION).toBytes(),
85+
bcs: bcs.u64().serialize(version).toBytes(),
6986
},
7087
});
7188

72-
const ksVersioned = KeyServerMoveV1.parse(resVersionedKs.dynamicField.value.bcs);
89+
// Parse based on version.
90+
switch (version) {
91+
case 2: {
92+
const ksV2 = KeyServerMoveV2.parse(versionedKeyServer.dynamicField.value.bcs);
93+
if (ksV2.keyType !== KeyType.BonehFranklinBLS12381) {
94+
throw new InvalidKeyServerError(
95+
`Server ${objectId} has invalid key type: ${ksV2.keyType}`,
96+
);
97+
}
7398

74-
if (ksVersioned.keyType !== KeyServerType.BonehFranklinBLS12381) {
75-
throw new InvalidKeyServerError(
76-
`Server ${objectId} has invalid key type: ${ksVersioned.keyType}`,
77-
);
78-
}
99+
// Return based on server type.
100+
switch (ksV2.serverType.$kind) {
101+
case 'Independent':
102+
return {
103+
objectId,
104+
name: ksV2.name,
105+
url: ksV2.serverType.Independent.url,
106+
keyType: ksV2.keyType,
107+
pk: new Uint8Array(ksV2.pk),
108+
serverType: 'Independent',
109+
};
110+
case 'Committee': {
111+
// For committee mode, get aggregator URL from config
112+
const config = configs.get(objectId);
113+
if (!config?.aggregatorUrl) {
114+
throw new InvalidClientOptionsError(
115+
`Committee server ${objectId} requires aggregatorUrl in config`,
116+
);
117+
}
118+
return {
119+
objectId,
120+
name: ksV2.name,
121+
url: config.aggregatorUrl,
122+
keyType: ksV2.keyType,
123+
pk: new Uint8Array(ksV2.pk),
124+
serverType: 'Committee',
125+
};
126+
}
127+
default:
128+
throw new InvalidKeyServerError(`Unknown server type for ${objectId}`);
129+
}
130+
}
131+
case 1: {
132+
const ksV1 = KeyServerMoveV1.parse(versionedKeyServer.dynamicField.value.bcs);
133+
if (ksV1.keyType !== KeyType.BonehFranklinBLS12381) {
134+
throw new InvalidKeyServerError(
135+
`Server ${objectId} has invalid key type: ${ksV1.keyType}`,
136+
);
137+
}
79138

80-
return {
81-
objectId,
82-
name: ksVersioned.name,
83-
url: ksVersioned.url,
84-
keyType: ksVersioned.keyType,
85-
pk: new Uint8Array(ksVersioned.pk),
86-
};
139+
return {
140+
objectId,
141+
name: ksV1.name,
142+
url: ksV1.url,
143+
keyType: ksV1.keyType,
144+
pk: new Uint8Array(ksV1.pk),
145+
serverType: 'Independent',
146+
};
147+
}
148+
default:
149+
throw new InvalidKeyServerVersionError(`Unsupported key server version: ${version}`);
150+
}
87151
}),
88152
);
89153
}

packages/seal/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export interface KeyServerConfig {
3636
weight: number;
3737
apiKeyName?: string;
3838
apiKey?: string;
39+
/** Must be provided if object ID is for a committee mode server since all fetch key calls go
40+
* through an aggregator. */
41+
aggregatorUrl?: string;
3942
}
4043

4144
export interface SealClientOptions extends SealClientExtensionOptions {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Copyright (c) Mysten Labs, Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { fromHex } from '@mysten/bcs';
5+
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
6+
import { Transaction } from '@mysten/sui/transactions';
7+
import { getFullnodeUrl, SuiClient } from '@mysten/sui/client';
8+
import { describe, it, expect } from 'vitest';
9+
10+
import { SealClient } from '../../src/client.js';
11+
import { SessionKey } from '../../src/session-key.js';
12+
13+
/**
14+
* Committee Aggregator Integration Tests against ci aggregator.
15+
*/
16+
describe('Committee Aggregator Tests', () => {
17+
it('encrypt and decrypt through aggregator', { timeout: 12000 }, async () => {
18+
// Committee key server object for aggregator ci server, that points to a committee of ci key servers.
19+
const COMMITTEE_KEY_SERVER_OBJ_ID =
20+
'0xa5d2b47e7c649a3c6f9730967a5514abb8e21f19f908ad78a6ad943970c6ad02';
21+
const AGGREGATOR_URL = 'https://seal-aggregator-ci.mystenlabs.com';
22+
23+
// A v1 independent server in ci.
24+
const INDEPENDENT_SERVER_OBJ_ID =
25+
'0x71a3962c5d06a94d1ef5a9c0e7d63ad72cefb48acc93001eaa7ba13fab52786e';
26+
27+
// Testnet package with account_based policy.
28+
const PACKAGE_ID = '0x58dce5d91278bceb65d44666ffa225ab397fc3ae9d8398c8c779c5530bd978c2';
29+
30+
const testKeypair = Ed25519Keypair.generate();
31+
const testAddress = testKeypair.getPublicKey().toSuiAddress();
32+
33+
const testData = crypto.getRandomValues(new Uint8Array(100));
34+
const suiClient = new SuiClient({ url: getFullnodeUrl('testnet') });
35+
36+
// Create Seal client.
37+
const client = new SealClient({
38+
suiClient,
39+
serverConfigs: [
40+
{
41+
objectId: COMMITTEE_KEY_SERVER_OBJ_ID,
42+
weight: 1,
43+
aggregatorUrl: AGGREGATOR_URL,
44+
},
45+
{
46+
objectId: INDEPENDENT_SERVER_OBJ_ID,
47+
weight: 1,
48+
},
49+
],
50+
verifyKeyServers: false,
51+
});
52+
53+
// Encrypt with policy and 2 servers (1 for committee, 1 for independent).
54+
const { encryptedObject: encryptedBytes } = await client.encrypt({
55+
threshold: 2,
56+
packageId: PACKAGE_ID,
57+
id: testAddress,
58+
data: testData,
59+
});
60+
61+
// Create session key.
62+
const sessionKey = await SessionKey.create({
63+
address: testAddress,
64+
packageId: PACKAGE_ID,
65+
ttlMin: 10,
66+
signer: testKeypair,
67+
suiClient,
68+
});
69+
70+
// Build transaction.
71+
const tx = new Transaction();
72+
const keyIdArg = tx.pure.vector('u8', fromHex(testAddress));
73+
tx.moveCall({
74+
target: `${PACKAGE_ID}::account_based::seal_approve`,
75+
arguments: [keyIdArg],
76+
});
77+
const txBytes = await tx.build({ client: suiClient, onlyTransactionKind: true });
78+
79+
// Decrypt data through aggregator.
80+
const decryptedData = await client.decrypt({
81+
data: encryptedBytes,
82+
sessionKey,
83+
txBytes,
84+
});
85+
86+
// Verify decrypted data matches original.
87+
expect(decryptedData).toEqual(testData);
88+
});
89+
});

0 commit comments

Comments
 (0)