diff --git a/docs.json b/docs.json index 07d2423..a89dbec 100644 --- a/docs.json +++ b/docs.json @@ -171,10 +171,15 @@ "group": "Sign in with World ID", "pages": ["world-id/sign-in/oidc"] }, + { + "group": "Credential Issuance", + "pages": ["world-id/credential-issuance/refresh"] + }, { "group": "Technical Reference", "pages": [ "world-id/reference/idkit", + "world-id/reference/credential", "world-id/reference/api", "world-id/reference/sign-in", "world-id/reference/contracts", diff --git a/world-id/concepts.mdx b/world-id/concepts.mdx index 58ade78..f266a55 100644 --- a/world-id/concepts.mdx +++ b/world-id/concepts.mdx @@ -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) - **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. diff --git a/world-id/credential-issuance/refresh.mdx b/world-id/credential-issuance/refresh.mdx new file mode 100644 index 0000000..ad29bc9 --- /dev/null +++ b/world-id/credential-issuance/refresh.mdx @@ -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. + + + 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. + + +## Endpoint + + + /api/v1/refresh + + +**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:///api/v1/refresh?idComm=0xabc123..." \ + -H "x-zkp-proof: " \ + -F "sub=0x1a2b3c" +``` + +### Example (refresh with PCP) + +```bash +curl -X POST "https:///api/v1/refresh?idComm=0xabc123..." \ + -H "x-zkp-proof: " \ + -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": "", + "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 + +```javascript +const decoded = JSON.parse(Buffer.from(credential, "base64").toString("utf8")); +``` + +### Example (decoded) + +```json +{ + "id": 123456789, + "version": "V1", + "issuer_schema_id": 42, + "sub": "", + "genesis_issued_at": 1733241600, + "expires_at": 1764777600, + "claims": ["", "", ""], + "associated_data_hash": "", + "signature": "", + "issuer": { + "pk": ["", ""] + } +} +``` + +### 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 + +- 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) + diff --git a/world-id/reference/credential.mdx b/world-id/reference/credential.mdx new file mode 100644 index 0000000..ed4dc80 --- /dev/null +++ b/world-id/reference/credential.mdx @@ -0,0 +1,128 @@ +--- +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. + + + The refresh endpoint returns a base64-encoded Credential. See + [Credential Issuance](/world-id/reference/credential) for issuance details. + + +## 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"] + } +} +``` + + + The `claims` array is fixed-length (16 entries) and padded with the zero field + element. The example above is shortened for readability. + + +### 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. | + + + 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. + + +### 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) +