A credential is made from header, body, an array of signatures and attachments. This design allows a credential to be endorsed by multiple signatures from different issuers, or using different verification stacks. Credentials are primarily stored and presented to holders in JSON format, for readability, while other formats like YAML, XML can also be supported. Note the formats of storing and showing the credential are not related to how the digest (i.e., the hash value) is computed, and will not affect zero-knowledge proof generation. The digest algorithm is determined by the verification stack and it can evolve without upgrading the main protocol.

                +----------------------------------------+
        Header  |       Version, Type, Context, ID       |
                +----------------------------------------+
          Body  |                (Field)*                |
                +----------------------------------------+
                |    Metadata      |      Signature      |
  Signature(s)  |----------------------------------------|
                |         ...more signatures...          |
                +----------------------------------------+
  Attachments   |        Signer ID, Public key....       |
                +----------------------------------------+
{
  "header": {
    "version": "1",
    "type": "778",
    "context": "666",
    "id": "9"
  },
  "body": {
    "token_balance": "100",
    "birthday": "200",
    "status": {
      "str": "enabled",
      "value": "2"
    },
    "followed": "true"
  },
  "signatures": [sig1, sig2...]
}
{
  "metadata": {
    "verification_stack": 1,
    "signature_id": "999",
    "expired_at": "100",
    "identity_commitment": "123",
    "issuer_id": "456",
    "chain_id": "0",
    "public_key": "public key bytes"
  },
  "signature": "signature bytes"
}

Header contains the common fields that all every credential must have.

  • Version (uint8) : The version of protocol. Currently, the only supported version is 1.
  • Type (uint160) : The type ID this credential, specifying the order and type of the claims in the body. This value will be a public signal of a proof.
  • Context (uint160) : The context ID of the credential. This context ID needs to be registered on-chain before using it. Issuers are allowed to re-use the context created by others. Context IDs are computed by taking the 160 least significant bits of the keccak256 hash of the context string. Optionally, the issue can attach the original context string in the attachments section for better readability. This value will be a public signal of a proof.
  • ID (uint248) : An identifier of holder. For example, an EVM/Bitcoin/Solana/Cosmos/Mina address, a X (Twitter) handle, or a Discord handle. If the original identity is a string, issuer must convert it to an integer value, by taking the 248 least significant bits of the output of some collision-resistant hash function, or ways that can guarantee the 1-to-1 mapping from the original handle to the ID. For example, 160-bit EVM address can be directly used as IDs. This value does not need to be registered on-chain. It will not be a public signal of a proof. Instead, the proof will contains a equality check result that shows the ID equals or not equals to some ID.

For example, to create a credential of the loyalty points, that a Galxe.com user has earned in the Galxe space, assuming that we have registered the context “Galxe.com loyalty points” on-chain and the context ID is 0xdeadbeef...., when the user’s Galxe ID is 123123 , Here is an example header. Note that Its type ID is 2, meaning that it is using the primitive credential type containing just one scalar value.

"header": {
  "version": "1",
  "type": "2",
  "context": "0xdeadbeef..",
  "id": "123123"
 }

Body

The body part stores all claim values, corresponding to the claim types declared in the credential type. Below is an example of credential body and its type definition. Note that the status claim value stores not only the hash value, but also the original value. This is because that the claim declaration specify that the hash algorithm is user-defined ( c), storing both values is mandatory by protocol’s specification.

  "body": {
    "token_balance": "100",
    "birthday": "200",
    "status": {
      "str": "enabled",
      "value": "2"
    },
    "followed": "true"
  },
token_balance:uint<256>;
birthday:uint<64>;
status:prop<32,c,1>;
followed:bool;

Signature

The signature part of a credential is an array of pairs of metadata and signature, allowing one credential to have multiple signatures. Although the format of a signature is specified by the verification stack used by the issuer, all signatures shares a common metadata structure.

Signed signature metadata

  • verification_stack_id (uint8): an ID of verification stack used for this credential.
  • signature_id (uint248): the id of this signature that will be used to manage revocation. This value will never be exported as a public value for user’s privacy. It will only be used in the circuit when proving that the credential is not revoked, if required.
  • expiration (uint64): the UNIX timestamp of when the credential will be expired. This field is mandatory and you can fill-in an UINT64_MAX if the credential never expires.
  • identity_commitment (uint256): a field serves as the identity commitment for zero-knowledge proofs. The algorithm of computing the value is depending the chosen verification stack.

Identity commitment will be used as the key to the power of creating a link from an identity to another one, e.g., from a twitter handle to an EVM address. At verification, a holder must generate a proof that he is the actual owner of the credential, by showing that he knows the secrets behind the commitment, and output a number representing the pseudonymous identity the verifier will see. Thus, the verifier is assured that the verification originates from the holder and confirms the holder’s identity as the entity communicating with the verifier in that session.

