-
Notifications
You must be signed in to change notification settings - Fork 77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Throw exceptions when event ID doesn't match configuration #1802
Changes from 14 commits
250e623
cc2b5e5
628c7c0
1dbf722
c6b914f
fe70abe
ac7f09a
f033c9f
1c544d3
e07f3bf
312aa8a
6831f58
cb6f324
ae9ecf1
d483bbf
a98d8d7
f8c1d92
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 and should have a revealed 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 and should not have a revealed 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 && | ||
claim.partialTicket.productId === config.productId | ||
); | ||
} | ||
|
||
const revealedFields: Record< | ||
keyof EdDSATicketFieldsToReveal, | ||
keyof ITicketData | ||
> = { | ||
revealAttendeeEmail: "attendeeEmail", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be done programmatically rather than with a lookup table. output = input.slice(6, 7).toLowerCase() + input.slice(7); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes logical sense, but TypeScript really wants keys into objects to have the correct types. The key type for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You could do it with an |
||
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. | ||
|
@@ -20,9 +83,11 @@ import { | |
*/ | ||
export async function authenticate( | ||
pcdStr: string, | ||
watermark: string, | ||
config: PipelineEdDSATicketZuAuthConfig[] | ||
{ watermark, config, fieldsToReveal, externalNullifier }: ZuAuthArgs | ||
): Promise<ZKEdDSAEventTicketPCD> { | ||
/** | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: I think the double There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If I don't use the double-asterisk, then VSCode does not insert a new asterisk at the beginning of the subsequent lines. From that I inferred a distinction between using |
||
* 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"); | ||
|
@@ -34,37 +99,66 @@ 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"); | ||
} | ||
|
||
/** | ||
* 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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Suggestion without the lookup table: for (const [revealedFieldName, shouldReveal] of Object.entries(fieldsToReveal)) {
checkIsDefined(
pcd.claim.partialticket[revealedFieldName.slice(6, 7).toLowerCase() + revealedFieldName.slice(7)],
revealedFieldName
);
} |
||
if (fieldsToReveal[revealedField as keyof EdDSATicketFieldsToReveal]) { | ||
checkIsDefined(pcd.claim.partialTicket[fieldName], fieldName); | ||
} else { | ||
checkIsUndefined(pcd.claim.partialTicket[fieldName], fieldName); | ||
} | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you're now desupporting validEventIds, should you check here that it's required to be undefined? I don't think it changes the security (given you're checking that all the necessary fields are being revealed), but it would catch a configuration error. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've added a check for this too. |
||
/** | ||
* 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; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -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, | ||
|
@@ -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, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is losing a feature which we advertized at Devconnect. How sure are you that nobody is depending on it? It would be possible to keep using this field when you know it's sufficient (number of eventIDs is <=20, and no productIDs are used). That being said, I don't like that it was an implicit feature before. It would be better if it were an explicit configuration choice by the developer to use this feature, with the library throwing an exception if the dev tries to use this feature with an overlarge list. Note that when we get to PODTickets we can have longer lists, and also triples of (key, eventID, productID), so maybe looking ahead to that is the right way to not worry too much about getting this perfect. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ZuAuth was not used at Devconnect, because it didn't exist then. The underlying code in My thinking was that That said, I can make it an optionally-configurable feature that is off by default, which seems like the best of both worlds. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm. After implementing this, I remember why I didn't want to do it: the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Understood. The limitation of that feature to validEventIds does assume that the signing key and product ID are either always a single value (e.g. signing key for last year's events), or don't matter for authentication (e.g. the product IDs for devconnect). It would be possible to check for those scenarios, or leave them up to the user to configure properly. However given comments elsewhere I'm okay with removing this so long as it's not affecting the use cases which were built to depend on it (like ZuRat for Devconnect). With PODTickets we can bring back a much more flexible version of this. |
||
userProvided: false | ||
}, | ||
fieldsToReveal: { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This means a config with no productID never matches? Seems like you might want an OR here for the case where productID is undefined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ping on this comment? Am I misreading this?