diff --git a/apps/client-web/package.json b/apps/client-web/package.json index 57c8ec4..5f45f90 100644 --- a/apps/client-web/package.json +++ b/apps/client-web/package.json @@ -16,6 +16,7 @@ "@pcd/gpc": "0.0.6", "@pcd/pod": "0.1.5", "@pcd/proto-pod-gpc-artifacts": "^0.5.0", + "@semaphore-protocol/identity": "^4.0.3", "eventemitter3": "^5.0.1", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/apps/client-web/src/App.tsx b/apps/client-web/src/App.tsx index b92d619..9dcf9b3 100644 --- a/apps/client-web/src/App.tsx +++ b/apps/client-web/src/App.tsx @@ -13,7 +13,11 @@ import { } from "react"; import { ParcnetClientProcessor } from "./client/client"; import { PODCollection } from "./client/pod_collection"; -import { loadPODsFromStorage, savePODsToStorage } from "./client/utils"; +import { + getIdentity, + loadPODsFromStorage, + savePODsToStorage +} from "./client/utils"; import { Rabbit } from "./rabbit"; import { ClientAction, clientReducer, ClientState } from "./state"; @@ -23,7 +27,8 @@ function App() { advice: null, zapp: null, authorized: false, - proofInProgress: undefined + proofInProgress: undefined, + identity: getIdentity() }); useEffect(() => { @@ -48,7 +53,7 @@ function App() { savePODsToStorage(pods.getAll()); }); state.advice.ready( - new ParcnetClientProcessor(state.advice, pods, dispatch) + new ParcnetClientProcessor(state.advice, pods, dispatch, getIdentity()) ); } }, [state.authorized, state.advice]); diff --git a/apps/client-web/src/client/client.ts b/apps/client-web/src/client/client.ts index 9b06c41..fbf4261 100644 --- a/apps/client-web/src/client/client.ts +++ b/apps/client-web/src/client/client.ts @@ -5,6 +5,7 @@ import { ParcnetPODRPC, ParcnetRPC } from "@parcnet/client-rpc"; +import { Identity } from "@semaphore-protocol/identity"; import { Dispatch } from "react"; import { ClientAction } from "../state.js"; import { ParcnetGPCProcessor } from "./gpc.js"; @@ -23,13 +24,18 @@ export class ParcnetClientProcessor implements ParcnetRPC { public constructor( public readonly clientChannel: ConnectorAdvice, private readonly pods: PODCollection, - dispatch: Dispatch + dispatch: Dispatch, + userIdentity: Identity ) { this.subscriptions = new QuerySubscriptions(this.pods); this.subscriptions.onSubscriptionUpdated((update, serial) => { this.clientChannel.subscriptionUpdate(update, serial); }); - this.pod = new ParcnetPODProcessor(this.pods, this.subscriptions); + this.pod = new ParcnetPODProcessor( + this.pods, + this.subscriptions, + userIdentity + ); this.identity = new ParcnetIdentityProcessor(); this.gpc = new ParcnetGPCProcessor(this.pods, dispatch, this.clientChannel); } diff --git a/apps/client-web/src/client/pod.ts b/apps/client-web/src/client/pod.ts index 5d4e338..f7bedc8 100644 --- a/apps/client-web/src/client/pod.ts +++ b/apps/client-web/src/client/pod.ts @@ -1,18 +1,17 @@ -import { ParcnetPODRPC } from "@parcnet/client-rpc"; -import { EntriesSchema, PODSchema } from "@parcnet/podspec"; -import { POD } from "@pcd/pod"; +import { ParcnetPODRPC, PODQuery } from "@parcnet/client-rpc"; +import { encodePrivateKey, POD, PODEntries } from "@pcd/pod"; +import { Identity } from "@semaphore-protocol/identity"; import { PODCollection } from "./pod_collection.js"; import { QuerySubscriptions } from "./query_subscriptions.js"; export class ParcnetPODProcessor implements ParcnetPODRPC { public constructor( private readonly pods: PODCollection, - private readonly subscriptions: QuerySubscriptions + private readonly subscriptions: QuerySubscriptions, + private readonly identity: Identity ) {} - public async query( - query: PODSchema - ): Promise { + public async query(query: PODQuery): Promise { return this.pods.query(query).map((pod) => pod.serialize()); } @@ -25,13 +24,23 @@ export class ParcnetPODProcessor implements ParcnetPODRPC { this.pods.delete(signature); } - public async subscribe( - query: PODSchema - ): Promise { + public async subscribe(query: PODQuery): Promise { return this.subscriptions.subscribe(query); } public async unsubscribe(subscriptionId: string): Promise { this.subscriptions.unsubscribe(subscriptionId); } + + public async sign(entries: PODEntries): Promise { + /** + * @todo: Once we have decided how to restrict this, we would implement + * some security restrictions here. + */ + const pod = POD.sign( + entries, + encodePrivateKey(Buffer.from(this.identity.export(), "base64")) + ); + return pod.serialize(); + } } diff --git a/apps/client-web/src/client/utils.ts b/apps/client-web/src/client/utils.ts index 00e5729..0b7cc1d 100644 --- a/apps/client-web/src/client/utils.ts +++ b/apps/client-web/src/client/utils.ts @@ -1,4 +1,5 @@ import { POD } from "@pcd/pod"; +import { Identity } from "@semaphore-protocol/identity"; export function loadPODsFromStorage(): POD[] { let pods: POD[] = []; @@ -21,3 +22,17 @@ export function savePODsToStorage(pods: POD[]): void { const serializedPODs = pods.map((pod) => pod.serialize()); localStorage.setItem("pod_collection", JSON.stringify(serializedPODs)); } + +export function getIdentity(): Identity { + const serializedIdentity = localStorage.getItem("identity"); + + let identity: Identity; + if (!serializedIdentity) { + identity = new Identity(); + localStorage.setItem("identity", identity.export()); + } else { + identity = Identity.import(serializedIdentity); + } + + return identity; +} diff --git a/apps/client-web/src/state.ts b/apps/client-web/src/state.ts index 0e2daaa..7a1e49a 100644 --- a/apps/client-web/src/state.ts +++ b/apps/client-web/src/state.ts @@ -2,6 +2,7 @@ import { ConnectorAdvice } from "@parcnet/client-helpers"; import { ProveResult, Zapp } from "@parcnet/client-rpc"; import { PodspecProofRequest } from "@parcnet/podspec"; import { POD } from "@pcd/pod"; +import { Identity } from "@semaphore-protocol/identity"; export type ClientState = { loggedIn: boolean; @@ -17,6 +18,7 @@ export type ClientState = { resolve?: (result: ProveResult) => void; } | undefined; + identity: Identity; }; export type ClientAction = diff --git a/examples/test-app/src/apis/PODSection.tsx b/examples/test-app/src/apis/PODSection.tsx index 4d14735..ec20137 100644 --- a/examples/test-app/src/apis/PODSection.tsx +++ b/examples/test-app/src/apis/PODSection.tsx @@ -2,16 +2,20 @@ import { ParcnetAPI, Subscription } from "@parcnet/app-connector"; import * as p from "@parcnet/podspec"; import { POD, POD_INT_MAX, POD_INT_MIN, PODEntries, PODValue } from "@pcd/pod"; import JSONBig from "json-bigint"; -import { ReactNode, useReducer, useState } from "react"; +import { + Dispatch, + ReactNode, + SetStateAction, + useReducer, + useState +} from "react"; import { Button } from "../components/Button"; import { TryIt } from "../components/TryIt"; import { useParcnetClient } from "../hooks/useParcnetClient"; -const MAGIC_PRIVATE_KEY = - "00112233445566778899AABBCCDDEEFF00112233445566778899aabbccddeeff"; - export function PODSection(): ReactNode { const { z, connected } = useParcnetClient(); + const [pod, setPOD] = useState(null); return !connected ? null : (
@@ -19,8 +23,10 @@ export function PODSection(): ReactNode {

Query PODs

+

Sign POD

+

Insert POD

- +

Delete POD

Subscribe to PODs

@@ -122,7 +128,7 @@ type Action = const stringish = ["string", "eddsa_pubkey"]; const bigintish = ["int", "cryptographic"]; -const insertPODReducer = function ( +const editPODReducer = function ( state: PODEntries, action: Action ): PODEntries { @@ -186,23 +192,29 @@ enum PODCreationState { Failure } -function InsertPOD({ z }: { z: ParcnetAPI }): ReactNode { +function SignPOD({ + z, + setSignedPOD +}: { + z: ParcnetAPI; + setSignedPOD: Dispatch>; +}): ReactNode { const [creationState, setCreationState] = useState( PODCreationState.None ); - const [signature, setSignature] = useState(""); - const [entries, dispatch] = useReducer(insertPODReducer, { + const [pod, setPOD] = useState(null); + const [entries, dispatch] = useReducer(editPODReducer, { test: { type: "string", value: "Testing" } } satisfies PODEntries); return (

- To insert a POD, first we have to create one. Select the entries for the - POD below: + To sign a POD, first we have to create the entries. Select the entries + for the POD below:

{Object.entries(entries).map(([name, value], index) => ( -

- Then we can insert the POD: + Then we can sign the POD: - {`const pod = POD.sign({ + {`const pod = await z.pod.sign({ ${Object.entries(entries) .map(([key, value]) => { return ` ${key}: { type: "${value.type}", value: ${ @@ -235,36 +247,37 @@ ${Object.entries(entries) } }`; }) .join(",\n")} -}, privateKey); +}); -await z.pod.insert(pod);`} +`}

{ try { - const pod = POD.sign(entries, MAGIC_PRIVATE_KEY); - await z.pod.insert(pod); - setSignature(pod.signature); + const pod = await z.pod.sign(entries); + setPOD(pod); + setSignedPOD(pod); setCreationState(PODCreationState.Success); - } catch (_e) { + } catch (e) { + console.error(e); setCreationState(PODCreationState.Failure); } }} - label="Insert POD" + label="Sign POD" /> {creationState !== PODCreationState.None && (
{creationState === PODCreationState.Success && (
- POD inserted successfully! The signature is{" "} + POD signed successfully! The signature is{" "} - {signature} + {pod?.signature}
)} {creationState === PODCreationState.Failure && ( -
An error occurred while inserting your POD.
+
An error occurred while signing your POD.
)}
)} @@ -272,7 +285,7 @@ await z.pod.insert(pod);`} ); } -function InsertPODEntry({ +function EditPODEntry({ name, value, type, @@ -341,6 +354,47 @@ function InsertPODEntry({ ); } +function InsertPOD({ z, pod }: { z: ParcnetAPI; pod: POD }): ReactNode { + const [insertionState, setInsertionState] = useState( + PODCreationState.None + ); + return ( +
+

+ To insert a POD, we must first create it. You can create a new POD by + using the "Sign POD" section above. +

+

+ + {`await z.pod.insert(pod);`} + +

+ + { + try { + await z.pod.insert(pod); + setInsertionState(PODCreationState.Success); + } catch (_e) { + setInsertionState(PODCreationState.Failure); + } + }} + label="Insert POD" + /> + {insertionState !== PODCreationState.None && ( +
+ {insertionState === PODCreationState.Success && ( +
POD inserted successfully!
+ )} + {insertionState === PODCreationState.Failure && ( +
An error occurred while inserting your POD.
+ )} +
+ )} +
+ ); +} + enum PODDeletionState { None, Success, diff --git a/packages/app-connector/src/api_wrapper.ts b/packages/app-connector/src/api_wrapper.ts index c7a09a9..1188313 100644 --- a/packages/app-connector/src/api_wrapper.ts +++ b/packages/app-connector/src/api_wrapper.ts @@ -5,7 +5,7 @@ import { } from "@parcnet/client-rpc"; import * as p from "@parcnet/podspec"; import { GPCBoundConfig, GPCProof, GPCRevealedClaims } from "@pcd/gpc"; -import { POD } from "@pcd/pod"; +import { POD, PODEntries } from "@pcd/pod"; import { EventEmitter } from "eventemitter3"; import { PodspecProofRequest } from "../../podspec/src/index.js"; import { ParcnetRPCConnector } from "./rpc_client.js"; @@ -81,6 +81,11 @@ class ParcnetPODWrapper { async delete(signature: string): Promise { return this.#api.pod.delete(signature); } + + async sign(entries: PODEntries): Promise { + const pod = await this.#api.pod.sign(entries); + return POD.deserialize(pod); + } } class ParcnetGPCWrapper { diff --git a/packages/app-connector/src/rpc_client.ts b/packages/app-connector/src/rpc_client.ts index 1f63372..edbee68 100644 --- a/packages/app-connector/src/rpc_client.ts +++ b/packages/app-connector/src/rpc_client.ts @@ -15,6 +15,7 @@ import { } from "@parcnet/client-rpc"; import { PodspecProofRequest } from "@parcnet/podspec"; import { GPCBoundConfig, GPCProof, GPCRevealedClaims } from "@pcd/gpc"; +import { PODEntries } from "@pcd/pod"; import { EventEmitter } from "eventemitter3"; import { z, ZodFunction, ZodTuple, ZodTypeAny } from "zod"; import { DialogController } from "./adapters/iframe.js"; @@ -149,6 +150,13 @@ export class ParcnetRPCConnector implements ParcnetRPC, ParcnetEvents { [subscriptionId], ParcnetRPCSchema.shape.pod.shape.unsubscribe ); + }, + sign: async (entries: PODEntries): Promise => { + return this.#typedInvoke( + "pod.sign", + [entries], + ParcnetRPCSchema.shape.pod.shape.sign + ); } }; this.gpc = { diff --git a/packages/client-helpers/src/connection/iframe.ts b/packages/client-helpers/src/connection/iframe.ts index f711f7f..1bb4605 100644 --- a/packages/client-helpers/src/connection/iframe.ts +++ b/packages/client-helpers/src/connection/iframe.ts @@ -80,6 +80,8 @@ function getSchema(method: ParcnetRPCMethodName) { return ParcnetRPCSchema.shape.pod.shape.subscribe; case "pod.unsubscribe": return ParcnetRPCSchema.shape.pod.shape.unsubscribe; + case "pod.sign": + return ParcnetRPCSchema.shape.pod.shape.sign; default: const unknownMethod: never = method; throw new Error(`Unknown method: ${unknownMethod as string}`); diff --git a/packages/client-rpc/package.json b/packages/client-rpc/package.json index 698d9e5..a6fc132 100644 --- a/packages/client-rpc/package.json +++ b/packages/client-rpc/package.json @@ -19,6 +19,7 @@ "dependencies": { "@parcnet/podspec": "workspace:*", "@pcd/gpc": "0.0.6", + "@pcd/pod": "0.1.5", "zod": "^3.22.4" }, "devDependencies": { diff --git a/packages/client-rpc/src/rpc_interfaces.ts b/packages/client-rpc/src/rpc_interfaces.ts index be7579c..106ebd5 100644 --- a/packages/client-rpc/src/rpc_interfaces.ts +++ b/packages/client-rpc/src/rpc_interfaces.ts @@ -4,6 +4,7 @@ import { PodspecProofRequest } from "@parcnet/podspec"; import { GPCBoundConfig, GPCProof, GPCRevealedClaims } from "@pcd/gpc"; +import { PODEntries } from "@pcd/pod"; /** * @file This file contains the RPC interfaces for the Parcnet client. @@ -52,6 +53,8 @@ export interface ParcnetPODRPC { delete: (signature: string) => Promise; subscribe: (query: PODQuery) => Promise; unsubscribe: (subscriptionId: string) => Promise; + // Returns serialized POD + sign: (entries: PODEntries) => Promise; } export interface ParcnetRPC { diff --git a/packages/client-rpc/src/schema.ts b/packages/client-rpc/src/schema.ts index f9c2300..44c392b 100644 --- a/packages/client-rpc/src/schema.ts +++ b/packages/client-rpc/src/schema.ts @@ -8,6 +8,8 @@ const PODValueSchema = z.object({ value: z.union([z.string(), z.bigint()]) }); +const PODEntriesSchema = z.record(PODValueSchema); + const DefinedEntrySchema = z.object({ type: z.enum(["string", "int", "cryptographic", "eddsa_pubkey"]), isMemberOf: z.array(PODValueSchema).optional(), @@ -102,6 +104,7 @@ export const ParcnetRPCSchema = z.object({ .function() .args(PODSchemaSchema) .returns(z.promise(z.string())), - unsubscribe: z.function().args(z.string()).returns(z.promise(z.void())) + unsubscribe: z.function().args(z.string()).returns(z.promise(z.void())), + sign: z.function().args(PODEntriesSchema).returns(z.promise(z.string())) }) }) satisfies z.ZodSchema; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a281e8a..e044229 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -120,6 +120,9 @@ importers: '@pcd/proto-pod-gpc-artifacts': specifier: ^0.5.0 version: 0.5.0 + '@semaphore-protocol/identity': + specifier: ^4.0.3 + version: 4.0.3 eventemitter3: specifier: ^5.0.1 version: 5.0.1 @@ -328,6 +331,9 @@ importers: '@pcd/gpc': specifier: 0.0.6 version: 0.0.6 + '@pcd/pod': + specifier: 0.1.5 + version: 0.1.5 zod: specifier: ^3.22.4 version: 3.23.8 @@ -1099,6 +1105,9 @@ packages: '@semaphore-protocol/identity@3.15.2': resolution: {integrity: sha512-MJ1MO5QL+oX+OFK2rHAPjQ6+kKgGxCsVJLNdn1soRawxbrxH9A6tV9AsVHV0DN4saegQ4qaOOy1XO1PAN6PiQA==} + '@semaphore-protocol/identity@4.0.3': + resolution: {integrity: sha512-IfGpE00gWnuM0c41g3rY1uU0he8X8JOrerqXjGzrsW0sXRe/jkxL/jln6zufVTGleN/mGdP9CkqXh7Ca7QjAiw==} + '@snyk/github-codeowners@1.1.0': resolution: {integrity: sha512-lGFf08pbkEac0NYgVf4hdANpAgApRjNByLXB+WBip3qj1iendOIyAwP2GKkKbQMNVy2r1xxDf0ssfWscoiC+Vw==} engines: {node: '>=8.10'} @@ -2868,6 +2877,9 @@ packages: resolution: {integrity: sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==} engines: {node: '>=10'} + poseidon-lite@0.2.0: + resolution: {integrity: sha512-vivDZnGmz8W4G/GzVA72PXkfYStjilu83rjjUfpL4PueKcC8nfX6hCPh2XhoC5FBgC6y0TA3YuUeUo5YCcNoig==} + poseidon-lite@0.2.1: resolution: {integrity: sha512-xIr+G6HeYfOhCuswdqcFpSX47SPhm0EpisWJ6h7fHlWwaVIvH3dLnejpatrtw6Xc6HaLrpq05y7VRfvDmDGIog==} @@ -4313,6 +4325,13 @@ snapshots: '@ethersproject/strings': 5.7.0 js-sha512: 0.8.0 + '@semaphore-protocol/identity@4.0.3': + dependencies: + '@zk-kit/baby-jubjub': 1.0.1 + '@zk-kit/eddsa-poseidon': 1.0.2(patch_hash=ulg4x5tm5aeuwpznqhsdha2vyq) + '@zk-kit/utils': 1.2.0 + poseidon-lite: 0.2.0 + '@snyk/github-codeowners@1.1.0': dependencies: commander: 4.1.1 @@ -6356,6 +6375,8 @@ snapshots: dependencies: find-up: 5.0.0 + poseidon-lite@0.2.0: {} + poseidon-lite@0.2.1: {} possible-typed-array-names@1.0.0: {}