Skip to content
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

Ticket proof configuration docs #13

Merged
merged 5 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/docs/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ export default defineConfig({
items: [
// Each item here is one entry in the navigation menu.
{ label: "Introduction", slug: "guides/introduction" },
{ label: "Getting Started", slug: "guides/getting-started" }
{ label: "Getting Started", slug: "guides/getting-started" },
{
label: "Making Proofs about Ticket PODs",
slug: "guides/ticket-proofs"
}
]
},
typeDocSidebarGroup
Expand Down
235 changes: 235 additions & 0 deletions apps/docs/src/content/docs/guides/ticket-proofs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
---
title: Making Proofs about Ticket PODs
description: How to use POD tickets for authentication
---
import { Aside } from '@astrojs/starlight/components';
import { PackageManagers } from "starlight-package-managers";

Zupass tickets are [PODs](https://www.pod.org), Provable Object Datatypes. This means that they're ZK-friendly, and it's easy to make proofs about them.

ZK proofs in Zupass are used to reveal information about some POD-format data that the user has. This might involve directly revealing specific POD entries, such as the e-mail address associated with an event ticket. Or, it might involve revealing that an entry has a value in a given range, or with a value that matches a list of valid values. Or, it might even involve revealing that a set of related entry values match a sets of values from a list.

<Aside type="note" title="What are entries?">
PODs are collections of "entries", which are name and value pairs, where values have a type of `string` (for text), `int` (for integers), `cryptographic` (for very large integers), and `eddsa_pubkey` (for EdDSA public keys).

Zero-knowledge proofs in Zupass work by proving that a POD's entries match certain criteria, without necessarily revealing _how_ they match it. For example, you could prove that a `birthdate` entry of type `int` is below a certain number, indicating that a person was born before a certain date, without revealing what the `birthdate` value is.
</Aside>

Zupass allows you to configure proofs by providing a set of criteria describing one or more PODs that the user must have. If the user has matching PODs, they can make a proof and share it with someone else - such as with your application or another user of your app. As the app developer, it's your job to design the proof configuration.

Because this system is very flexible, it can also be intimidating at first. Mistakes can be subtle, and might result in a proof that doesn't really prove what you want it to. To help with this, there's a specialized library for preparing ticket proof requests, `ticket-spec`. This guide will explain how to use it.

# Quick Start

Below you will find more detailed background on ticket proofs. But if you just want to get started, here's how to do it:

Following the [Getting Started](getting-started) guide, install the App Connector module and verify that you can connect to Zupass.

Next, install the `ticket-spec` package:

<PackageManagers
frame="none"
pkgManagers={["npm", "yarn", "pnpm", "bun"]}
pkg="@parcnet-js/ticket-spec"
/>

Then, you can create a ticket proof request, and use it to create a proof:

```ts wrap=true title="src/main.ts"
import { ticketProofRequest } from "@parcnet-js/ticket-spec";

const request = ticketProofRequest({
classificationTuples: [
// If you know the public key and event ID of a POD ticket, you can specify
// them here, otherwise delete the object below.
{
signerPublicKey: "PUBLIC_KEY",
eventId: "EVENT_ID"
}
],
fieldsToReveal: {
// The proof will reveal the attendeeEmail entry
attendeeEmail: true
},
externalNullifier: {
type: "string",
value: "APP_SPECIFIC_NULLIFIER"
}
});

const gpcProof = await z.gpc.prove(request);
```

This will create a GPC proof which proves that the ticket has a given signer public key and event ID, and also reveals the `attendeeEmail` address for the ticket.

If you know the signer public key and event ID of a ticket then you can specify those in the `classificationTuples` array, but for getting started you can delete those values if you don't know which ones to specify.

The result should be an object containing `proof`, `boundConfig`, and `revealedClaims` fields. For now, we can ignore `proof` and `boundConfig`, and focus on `revealedClaims`, which, given the above example, should look like this:

```ts wrap=true
{
"pods": {
"ticket": {
"entries": {
"attendeeEmail": {
"type": "string",
"value": "[email protected]"
}
},
"signerPublicKey": "MolS1FubqfCmFB8lHOSTo1smf8hPgTPal6FgpajFiYY"
}
},
"owner": {
"externalNullifier": {
"type": "string",
"value": "APP_SPECIFIC_NULLIFIER"
},
"nullifierHashV4": 18601332455379395925267579735435017582946383130668625217012137367106027237345
},
"membershipLists": {
"allowlist_tuple_ticket_entries_$signerPublicKey_eventId": [
[
{
"type": "eddsa_pubkey",
"value": "MolS1FubqfCmFB8lHOSTo1smf8hPgTPal6FgpajFiYY"
},
{
"type": "string",
"value": "fca101d3-8c9d-56e4-9a25-6a3c1abf0fed"
}
]
]
}
}
```

As we saw in the original proof request, the `attendeeEmail` entry is revealed: `revealedClaims.pods.ticket.entries.attendeeEmail` contains the value.

To understand how this works, read on!

# Configuring a ticket proof request

Ticket proofs enable the user to prove that they hold an event ticket matching certain criteria. They also allow the user to selectively reveal certain pieces of data contained in the ticket, such as their email address or name.

A typical use-case for ticket proofs is to prove that the user has a ticket to a specific event, and possibly that the ticket is of a certain "type", such as a speaker ticket or day pass. To determine this, we need to specify two or three entries:
- Public key of the ticket signer or issuer
- The unique ID of the event
- Optionally, the unique ID of the product type, if the event has multiple types of tickets

For example, if you want the user to prove that they have a ticket to a specific event, then you want them to prove the following:

- That their ticket POD was signed by the event organizer's ticket issuance key
- That their ticket has the correct event identifier
- That their ticket is of the appropriate type, if you want to offer a different experience to holders of different ticket types

## How to specify ticket details

To match a ticket based on the above criteria, you must specify _either_ pairs of public key and event ID, or triples of public key, event ID, and product ID. For example:

```ts wrap=true title="src/main.ts"
const pair = [{
publicKey: "2C4B7JzSakdQaRlnJPPlbksW9F04vYc5QFLy//nuIho",
eventId: "ab9306be-019f-40d9-990d-88826a15fde5"
}];
const triple = [{
publicKey: "2C4B7JzSakdQaRlnJPPlbksW9F04vYc5QFLy//nuIho",
eventId: "ab9306be-019f-40d9-990d-88826a15fde5",
productId: "672c6ff1-9947-41d4-8876-4ef1e3317f08"
}];
```

The first example, containing only a public key and event ID, will match any ticket which has those attributes. The second is more precise, requiring that the ticket have a specific product type.

It's possible to specify _multiple_ pairs or triples. For example:
```ts wrap=true title="src/main.ts"
const pairs = [
{
publicKey: "2C4B7JzSakdQaRlnJPPlbksW9F04vYc5QFLy//nuIho",
eventId: "ab9306be-019f-40d9-990d-88826a15fde5"
},
{
publicKey: "2C4B7JzSakdQaRlnJPPlbksW9F04vYc5QFLy//nuIho",
eventId: "5ddb8781-b893-4187-9044-9ac229368aac"
}
]
```

These would be used like this:

```ts wrap=true title="src/main.ts" {2-11}
const request = ticketProofRequest({
classificationTuples: [
{
publicKey: "2C4B7JzSakdQaRlnJPPlbksW9F04vYc5QFLy//nuIho",
eventId: "ab9306be-019f-40d9-990d-88826a15fde5"
},
{
publicKey: "2C4B7JzSakdQaRlnJPPlbksW9F04vYc5QFLy//nuIho",
eventId: "5ddb8781-b893-4187-9044-9ac229368aac"
}
],
fieldsToReveal: {
// The proof will reveal the attendeeEmail entry
attendeeEmail: true
},
externalNullifier: {
type: "string",
value: "APP_SPECIFIC_NULLIFIER"
}
});
```

In this case, the proof request will match any ticket which matches _either_ of the above pairs. If you provide more, then the ticket just needs to match any of the provided pairs.

This underlines an important principle: when the proof is created, you might not know _which_ pair of values the user's ticket matches. This is by design, and is part of how ZK proofs provide privacy. If you only need to know that the user has a ticket matching a list of values, but don't need to know exactly which ticket the user has, then by default that information will not

## What gets revealed in a ticket proof

If you have specified pairs or triples of public key, event ID and (optionally) product ID, then the list of valid values will be revealed in the proof. This might be the _only_ information you want to reveal: the proof discloses that the user has a ticket which satisfies these criteria, but no more.

However, you might want the proof to disclose more information about the ticket. There are two further types of information that a proof might reveal: a "nullifier hash", a unique value derived from the user's identity, and a subset of the ticket's entries.

### Nullifier hash

A nullifier hash is a unique value which is derived from the user's identity, but which cannot be used to determine the user's identity. Typically this is used to pseudonymously identify a user: if the same user creates two proofs, both proofs will have the same nullifier hash, giving the user a consistent identity, but not one that can be used to determine their public key or other information.

The nullifier hash requires an "external nullifier", a value which your application must provide. This ensures that the nullifier hash is derived from _both_ the user's identity and a value that your application provides. This means that the nullifier hash that the user has when creating proofs for your application will be different to the nullifier hash they have when creating proofs for another application.

### Revealed entries

Proofs can also directly reveal the values held in certain entries, meaning that the `revealedClaims` object in the proof result will be populated with values from the ticket that the use selects when making the proof. In the Quick Start example above, the `attendeeEmail` entry is revealed, but you can reveal any of the ticket's entries by setting them in the `fieldsToReveal` parameter:

```ts wrap=true title="src/main.ts" {10-13}
const request = ticketProofRequest({
classificationTuples: [
// If you know the public key and event ID of a POD ticket, you can specify
// them here, otherwise delete the object below.
{
signerPublicKey: "PUBLIC_KEY",
eventId: "EVENT_ID"
}
],
fieldsToReveal: {
// Reveal the unique ticket ID
ticketId: true,
// Reveal the attendee's name
attendeeName: true,
// Reveal if the ticket is checked in
isConsumed: true
},
externalNullifier: {
type: "string",
value: "APP_SPECIFIC_NULLIFIER"
}
});
```

## Watermark

You can add a watermark to your proof, which allows you to uniquely identify a proof. Precisely which value to use for the watermark depends on your application and use-case, but you might use a unique session ID, or a single-use number generated by your application.

If you add a watermark to your proof request, you can check the watermark when later verifying the proof. A typical workflow might involve your client application requesting a random number from your server, which stores the number. The number is passed as a watermark in the proof request, and then you can send the proof to the server for verification. The server then checks that the watermark is equal to the random number it generated. By requiring the watermark to equal some single-use secret value, you ensure that the client cannot re-use a previously-generated proof.

# Verifying a ticket proof

TODO.
27 changes: 18 additions & 9 deletions examples/test-app/src/apis/GPC.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,15 +264,17 @@ await z.pod.insert(pod);
{`
const request = ticketProofRequest({
classificationTuples: [
[
// The public key to match
"${publicKey}",
// The event ID to match
"${EVENT_ID}"
]
{
signerPublicKey: "${publicKey}",
eventId: "${EVENT_ID}"
}
],
fieldsToReveal: {
eventId: true
attendeeEmail: true
},
externalNullifier: {
type: "string",
value: "APP_SPECIFIC_NULLIFIER"
}
});

Expand All @@ -286,10 +288,17 @@ const gpcProof = await z.gpc.prove(request);
try {
const request = ticketProofRequest({
classificationTuples: [
[await z.identity.getPublicKey(), EVENT_ID]
{
signerPublicKey: await z.identity.getPublicKey(),
eventId: EVENT_ID
}
],
fieldsToReveal: {
eventId: true
attendeeEmail: true
},
externalNullifier: {
type: "string",
value: "APP_SPECIFIC_NULLIFIER"
}
});
setProveResult(await z.gpc.prove(request.schema));
Expand Down
4 changes: 4 additions & 0 deletions mprocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ procs:
cwd: "examples/test-app"
shell: "pnpm dev"
autostart: false
docs:
cwd: "apps/docs"
shell: pnpm dev
autostart: false
keymap_procs: # keymap when process list is focused
<1>: { c: select-proc, index: 0 }
<2>: { c: select-proc, index: 1 }
Expand Down
12 changes: 6 additions & 6 deletions packages/podspec/src/gpc/proof_request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,12 +161,12 @@ function makeProofRequest<P extends NamedPODs>(

const entryConfig: GPCProofEntryConfig = {
isRevealed,
isMemberOf: isMemberOf
? `allowlist_${podName}_${entryName}`
: undefined,
isNotMemberOf: isNotMemberOf
? `blocklist_${podName}_${entryName}`
: undefined,
...(isMemberOf
? { isMemberOf: `allowlist_${podName}_${entryName}` }
: {}),
...(isNotMemberOf
? { isNotMemberOf: `blocklist_${podName}_${entryName}` }
: {}),
...(inRange ? { inRange: entrySchema.inRange } : {}),
...(isOwnerID ? { isOwnerID: owner.protocol } : {})
};
Expand Down
2 changes: 2 additions & 0 deletions packages/podspec/test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,7 @@ export function generateKeyPair(): { privateKey: string; publicKey: string } {
const publicKey = encodePublicKey(
derivePublicKey(decodePrivateKey(privateKey))
);
console.log("privateKey", privateKey);
console.log("publicKey", publicKey);
return { privateKey, publicKey };
}
2 changes: 2 additions & 0 deletions packages/ticket-spec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,9 @@
"devDependencies": {
"@parcnet-js/eslint-config": "workspace:*",
"@parcnet-js/typescript-config": "workspace:*",
"@pcd/gpc": "0.0.8",
"@pcd/pod": "0.1.7",
"@pcd/proto-pod-gpc-artifacts": "^0.9.0",
"@semaphore-protocol/core": "^4.0.3",
"@semaphore-protocol/identity": "^3.15.2",
"@types/uuid": "^9.0.0",
Expand Down
Loading
Loading