Skip to content

Commit

Permalink
POD Email PCD (#2193)
Browse files Browse the repository at this point in the history
Implements a POD version of the Email PCD.

The new PCD wraps a POD in a similar manner to PODTicketPCD. The POD
contains the user's email address and Semaphore v4 public key (note:
Semaphore v3 identity is not included - is there a use-case for
including it?)

`passport-server` has been updated to issue a POD Email PCD alongside
every legacy Email PCD. `passport-client` has been updated to hide the
visual display of POD Email PCDs in the same way that it does with Email
PCDs.

The next step after this PR would be to switch over to using POD Email
PCDs as part of feed credentials, in place of the legacy Email PCD.
  • Loading branch information
robknight authored Jan 21, 2025
1 parent e8553f2 commit 79efcda
Show file tree
Hide file tree
Showing 39 changed files with 2,347 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/16/solid";
import { isEmailPCD } from "@pcd/email-pcd";
import { PCDGetRequest } from "@pcd/passport-interface";
import { Spacer } from "@pcd/passport-ui";
import { isPODEmailPCD } from "@pcd/pod-email-pcd";
import { isSemaphoreIdentityPCD } from "@pcd/semaphore-identity-pcd";
import {
ReactElement,
Expand Down Expand Up @@ -195,8 +196,12 @@ export const NewHomeScreen = (): ReactElement => {
const noPods =
collection
.getAll()
.filter((pcd) => !isEmailPCD(pcd) && !isSemaphoreIdentityPCD(pcd))
.length === 0;
.filter(
(pcd) =>
!isEmailPCD(pcd) &&
!isSemaphoreIdentityPCD(pcd) &&
!isPODEmailPCD(pcd)
).length === 0;

const orientation = useOrientation();
const isLandscape =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isEmailPCD } from "@pcd/email-pcd";
import { isPODEmailPCD } from "@pcd/pod-email-pcd";
import { isSemaphoreIdentityPCD } from "@pcd/semaphore-identity-pcd";
import {
ReactElement,
Expand All @@ -9,17 +10,17 @@ import {
} from "react";
import { useSearchParams } from "react-router-dom";
import styled from "styled-components";
import { ZappButton } from "../../../components/screens/ZappScreens/ZappButton";
import { ZappButtonsContainer } from "../../../components/screens/ZappScreens/ZappButtonsContainer";
import { ZappFullScreen } from "../../../components/screens/ZappScreens/ZappFullScreen";
import { appConfig } from "../../../src/appConfig";
import { useDispatch, usePCDCollection, useSelf } from "../../../src/appHooks";
import { BANNER_HEIGHT, MAX_WIDTH_SCREEN } from "../../../src/sharedConstants";
import { nextFrame } from "../../../src/util";
import { PodsCollectionList } from "../../shared/Modals/PodsCollectionBottomModal";
import { Typography } from "../../shared/Typography";
import { hideScrollCSS, replaceDotWithSlash } from "../../shared/utils";
import { ScrollIndicator } from "./ScrollIndicator";
import { ZappButtonsContainer } from "../../../components/screens/ZappScreens/ZappButtonsContainer";
import { ZappButton } from "../../../components/screens/ZappScreens/ZappButton";
import { appConfig } from "../../../src/appConfig";
import { ZappFullScreen } from "../../../components/screens/ZappScreens/ZappFullScreen";

const EMPTY_CARD_CONTAINER_HEIGHT = 220;
const EmptyCardContainer = styled.div<{ longVersion: boolean }>`
Expand Down Expand Up @@ -124,8 +125,12 @@ export const NoUpcomingEventsState = ({
const noPods =
pods
.getAll()
.filter((pcd) => !isEmailPCD(pcd) && !isSemaphoreIdentityPCD(pcd))
.length === 0;
.filter(
(pcd) =>
!isEmailPCD(pcd) &&
!isSemaphoreIdentityPCD(pcd) &&
!isPODEmailPCD(pcd)
).length === 0;

useLayoutEffect(() => {
// Restore scroll position when list is shown again
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { isEdDSAFrogPCD } from "@pcd/eddsa-frog-pcd";
import { isEdDSATicketPCD } from "@pcd/eddsa-ticket-pcd";
import { isEmailPCD } from "@pcd/email-pcd";
import { PCD } from "@pcd/pcd-types";
import { isPODEmailPCD } from "@pcd/pod-email-pcd";
import {
getImageUrlEntry,
getDisplayOptions as getPodDisplayOptions,
Expand Down Expand Up @@ -57,7 +58,7 @@ const filterOverlappingEdDSATickets = (
)
.map((eddsa) => eddsa.id);

const noEmails = pcds.filter((p) => !isEmailPCD(p));
const noEmails = pcds.filter((p) => !isEmailPCD(p) && !isPODEmailPCD(p));

return noEmails.filter((pcd) => !overlapping.includes(pcd.id));
};
Expand All @@ -67,6 +68,8 @@ const getPcdName = (pcd: PCD<unknown, unknown>): string => {
return pcd.claim.ticket.eventName + " - " + pcd.claim.ticket.ticketName;
case isEmailPCD(pcd):
return pcd.claim.emailAddress;
case isPODEmailPCD(pcd):
return pcd.claim.podEntries.emailAddress.value;
case isPODPCD(pcd):
return getPodDisplayOptions(pcd).header ?? pcd.id;
case isEdDSAFrogPCD(pcd):
Expand Down
2 changes: 2 additions & 0 deletions apps/passport-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@
"@pcd/pcd-collection": "0.15.0",
"@pcd/pcd-types": "0.15.0",
"@pcd/pod": "0.5.0",
"@pcd/pod-email-pcd": "0.1.0",
"@pcd/pod-email-pcd-ui": "0.1.0",
"@pcd/pod-pcd": "0.5.0",
"@pcd/pod-pcd-ui": "0.5.0",
"@pcd/pod-ticket-pcd": "0.5.0",
Expand Down
2 changes: 2 additions & 0 deletions apps/passport-client/src/pcdPackages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { MessagePCDPackage } from "@pcd/message-pcd";
import { requestLogToServer } from "@pcd/passport-interface";
import { PCDCollection } from "@pcd/pcd-collection";
import { PCD, PCDPackage, SerializedPCD } from "@pcd/pcd-types";
import { PODEmailPCDPackage } from "@pcd/pod-email-pcd";
import { PODPCDPackage } from "@pcd/pod-pcd";
import { PODTicketPCDPackage } from "@pcd/pod-ticket-pcd";
import { RSAImagePCDPackage } from "@pcd/rsa-image-pcd";
Expand Down Expand Up @@ -98,6 +99,7 @@ async function loadPackages(): Promise<PCDPackage[]> {
PODPCDPackage,
PODTicketPCDPackage,
GPCPCDPackage,
PODEmailPCDPackage,
UnknownPCDPackage
];
}
Expand Down
4 changes: 4 additions & 0 deletions apps/passport-client/src/pcdRenderers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { HaLoNoncePCDUI } from "@pcd/halo-nonce-pcd-ui";
import { MessagePCDTypeName } from "@pcd/message-pcd";
import { MessagePCDUI } from "@pcd/message-pcd-ui";
import { PCD, PCDUI } from "@pcd/pcd-types";
import { PODEmailPCDTypeName } from "@pcd/pod-email-pcd";
import { PODEmailPCDUI } from "@pcd/pod-email-pcd-ui";
import { PODPCDTypeName } from "@pcd/pod-pcd";
import { PODPCDUI } from "@pcd/pod-pcd-ui";
import { PODTicketPCDTypeName } from "@pcd/pod-ticket-pcd";
Expand Down Expand Up @@ -55,6 +57,7 @@ const renderablePCDs = [
PODPCDTypeName,
PODTicketPCDTypeName,
GPCPCDTypeName,
PODEmailPCDTypeName,
UnknownPCDTypeName
] as const;

Expand All @@ -80,5 +83,6 @@ export const pcdRenderers: {
[PODPCDTypeName]: PODPCDUI,
[PODTicketPCDTypeName]: PODTicketPCDUI,
[GPCPCDTypeName]: GPCPCDUI,
[PODEmailPCDTypeName]: PODEmailPCDUI,
[UnknownPCDTypeName]: UnknownPCDUI
};
6 changes: 6 additions & 0 deletions apps/passport-client/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@
{
"path": "../../packages/lib/pod"
},
{
"path": "../../packages/pcd/pod-email-pcd"
},
{
"path": "../../packages/ui/pod-email-pcd-ui"
},
{
"path": "../../packages/pcd/pod-pcd"
},
Expand Down
1 change: 1 addition & 0 deletions apps/passport-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@pcd/pcd-collection": "0.15.0",
"@pcd/pcd-types": "0.15.0",
"@pcd/pod": "0.5.0",
"@pcd/pod-email-pcd": "0.1.0",
"@pcd/pod-pcd": "0.5.0",
"@pcd/pod-ticket-pcd": "0.5.0",
"@pcd/podbox-shared": "0.4.0",
Expand Down
85 changes: 59 additions & 26 deletions apps/passport-server/src/services/issuanceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
ITicketData,
TicketCategory
} from "@pcd/eddsa-ticket-pcd";
import { EmailPCD, EmailPCDPackage } from "@pcd/email-pcd";
import { EmailPCD, EmailPCDPackage, isEmailPCD } from "@pcd/email-pcd";
import { getHash } from "@pcd/passport-crypto";
import {
Credential,
Expand Down Expand Up @@ -44,7 +44,10 @@ import {
PCDPermissionType
} from "@pcd/pcd-collection";
import { ArgumentTypeName, SerializedPCD } from "@pcd/pcd-types";
import { encodePublicKey } from "@pcd/pod";
import { PODEmailPCD, PODEmailPCDPackage } from "@pcd/pod-email-pcd";
import { RSAImagePCDPackage } from "@pcd/rsa-image-pcd";
import { IdentityV3, v3tov4Identity } from "@pcd/semaphore-identity-pcd";
import { RollbarService } from "@pcd/server-shared";
import { ONE_HOUR_MS } from "@pcd/util";
import { ZKEdDSAEventTicketPCDPackage } from "@pcd/zk-eddsa-event-ticket-pcd";
Expand Down Expand Up @@ -189,7 +192,11 @@ export class IssuanceService {
type: PCDActionType.ReplaceInFolder,
folder: "Email",
pcds: await Promise.all(
pcds.map((pcd) => EmailPCDPackage.serialize(pcd))
pcds.map((pcd) =>
isEmailPCD(pcd)
? EmailPCDPackage.serialize(pcd)
: PODEmailPCDPackage.serialize(pcd)
)
)
});
});
Expand Down Expand Up @@ -584,7 +591,7 @@ export class IssuanceService {
private async issueEmailPCDs(
client: PoolClient,
credential: VerifiedCredential
): Promise<EmailPCD[]> {
): Promise<(EmailPCD | PODEmailPCD)[]> {
return traced("IssuanceService", "issueEmailPCDs", async (span) => {
const user = await this.checkUserExists(client, credential);

Expand All @@ -598,33 +605,59 @@ export class IssuanceService {
span?.setAttribute("emails", user.emails);
}

return Promise.all(
const pcds = await Promise.all(
user.emails.map((email) => {
const stableId = "attested-email-" + email;
return EmailPCDPackage.prove({
privateKey: {
value: this.eddsaPrivateKey,
argumentType: ArgumentTypeName.String
},
id: {
value: stableId,
argumentType: ArgumentTypeName.String
},
emailAddress: {
value: email,
argumentType: ArgumentTypeName.String
},
semaphoreId: {
value: user.commitment,
argumentType: ArgumentTypeName.String
},
semaphoreV4Id: {
value: user.semaphore_v4_pubkey ?? undefined,
argumentType: ArgumentTypeName.String
}
});
const stablePodId = "pod-attested-email-" + email;
return Promise.all([
EmailPCDPackage.prove({
privateKey: {
value: this.eddsaPrivateKey,
argumentType: ArgumentTypeName.String
},
id: {
value: stableId,
argumentType: ArgumentTypeName.String
},
emailAddress: {
value: email,
argumentType: ArgumentTypeName.String
},
semaphoreId: {
value: user.commitment,
argumentType: ArgumentTypeName.String
},
semaphoreV4Id: {
value: user.semaphore_v4_pubkey ?? undefined,
argumentType: ArgumentTypeName.String
}
}),
PODEmailPCDPackage.prove({
id: {
value: stablePodId,
argumentType: ArgumentTypeName.String
},
privateKey: {
value: this.eddsaPrivateKey,
argumentType: ArgumentTypeName.String
},
emailAddress: {
value: email,
argumentType: ArgumentTypeName.String
},
semaphoreV4PublicKey: {
value:
user.semaphore_v4_pubkey ??
encodePublicKey(
v3tov4Identity(new IdentityV3(user.commitment)).publicKey
),
argumentType: ArgumentTypeName.String
}
})
]);
})
);
return pcds.flat();
});
}

Expand Down
28 changes: 25 additions & 3 deletions apps/passport-server/test/email-feed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
requestPollFeed
} from "@pcd/passport-interface";
import { PCDActionType, isReplaceInFolderAction } from "@pcd/pcd-collection";
import { encodePublicKey } from "@pcd/pod";
import { PODEmailPCDPackage, PODEmailPCDTypeName } from "@pcd/pod-email-pcd";
import { Identity } from "@semaphore-protocol/identity";
import { expect } from "chai";
import { assert, expect } from "chai";
import "mocha";
import { step } from "mocha-steps";
import MockDate from "mockdate";
Expand Down Expand Up @@ -75,8 +77,9 @@ describe("attested email feed functionality", function () {
const action = pollFeedResult?.value?.actions?.[1];
expectToExist(action, isReplaceInFolderAction);
expect(action.type).to.eq(PCDActionType.ReplaceInFolder);
expect(action.pcds.length).to.eq(1);
expect(action.pcds.length).to.eq(2);
expect(action.pcds[0].type).to.eq(EmailPCDTypeName);
expect(action.pcds[1].type).to.eq(PODEmailPCDTypeName);

// Check that the PCD contains the expected email address
const deserializedPCD = await EmailPCDPackage.deserialize(
Expand All @@ -87,10 +90,29 @@ describe("attested email feed functionality", function () {
// Check that the PCD verifies
expect(await EmailPCDPackage.verify(deserializedPCD)).to.be.true;

const serverPublicKey =
await application.services.issuanceService?.getEdDSAPublicKey();
assert(serverPublicKey, "Server public key should exist");

// Check the public key
expect(deserializedPCD.proof.eddsaPCD.claim.publicKey).to.deep.eq(
await application.services.issuanceService?.getEdDSAPublicKey()
serverPublicKey
);

// Check the PODEmailPCD
const podEmailPCD = await PODEmailPCDPackage.deserialize(
action.pcds[1].pcd
);
expect(podEmailPCD.claim.podEntries.emailAddress.value).to.eq(testEmail);
expect(podEmailPCD.claim.signerPublicKey).to.deep.eq(
encodePublicKey([
BigInt(`0x${serverPublicKey[0]}`),
BigInt(`0x${serverPublicKey[1]}`)
])
);

// Check that the PODEmailPCD verifies
expect(await PODEmailPCDPackage.verify(podEmailPCD)).to.be.true;
}
);
});
Loading

0 comments on commit 79efcda

Please sign in to comment.