Note that issuer_id is not part of the signed metadata. In our design, the issuer_id is intentionally omitted. This approach prevents the embedding of trust relationships between the issuer and public keys within the credential, because the verification of a public key trustworthiness can only occur externally to the zero-knowledge proof circuit, as it involves stateful checks beyond cryptographic validation. This simplification eliminates the redundancy of including an issuer_id for zk proof generation. Furthermore, this flexibility enhances the system’s scalability and the fluidity of trust relationships, thereby maintaining the simplicity of the credential’s structure while accommodating a dynamic network of trust.

Unsigned signature metadata

In the unsigned signature part, there are three mandatory fields: issuer_id, chain_id and public_key. They do not require explicit signing, because the issuer’s signature inherently implies the public key value, and issuer_id and chain_id should not be signed and fixed.

  • issuer_id (uint248): Represents the unique identifier of the issuer.
  • chain_id (uint64): Represents the ID of the primary chain that the issuer was registered. Because the protocol is chain-agnostic and an issuer can register themselves in different chains using a consistent issuer_id, this primary chain ID is only a suggestion from issuer to verifier about where to check the public key status and revocable credential state. Note that although chain id may a 256-bit value, we do not support chain id larger than uint64_max.
  • public_key (specific to the verification stack): Contains public keys essential for signature validation without querying it from the network.
  • Additional fields as deemed necessary by the issuer.

Attachments

The “attachments” section houses values that are external to the main body of the credential. It means that these values are not authenticated (or signed) by the issuer by default. However, issuers have the freedom to add an signature attachment that signs all the other attached fields, using a preferred signature algorithm. The protocol do have any specification for this but may introduce one in the future.

Issuers have the flexibility to introduce any arbitrary fields to this section. For instance, an issuer could provide a passport credential accompanied by a PNG photo. In such a case, issuer can place the hash of the photo in the credential body, while the actual binary data of the image would reside in the attachments.

Proof

Credential holders can generate zero-knowledge proofs to selectively disclose specific pieces of information on credentials. Regardless of the chosen verification stack, every proof consists of two main components: a proof and some public inputs. The structure and representation of a proof is varying by the verification stack. There are two types of public inputs: intrinsic public signals and user-defined signals. Intrinsic signals must be the first 8 or 9 signals (depending on if it is revocable) of the public input array. The protocol mandates the sequence of intrinsic signals as described below:

  1. out_type (uint160): The type ID.
  2. out_context (uint160): The context ID.
  3. out_nullifier (uint256): A value derived from both internal_nullifier and external_nullifier. When using the babyzk verification stack, this value is computed as poseidon(internal_nullifier, external_nullifier).
  4. out_external_nullifier (uint160): The external nullifier used to generate the proof.
  5. out_reveal_identity (uint256): The identity endorsed by the credential holder. Verifiers are obliged to ensure this value aligns with the session user’s identity. For instance, for on-chain verification on EVMs, this value should be the direct caller, represented by msg.sender, or indirectly be authenticated through an EIP-712 signature.
  6. out_expiration_lb (uint64): This signal delineates the lower boundary of the credential’s expiration date.
  7. out_key_id (uint256): Represents the hash of the public key that authenticated the credential.
  8. out_id_equals_to (uint256): This field confirms the equality of the ID field. It adopts a compressed schema similar to prop<248, c, 1>. Here, the least significant bit designates equality, while its [249..1] bits convey the value that underwent comparison.
  9. (optional) out_sig_revocation_smt_root (uint256): Only if the credential is revocable, there will be this intrinsic public signal, representing the hash of the root of the sparse merkle tree used provided during proof generation.
  10. Public inputs introduced by claims in the body will start from here.
{
  "out_type": "778",
  "out_context": "666",
  "out_nullifier": "20587068051456727584720549518481563004145412893468610943364726469406950307723",
  "out_external_nullifier": "20565030651454249290584727781400485209601570980842971037547488913190033044629",
  "out_reveal_identity": "0xdeadbeef",
  "out_expiration_lb": "99",
  "out_key_id": "1743582416365651167392966598529843347617363862106697818328310770809664607117",
  "out_id_equals_to": "19",
  "out_sig_revocation_smt_root": "1243904711429961858774220647610724273798918457991486031567244100767259239747",
  "out_token_balance_lb_msb": "0",
  "out_token_balance_lb_lsb": "50",
  "out_token_balance_ub_msb": "0",
  "out_token_balance_ub_lsb": "101",
  "out_birthday_lb": "199",
  "out_birthday_ub": "201",
  "out_status_eq0": "6",
  "out_status_eq1": "8",
  "out_followed": "0"
}