Skip to content

Commit

Permalink
Throw exceptions when event ID doesn't match configuration (#1802)
Browse files Browse the repository at this point in the history
Closes
https://linear.app/0xparc-pcd/issue/0XP-974/zuauth-doesnt-check-event-id-when-revealed

Fixes `authenticate` so that, if the event ID is revealed and event IDs
are configured as filters, the revealed event ID must match one of the
filtered values.

Also updates the tests to be more careful in identifying the kind of
exception thrown.
  • Loading branch information
robknight authored Jul 16, 2024
1 parent 402cc63 commit f43d4ed
Show file tree
Hide file tree
Showing 9 changed files with 428 additions and 138 deletions.
4 changes: 3 additions & 1 deletion apps/consumer-client/src/pages/examples/zuauth/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ export default function ZuAuth(): JSX.Element {
});

if (result.type === "pcd") {
setAuthenticated(await serverLogin(result.pcdStr, config));
setAuthenticated(
await serverLogin(result.pcdStr, config, fieldsToReveal)
);
}
})();
}, [fieldsToReveal, configString]);
Expand Down
6 changes: 4 additions & 2 deletions apps/consumer-client/src/pages/examples/zuauth/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ITicketData } from "@pcd/eddsa-ticket-pcd";
import { PipelineEdDSATicketZuAuthConfig } from "@pcd/passport-interface";
import { EdDSATicketFieldsToReveal } from "@pcd/zk-eddsa-event-ticket-pcd";
import urlJoin from "url-join";
import { CONSUMER_SERVER_URL } from "../../../constants";

