-
Notifications
You must be signed in to change notification settings - Fork 9
docs for credential refresh / issuance for PoH #38
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
base: main
Are you sure you want to change the base?
Changes from all commits
3c39784
44e75eb
d214306
9374552
19f509b
4a1f881
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 |
|---|---|---|
|
|
@@ -28,11 +28,14 @@ Some terms are used throughout the World ID documentation. Here are a few of the | |
| - **World ID**: A user's self-custodial identity, as well as the name of the protocol. | ||
| - **App ID**: The ID of your app that is assigned in our [Developer Portal](https://developer.worldcoin.org/). | ||
| - **Action**: A developer-facing primitive that lets you put any app operation behind a unique-human gate. An app can have one or more actions depending on your use case. | ||
| - **Issuer**: An entity authorized to issue a credential for a specific schema. Issuers sign credentials and publish their public keys in the `CredentialSchemaIssuerRegistry`. | ||
| - **Credential**: A signed attestation about a subject used to generate proofs. It includes issuer, subject, validity window, and claim commitments as defined in the [World ID 4.0 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) | ||
|
Contributor
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. let's point instead to the source of truth for what is a credential, https://docs.rs/world-id-primitives/latest/world_id_primitives/credential/struct.Credential.html |
||
| - **Zero-Knowledge Proof (ZKP)**: A cryptographic method to prove that a statement is true without revealing any information about the statement itself. World ID uses ZKPs to prove that a user is verified without revealing the user's identity. | ||
| - **Nullifier Hash**: A component of the World ID ZKP; a unique identifier for a combination of a user, `app_id`, and `action`. | ||
| - **Signal**: A component of the World ID ZKP; data attached to the proof that cannot be tampered with. An example may be a user's choice for an election. | ||
| - **Merkle Root**: A component of the World ID ZKP; The root of the [Merkle Tree](https://en.wikipedia.org/wiki/Merkle_tree) that identity commitments are inserted to. | ||
|
|
||
|
|
||
| It's important to note the difference between the two types of verification, depending on the context: | ||
|
|
||
| - **Orb/Passport Verification**: A user's identity can be verified through either an Orb, Device or Passport and their identity commitment is then recorded on the blockchain. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| --- | ||
| title: "Credential Refresh" | ||
| description: "Refresh endpoint for issuing World ID PoH credentials and the Credential object format." | ||
| "og:image": "/images/docs/docs-meta.png" | ||
| "twitter:image": "/images/docs/docs-meta.png" | ||
| --- | ||
|
|
||
| This endpoint issues a new proof-of-human (PoH) credential to a holder of a valid World ID. It can re-verify with a Personal Custody Package (PCP) or issue a credential-only refresh when a PCP is not available. | ||
|
Contributor
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. It's not clear that this endpoint is only intended for v3 users to get their v4 credential |
||
|
|
||
| <Note> | ||
| Base URL is environment-specific and served by the signup-service app-api. Contact | ||
| your World ID point of contact for environment endpoints and access. | ||
| </Note> | ||
|
|
||
| ## Endpoint | ||
|
|
||
| <ParamField path="method" type="POST"> | ||
| /api/v1/refresh | ||
| </ParamField> | ||
|
|
||
| **Content-Type:** `multipart/form-data` | ||
|
|
||
| ## Request | ||
|
|
||
| ### Headers | ||
|
|
||
| | Header | Type | Required | Description | | ||
| | --- | --- | --- | --- | | ||
| | `x-zkp-proof` | `string` | yes | Base64-encoded ZKP string containing `idCommitment` and `sub`. | | ||
|
|
||
| ### Query parameters | ||
|
|
||
| | Query | Type | Required | Description | | ||
| | --- | --- | --- | --- | | ||
| | `idComm` | `string` | yes | User identity commitment. | | ||
|
|
||
| ### Form fields (always required) | ||
|
|
||
| | Field | Type | Required | Description | | ||
| | --- | --- | --- | --- | | ||
| | `sub` | `string` | yes | User account ID (hex with `0x` prefix). Must match previous refreshes for this `idComm`. | | ||
| | `encrypted_user_id` | `string` | no | Temporary field while operator rewards depend on it. | | ||
|
|
||
| ### PCP form fields (required only when submitting a PCP) | ||
|
|
||
| Include all fields below when refreshing with a Personal Custody Package. | ||
|
|
||
| | Field | Type | Description | | ||
| | --- | --- | --- | | ||
| | `signup_id` | `string` | Signup ID. | | ||
| | `signup_id_salt` | `string` | Signup ID salt. | | ||
| | `orb_id` | `string` | Orb ID. | | ||
| | `orb_id_salt` | `string` | Orb ID salt. | | ||
| | `operator_id` | `string` | Operator ID. | | ||
| | `operator_id_salt` | `string` | Operator ID salt. | | ||
| | `signup_reason` | `string` | Signup reason. | | ||
| | `signup_reason_salt` | `string` | Signup reason salt. | | ||
| | `timestamp` | `string` | Timestamp. | | ||
| | `timestamp_salt` | `string` | Timestamp salt. | | ||
| | `software_version` | `string` | Software version. | | ||
| | `software_version_salt` | `string` | Software version salt. | | ||
| | `orb_country` | `string` | Orb country. | | ||
| | `orb_country_salt` | `string` | Orb country salt. | | ||
| | `iris_code_shares_0` | `string` | Iris code share 0. | | ||
| | `iris_code_shares_1` | `string` | Iris code share 1. | | ||
| | `iris_code_shares_2` | `string` | Iris code share 2. | | ||
| | `hashes.json` | `file` | PCP hashes JSON file. | | ||
| | `hashes.sign` | `file` | PCP hashes signature file. | | ||
|
|
||
| ### Example (credential-only refresh) | ||
|
|
||
| ```bash | ||
| curl -X POST "https://<app-api-host>/api/v1/refresh?idComm=0xabc123..." \ | ||
| -H "x-zkp-proof: <base64-zkp>" \ | ||
| -F "sub=0x1a2b3c" | ||
| ``` | ||
|
|
||
| ### Example (refresh with PCP) | ||
|
|
||
| ```bash | ||
| curl -X POST "https://<app-api-host>/api/v1/refresh?idComm=0xabc123..." \ | ||
| -H "x-zkp-proof: <base64-zkp>" \ | ||
| -F "sub=0x1a2b3c" \ | ||
| -F "signup_id=signup_123" \ | ||
| -F "signup_id_salt=..." \ | ||
| -F "orb_id=orb_abc" \ | ||
| -F "orb_id_salt=..." \ | ||
| -F "operator_id=operator_123" \ | ||
| -F "operator_id_salt=..." \ | ||
| -F "signup_reason=..." \ | ||
| -F "signup_reason_salt=..." \ | ||
| -F "timestamp=1700000000" \ | ||
| -F "timestamp_salt=..." \ | ||
| -F "software_version=1.2.3" \ | ||
| -F "software_version_salt=..." \ | ||
| -F "orb_country=US" \ | ||
| -F "orb_country_salt=..." \ | ||
| -F "iris_code_shares_0=..." \ | ||
| -F "iris_code_shares_1=..." \ | ||
| -F "iris_code_shares_2=..." \ | ||
| -F "hashes.json=@hashes.json" \ | ||
| -F "hashes.sign=@hashes.sign" | ||
| ``` | ||
|
|
||
| ## Response | ||
|
|
||
| ### Success response | ||
|
|
||
| ```json | ||
| { | ||
| "success": true, | ||
| "credential": "<base64-credential>", | ||
| "message": "Credential refreshed successfully" | ||
| } | ||
| ``` | ||
|
|
||
| ### Error responses | ||
|
|
||
| | Status | Error | Description | | ||
| | --- | --- | --- | | ||
| | 400 | `INVALID_SUB` | `sub` is missing or invalid. | | ||
| | 400 | `SUB_MISMATCH` | `sub` does not match the one used in previous refreshes. | | ||
| | 404 | `NO_SIGNUP_RECORD` | No enrollment record found. User must re-enroll at an Orb. | | ||
| | 429 | `RATE_LIMIT_EXCEEDED` | Refresh rate limit exceeded for the current window. | | ||
| | 503 | - | Credential refresh is disabled. | | ||
|
|
||
| If PCP validation fails, the endpoint returns an error status with `PCP_VALIDATION_FAILED`. | ||
|
|
||
| ## Credential object format | ||
|
|
||
| The `credential` response field is a base64-encoded JSON representation of the World ID `Credential` object defined in `world-id-protocol/crates/primitives/src/credential.rs`. | ||
|
|
||
| ### Decoding | ||
|
Contributor
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'd drop this section, it's generally self-explanatory with the Credential struct and it may easily become outdated |
||
|
|
||
| ```javascript | ||
| const decoded = JSON.parse(Buffer.from(credential, "base64").toString("utf8")); | ||
| ``` | ||
|
|
||
| ### Example (decoded) | ||
|
|
||
| ```json | ||
| { | ||
| "id": 123456789, | ||
| "version": "V1", | ||
| "issuer_schema_id": 42, | ||
| "sub": "<field-element-hex>", | ||
| "genesis_issued_at": 1733241600, | ||
| "expires_at": 1764777600, | ||
| "claims": ["<field-element-hex>", "<field-element-hex>", "<field-element-hex>"], | ||
| "associated_data_hash": "<field-element-hex>", | ||
| "signature": "<signature-hex>", | ||
| "issuer": { | ||
| "pk": ["<x-decimal>", "<y-decimal>"] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Field definitions | ||
|
|
||
| | Field | Type | Description | | ||
| | --- | --- | --- | | ||
| | `id` | `uint64` | Issuer-scoped reference identifier for the credential. | | ||
| | `version` | `string` | Credential version. Current value is `V1`. | | ||
| | `issuer_schema_id` | `uint64` | Identifier for the (issuer, schema) pair registered in `CredentialSchemaIssuerRegistry`. | | ||
| | `sub` | `FieldElement` | Blinded subject identifier derived from the World ID leaf index and an issuer-specific blinding factor. | | ||
| | `genesis_issued_at` | `uint64` | Unix timestamp (seconds) of the first issuance of this credential. | | ||
| | `expires_at` | `uint64` | Unix timestamp (seconds) for expiration. | | ||
| | `claims` | `FieldElement[]` | Up to 16 claim commitments. Unused indices are the zero field element. | | ||
| | `associated_data_hash` | `FieldElement` | Poseidon2 hash of issuer-defined associated data. The associated data itself is not included. | | ||
| | `signature` | `string` | 64-byte compressed EdDSA signature over the credential hash, hex-encoded (128 hex chars, no `0x`). | | ||
| | `issuer` | `EdDSAPublicKey` | Issuer public key that signed the credential. | | ||
|
|
||
| ### Field representations | ||
|
|
||
| - **FieldElement** values (`sub`, `claims`, `associated_data_hash`) are hex strings with a `0x` prefix and 64 hex characters. | ||
| - **Issuer public key** (`issuer.pk`) is serialized as `[x, y]` decimal strings for BabyJubJub affine coordinates. | ||
| - **Signature** is hex-encoded compressed bytes (no `0x` prefix). | ||
|
|
||
| ### PoH-specific claims | ||
|
Contributor
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 super important to document, but let's keep it only in a single place |
||
|
|
||
| - When refreshing with a PCP, `claims[0]` is derived from the PCP `hashes.json` bytes. | ||
| - When refreshing without a PCP, `claims[0]` is derived from issuer-defined refresh data (a hash of `idCommitment`, `sub`, and a timestamp). | ||
| - In both cases, `associated_data_hash` is instead empty, i.e. the zero field element | ||
|
|
||
| ## References | ||
|
|
||
| - [World ID 4.0 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) | ||
| - [Credential reference](/world-id/reference/credential) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| --- | ||
|
Contributor
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. can we take the general information and move it to the protocol repo? perhaps it's worth having a generic Credential page here, but it should be fully separate from the PoH-specific definitions |
||
| title: "Credential Reference" | ||
| description: "World ID Credential object format, serialization, and PoH-specific usage." | ||
| "og:image": "/images/docs/docs-meta.png" | ||
| "twitter:image": "/images/docs/docs-meta.png" | ||
| --- | ||
|
|
||
| A **Credential** is a signed attestation issued to a subject. It is the canonical object used by authenticators to generate World ID proofs. Relying parties (RPs) do **not** receive the credential itself; they verify proofs derived from it. | ||
|
|
||
| This page documents the Credential object as defined in [world-id-protocol](https://github.com/worldcoin/world-id-protocol/blob/main/crates/primitives/src/credential.rs#L84), plus PoH-specific conventions from the issuer credential structure spec. | ||
|
|
||
| <Note> | ||
| The refresh endpoint returns a base64-encoded Credential. See | ||
| [Credential Issuance](/world-id/reference/credential) for issuance details. | ||
| </Note> | ||
|
|
||
| ## Encoding | ||
|
|
||
| Credentials are serialized as JSON and commonly wrapped in base64 when returned by API endpoints. | ||
|
|
||
| ```javascript | ||
| const decoded = JSON.parse(Buffer.from(credential, "base64").toString("utf8")); | ||
| ``` | ||
|
|
||
| ## Credential schema | ||
|
|
||
| ```json | ||
| { | ||
| "id": 123456789, | ||
| "version": "V1", | ||
| "issuer_schema_id": 42, | ||
| "sub": "0x0000000000000000000000000000000000000000000000000000000000000000", | ||
| "genesis_issued_at": 1733241600, | ||
| "expires_at": 1764777600, | ||
| "claims": [ | ||
| "0x0000000000000000000000000000000000000000000000000000000000000000" | ||
| ], | ||
| "associated_data_hash": "0x0000000000000000000000000000000000000000000000000000000000000000", | ||
| "signature": "5f2e...a3c1", | ||
| "issuer": { | ||
| "pk": ["1234567890", "9876543210"] | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| <Note> | ||
| The `claims` array is fixed-length (16 entries) and padded with the zero field | ||
| element. The example above is shortened for readability. | ||
| </Note> | ||
|
|
||
| ### Field definitions | ||
|
|
||
| | Field | Type | Description | | ||
| | --- | --- | --- | | ||
| | `id` | `uint64` | Issuer-scoped reference identifier. Not exposed to RPs. | | ||
| | `version` | `string` | Credential version. Current value is `V1`. | | ||
| | `issuer_schema_id` | `uint64` | Identifier for the (issuer, schema) pair registered in `CredentialSchemaIssuerRegistry`. | | ||
| | `sub` | `FieldElement` | Blinded subject identifier derived from a World ID leaf index and an issuer-specific blinding factor. | | ||
| | `genesis_issued_at` | `uint64` | Unix timestamp (seconds) of the **first issuance** of this credential. | | ||
| | `expires_at` | `uint64` | Unix timestamp (seconds) for expiration. | | ||
| | `claims` | `FieldElement[]` | Up to 16 claim commitments. Unused indices are the zero field element. | | ||
| | `associated_data_hash` | `FieldElement` | Poseidon2 hash of issuer-defined associated data. The data itself is not included. | | ||
| | `signature` | `string` | 64-byte compressed EdDSA signature over the credential hash, hex-encoded (no `0x` prefix). | | ||
| | `issuer` | `EdDSAPublicKey` | Issuer public key that signed the credential. | | ||
|
|
||
| <Note> | ||
| Claims are included for issuer-defined semantics and may not be enforced by | ||
| proofs today. Associated data is stored by authenticators and is not exposed | ||
| to RPs. | ||
| </Note> | ||
|
|
||
| ### Field representations | ||
|
|
||
| - **FieldElement** values (`sub`, `claims`, `associated_data_hash`) are hex strings with a `0x` prefix and 64 hex characters. | ||
| - **Issuer public key** (`issuer.pk`) is serialized as `[x, y]` decimal strings for BabyJubJub affine coordinates. | ||
| - **Signature** is hex-encoded compressed bytes (128 hex chars). | ||
|
|
||
| ## Hashing and signing | ||
|
|
||
| Credentials are hashed with Poseidon2 and signed using EdDSA over the BabyJubJub curve (V1). | ||
|
|
||
| ``` | ||
| claims_hash = Poseidon2_t16(claims[0..15]) | ||
| cred_hash = Poseidon2_t8( | ||
| DS, | ||
| issuer_schema_id, | ||
| sub, | ||
| genesis_issued_at, | ||
| expires_at, | ||
| claims_hash, | ||
| associated_data_hash, | ||
| id | ||
| ) | ||
| signature = EdDSA_BJJ.sign(cred_hash) | ||
| ``` | ||
|
|
||
| `sub` is computed by hashing the World ID leaf index with a blinding factor: | ||
|
|
||
| ``` | ||
| sub = Poseidon2_t3(DS_SUB, leaf_index, blinding_factor) | ||
| ``` | ||
|
|
||
| ## How credentials are used in proofs | ||
|
|
||
| When an authenticator generates a proof, it includes the credential and enforces: | ||
|
|
||
| - The credential signature matches the issuer key registered in `CredentialSchemaIssuerRegistry`. | ||
| - The credential `sub` matches the blinded leaf index for the holder. | ||
| - The credential is not expired and meets any minimum `genesis_issued_at` constraints. | ||
|
|
||
| This makes credentials portable across authenticators while keeping subjects unlinkable between issuers. | ||
|
|
||
| ## PoH credential usage | ||
|
|
||
| Proof-of-Human (PoH) credentials follow the issuer-defined structure. The current PoH issuer spec states: | ||
|
|
||
| - The user first obtains an Orb credential (currently a PCP in v2.3 format). | ||
| - `claim[0]` is a commitment to the Orb credential, currently `H(hashes.json)`. | ||
| - The PoH credential has no associated data, so `associated_data_hash` is the zero field element. | ||
| - The PoH credential subject is blinded and differs from the Orb credential subject. | ||
| - The issuer may require a proof for the requested `sub` to prevent bricking an identity. | ||
|
|
||
| These details are issuer-specific and may evolve as the protocol migrates to the World ID 4.0 credential format. | ||
|
|
||
| ## References | ||
|
|
||
| - [World ID 4.0 specs](https://github.com/worldcoin/world-id-protocol/tree/main/docs/world-id-4-specs) | ||
|
|
||
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.
let's create a new section altogether for the PoH Issuer. @andy-t-wang sg?