Expand Down Expand Up @@ -33,7 +34,8 @@ export async function logout(): Promise<void> {
*/
export async function serverLogin(
serialized: string,
config: PipelineEdDSATicketZuAuthConfig[]
config: PipelineEdDSATicketZuAuthConfig[],
fieldsToReveal: EdDSATicketFieldsToReveal
): Promise<Partial<ITicketData>> {
const response = await fetch(urlJoin(CONSUMER_SERVER_URL, `auth/login`), {
method: "POST",
Expand All @@ -43,7 +45,7 @@ export async function serverLogin(
"Access-Control-Allow-Origin": "*",
"Content-Type": "application/json"
},
body: JSON.stringify({ pcd: serialized, config })
body: JSON.stringify({ pcd: serialized, config, fieldsToReveal })
});

return await response.json();
Expand Down
10 changes: 5 additions & 5 deletions apps/consumer-server/src/routing/routes/zuauth/login.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ export function login(
return;
}

const pcd = await authenticate(
req.body.pcd,
session.watermark,
req.body.config
);
const pcd = await authenticate(req.body.pcd, {
watermark: session.watermark,
config: req.body.config,
fieldsToReveal: req.body.fieldsToReveal
});

session.ticket = pcd.claim.partialTicket;

Expand Down
11 changes: 10 additions & 1 deletion examples/zuauth/src/app/api/login/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,16 @@ export async function POST(req: NextRequest) {

try {
const session = await getIronSession<SessionData>(cookieStore, ironOptions);
const pcd = await authenticate(body.pcd, session.watermark ?? "", config);
const pcd = await authenticate(body.pcd, {
watermark: session.watermark ?? "",
config,
fieldsToReveal: {
revealAttendeeEmail: true,
revealAttendeeName: true,
revealEventId: true,
revealProductId: true
}
});

session.user = pcd.claim.partialTicket;
await session.save();
Expand Down
4 changes: 3 additions & 1 deletion examples/zuauth/src/app/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@ export default function Home() {
zupassUrl: process.env.NEXT_PUBLIC_ZUPASS_SERVER_URL as string,
fieldsToReveal: {
revealAttendeeEmail: true,
revealAttendeeName: true
revealAttendeeName: true,
revealEventId: true,
revealProductId: true
},
watermark,
config: config
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/zuauth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@
"@pcd/tsconfig": "0.11.1",
"@semaphore-protocol/identity": "^3.15.2",
"@types/chai": "^4.3.5",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^10.0.1",
"@types/react": "^18.2.0",
"chai-as-promised": "^7.1.1",
"eslint": "^8.57.0",
"mocha": "^10.2.0",
"ts-mocha": "^10.0.0",
Expand Down
152 changes: 124 additions & 28 deletions packages/lib/zuauth/src/server.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,73 @@
import { isEqualEdDSAPublicKey } from "@pcd/eddsa-pcd";
import { PipelineEdDSATicketZuAuthConfig } from "@pcd/passport-interface";
import { ITicketData } from "@pcd/eddsa-ticket-pcd";
import { PipelineZuAuthConfig } from "@pcd/passport-interface";
import {
EdDSATicketFieldsToReveal,
ZKEdDSAEventTicketPCD,
ZKEdDSAEventTicketPCDClaim,
ZKEdDSAEventTicketPCDPackage,
ZKEdDSAEventTicketPCDTypeName
} from "@pcd/zk-eddsa-event-ticket-pcd";
import { ZuAuthArgs } from ".";

/**
* Check if a given field is defined.
*/
function checkIsDefined<T>(
field: T | undefined,
fieldName: string
): field is T {
if (field === undefined || field === null) {
throw new Error(
`Field "${fieldName}" is undefined but should have a value`
);
}
return true;
}

/**
* Check if a given field is undefined.
*/
function checkIsUndefined(field: unknown, fieldName: string): boolean {
if (field !== undefined) {
throw new Error(
`Field "${fieldName}" is defined but should not have a value`
);
}
return true;
}

/**
* Check if an individual configuration matches the claim from the PCD.
*/
function claimMatchesConfiguration(
claim: ZKEdDSAEventTicketPCDClaim,
config: PipelineZuAuthConfig
): boolean {
return (
isEqualEdDSAPublicKey(claim.signer, config.publicKey) &&
claim.partialTicket.eventId === config.eventId &&
(config.productId === undefined ||
claim.partialTicket.productId === config.productId)
);
}

const revealedFields: Record<
keyof EdDSATicketFieldsToReveal,
keyof ITicketData
> = {
revealAttendeeEmail: "attendeeEmail",
revealAttendeeName: "attendeeName",
revealAttendeeSemaphoreId: "attendeeSemaphoreId",
revealEventId: "eventId",
revealIsConsumed: "isConsumed",
revealIsRevoked: "isRevoked",
revealProductId: "productId",
revealTicketCategory: "ticketCategory",
revealTicketId: "ticketId",
revealTimestampConsumed: "timestampConsumed",
revealTimestampSigned: "timestampSigned"
} as const;

/**
* Authenticates a ticket PCD.
Expand All @@ -20,9 +83,11 @@ import {
*/
export async function authenticate(
pcdStr: string,
watermark: string,
config: PipelineEdDSATicketZuAuthConfig[]
{ watermark, config, fieldsToReveal, externalNullifier }: ZuAuthArgs
): Promise<ZKEdDSAEventTicketPCD> {
/**
* Check to see if our inputs are valid, beginning with the PCD.
*/
const serializedPCD = JSON.parse(pcdStr);
if (serializedPCD.type !== ZKEdDSAEventTicketPCDTypeName) {
throw new Error("PCD is malformed or of the incorrect type");
Expand All @@ -34,37 +99,68 @@ export async function authenticate(
throw new Error("ZK ticket PCD is not valid");
}

if (pcd.claim.watermark.toString() !== watermark) {
throw new Error("PCD watermark doesn't match");
/**
* The configuration array must not be empty.
*/
if (config.length === 0) {
throw new Error("Configuration is empty");
}

const publicKeys = config.map((em) => em.publicKey);
const productIds = new Set(
// Product ID is optional, so it's important to filter out undefined values
config
.map((em) => em.productId)
.filter((productId) => productId !== undefined)
);

if (
publicKeys.length > 0 &&
!publicKeys.find((pubKey) =>
isEqualEdDSAPublicKey(pubKey, pcd.claim.signer)
)
) {
/**
* Check if the external nullifier matches the configuration.
*/
if (externalNullifier !== undefined) {
if (pcd.claim.externalNullifier === undefined) {
throw new Error(
"PCD is missing external nullifier when one was provided"
);
}
if (
pcd.claim.externalNullifier.toString() !== externalNullifier.toString()
) {
throw new Error("External nullifier does not match the provided value");
}
} else if (pcd.claim.externalNullifier !== undefined) {
throw new Error(
"Signing key does not match any of the configured public keys"
"PCD contains an external nullifier when none was provided"
);
}

if (
productIds.size > 0 &&
pcd.claim.partialTicket.productId &&
!productIds.has(pcd.claim.partialTicket.productId)
) {
throw new Error(
"Product ID does not match any of the configured product IDs"
);
if (pcd.claim.watermark !== watermark.toString()) {
throw new Error("PCD watermark does not match");
}

checkIsUndefined(pcd.claim.validEventIds, "validEventIds");

/**
* Check that the revealed fields in the PCD match the expectations set out
* in {@link revealedFields}. This is to ensure the consistency between the
* configuration passed to this function, and the configuration used on the
* client-side when generating the PCD.
*/
for (const [revealedField, fieldName] of Object.entries(revealedFields)) {
if (fieldsToReveal[revealedField as keyof EdDSATicketFieldsToReveal]) {
checkIsDefined(pcd.claim.partialTicket[fieldName], fieldName);
} else {
checkIsUndefined(pcd.claim.partialTicket[fieldName], fieldName);
}
}

/**
* Our inputs are formally valid. Now we check to see if any of the
* configuration patterns match the claim in the PCD.
*/
let match = false;

for (const em of config) {
if (claimMatchesConfiguration(pcd.claim, em)) {
match = true;
break;
}
}

if (!match) {
throw new Error("PCD does not match any of the configured patterns");
}

return pcd;
Expand Down
15 changes: 12 additions & 3 deletions packages/lib/zuauth/src/zuauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export function zuAuthRedirect(args: ZuAuthRedirectArgs): void {
*/
export function constructZkTicketProofUrl(zuAuthArgs: ZuAuthArgs): string {
const {
zupassUrl = "https://zupass.org",
zupassUrl = "https://zupass.org/",
returnUrl,
fieldsToReveal,
watermark,
Expand Down Expand Up @@ -114,6 +114,16 @@ export function constructZkTicketProofUrl(zuAuthArgs: ZuAuthArgs): string {
publicKeys.push(em.publicKey);
}

if (!fieldsToReveal.revealEventId) {
throw new Error("The event ID must be revealed for authentication");
}

if (productIds.length > 0 && !fieldsToReveal.revealProductId) {
throw new Error(
"When product IDs are specified for authentication, the product ID field must be revealed"
);
}

const args: ZKEdDSAEventTicketPCDArgs = {
ticket: {
argumentType: ArgumentTypeName.PCD,
Expand All @@ -135,8 +145,7 @@ export function constructZkTicketProofUrl(zuAuthArgs: ZuAuthArgs): string {
},
validEventIds: {
argumentType: ArgumentTypeName.StringArray,
value:
eventIds.length !== 0 && eventIds.length <= 20 ? eventIds : undefined,
value: undefined,
userProvided: false
},
fieldsToReveal: {
Expand Down
Loading

0 comments on commit f43d4ed

Please sign in to comment.