Introduction to Portablegabi NPM

The Portablegabi library provides an easy to use API for signing JSON objects and for verifying and revoking these signatures. This library intends to enable the claimer, who possesses the signed JSON object, to prove to a third party, called verifier, that a specific property is present inside the JSON object and that a trusted attester signed the object. The important benefit of Portablegabi is that the claimer stays anonymous during the verification. The verifier is not able to link two verification sessions to a single identity and learns nothing about the user besides the shared properties.

Terminology

Attester: An entity which signs JSON objects.

Claim: A JSON object.

Credential: A signed Claim which can be used to create a verifiable presentation.

Claimer: An entity which is in possession of a Credential.

Presentation: A Credential slice (JSON object) which contains selected properties plus a signature from an attester.

Verifier: An entity which requests signed properties from the claimer in form of a presentation.

Witness: A unique number tied to a credential which is required to prove whether it has been revoked or not.

Accumulator: A whitelist which contains all non-revocation witnesses and can be used to prove that a credential was not revoked.

Technical overview

The cryptographic primitives of Portablegabi are implemented in Gabi which is maintained by the privacy by design foundation and also used inside IRMA. Gabi makes use of CL-Signatures and is based on the Idemix Specification.

Portablegabi provides a protocol for attestation and verification of claims. The main goals of Portablegabi are selective disclosure and multi-show unlinkability. Selective disclosure enables the claimer to only present a subset of the information contained inside their attested JSON object. The unlinkability feature hinders the verifier to link two verification sessions of the same claimer together. The claimer can interact with the same verifier multiple times without the verifier being able to tell if they talked to the same claimer. Please note that this only holds true if the claimer does not reveal attributes which uniquely identify them. Otherwise, the verifier would be able to link multiple sessions together.

Revocation

The library also provides a scheme to support revocation of attestations using a distributed ledger. Each attestation contains a non-revocation witness which proves that the attestation is still valid and has not been revoked. A witness basically consists of two large integer numbers. It is valid against an attester's accumulator like a signature over a message can be valid against a public key.

Accumulator

Each attester has their own accumulator that is written on the blockchain. Initially, this is just a large random integer and a timestamp signed by the attester. Therefore, an accumulator includes every witness by default. To be more precise, checking whether a witness is included in an accumulator is done by checking a mathmatical equation. If the attester wishes to revoke an attestation, they remove the associated witness from the accumulator and update the blockchain with the new one. This is done by calculating the new accumulator in such a way that the inclusion equality check does not hold true for the witness of the revoked credential. Thus, if a claimer wants to prove their credential has not been revoked, they prove it is still included inside the newest accumulator.

For more information about the accumulator and witness, please check out the cryptography section of the IRMA docs or "Dynamic accumulators and application to efficient revocation of anonymous credentials" by Camenisch et al.

Architecture of Portablegabi

Portablegabi consists of a part written in Go which wraps the Gabi library and can be compiled to WASM. The second part is a Typescript layer which uses the WASM-Module and provides an API for attestation, verification and revocation. It provides usage with and without a Substrate-based blockchain implementing the portablegabi-pallet.

Portablegabi is documented using a generated API reference.

State of Security

There has been no code audit of portablegabi or of the gabi library. Portablegabi is therefore experimental and provides no security guarantees.

Getting Started

This tutorial will show you how to use the portablegabi library. It will take you approx. 30 minutes to work through. Before you dive in and try out the tutorial, you need to set up your environment.

To get started, you need to have the following dependencies installed:

  • node (any version starting with 10.19)
  • yarn

Set up a tutorial project

If you want to try out the examples inside this tutorial, create a new project and add Portablegabi as a dependency by using yarn add. If you have built and customized the Portablegabi repo on your own, just link it with yarn link.

mkdir portablegabi-rocks
cd portablegabi-rocks
yarn init -y
yarn add @kiltprotocol/portablegabi
# or if you have build and linked Portablegabi by yourself:
yarn link @kiltprotocol/portablegabi

Create an JavaScript file

touch index.js

and after adding some code from the examples, execute them with node

node index.js

Run the examples

Most of the Portablegabi functions are asynchronous (due to calling the WASM in a callback-fashion). Therefore, you need to wrap the examples inside an asynchronous function which you call in the end. Moreover, when sending data from JavaScript to WASM and vise versa, it needs to be serialized to a string. Since the zero knowledge magic happens in the WASM, we rarely deserialize the received data in JavaScript. In case you are curious, you can deeper inspect the data more by calling <data>.parse(). We wrap all data received from WASM into a custom class WasmData that only supports unmarshalling via JSON.parse(<data>) by calling .parse() and displaying the data as string via .toString()

Note: If you run the examples in Typescript and have version 3.8+ installed, you won't be required to do this due to the added top-level await.

async function exec() {
  // Portable gabi example functions...
}
exec();

The creation of attester keys can take 10 to 30 minutes in javascript. Therefore, we recommend using the following example keys which to speed up the process. Please note that you should never use these keys in production.

const portablegabi = require("@kiltprotocol/portablegabi");

const privKey = new portablegabi.AttesterPrivateKey(
  '{"XMLName":{"Space":"","Local":""},"Counter":0,"ExpiryDate":1610554062,"P":"iDYKxuFGt1Xv1aqMLaagjrOPX0hjkOlFrKOp4NPnSBHmQ9SFETUX1M43q3jLsGz+UEWFS3+SS9QpP4CTkl3p/w==","Q":"92MJOhwjESn7QohCCY1oBxsToAfccGoKtE3sBoaNxHWoowSiCy8fMG+B1sO5QU+bV3i1xwvVno9o30RcMoXEaw==","PPrime":"RBsFY3CjW6r36tVGFtNQR1nHr6QxyHSi1lHU8GnzpAjzIepCiJqL6mcb1bxl2DZ/KCLCpb/JJeoUn8BJyS70/w==","QPrime":"e7GEnQ4RiJT9oUQhBMa0A42J0APuODUFWib2A0NG4jrUUYJRBZePmDfA62HcoKfNq7xa44Xqz0e0b6IuGULiNQ==","ECDSA":"MHcCAQEEILO+g4uSDheZ6PSLxR7olFzUhZpeO9tQu84hX6UeIevaoAoGCCqGSM49AwEHoUQDQgAEKvmUz3HIZy890jE78CC9V9BuN8taO+L8GjAeS14v0CL7GCFZ1GMnaSZi4WG3mOjJlJ80CnMowIbUT3Fw1TluFw==","NonrevSk":null}'
);
const pubKey = new portablegabi.AttesterPublicKey(
  '{"XMLName":{"Space":"","Local":""},"Counter":0,"ExpiryDate":1610554062,"N":"g6DWNN/cWep9/lCc6gg0tA8wS1y5LgQx2/fM/wMpYJE8MTZ9SJ3y9kjIBAeSb4aY3vsFhRp8aWsEZzAA0Qu0kW4bzyKN1RU7A0tlmkmDetCxu7Gy2zQMHlTg4YkAVxVYAIIIWhHKHrVLzH7zCsuXos1qm/sthByVdEXv4HPjCZU=","Z":"BiDMFSNGKLIcHJY3tmh2vgiW7D3f5g5b+6Bjf0ns3/rPOg8x0BJ+CzqOLQL+loNIomOzBm/Pk36q3pmPPFMfug80AwUlZOvKTrzj29Agq4DF7p4jruElRyZsdGNjlFkVzILFT/9yrXfjD/9DAHXGm6/4unVnwKP4I0j1r9sLYtg=","S":"Bxm9bNpNLZUM6gy74aR0HW2DadFuy/l+MOdZkG2BiFxbTEP24GXBYA3+d1xajplWEm2iLF4w2OeviIpr8VIzDNy6dXRyGcTnGzj6sVeGlR5u3N+8M2XNH1pNEymLQQbUAt3ogYSWiJW88bxHCf3AZiS91XT1Zh3ENCS9NsyGzt8=","G":"Angd7BuIjTeWGsVLGVCtv+5dx1TMEUr/Z5Fhk7OFUNBexY8fuNfzxfeclgSQpC+nyIAFHc3RB+3Fcs2vOSygopVfLEJo9h7dSjtlcxSZ1wE8YNgouHwfVuq4KWixzIk7Le+IeUzNaQNOL9SI3h5mlxJ5QOO2Src+BPQuFjXPSfI=","H":"U1MyQqwl1LrZY5G61Z2ZDM3zWQKv78HOluCrtxCDBsMvYNRLvhbppOhOdsnG3axN5NIH01/R6mlYojBDg9L7xSwR+1QpmHGUbwkemADlUZQ9c98Up1ORKxNW0asQJdPHV4NGqjQbDfJzejdGJwd95scmSpqLNvRTT+L0iW0ln4A=","T":"BEIUJ5pXzFZPeoB3us341EWxwE7HByM4NaPYRS6YVtDcJdz+H9EEKdUcXhUVrJAQ2OZy2FP0+SNvQVk8AxWDiD73tHUUKDnkMoKSkHPnEnsCInGHr4iTYE2zp8/uEBFxNppq5SP9gQOzE2qekGket2co0W/+jKNtg63u1udlZjo=","R":["OpuoX8xEvGaULH7ir3G/W9zBB1gmYN6lllJsk8+QGGQxydbrtoQiFfhU1Tyqm59sq3GIhksiYB6Th6jYq3BIFKVynX993FPYU2HS2dceFk5kvymIx33u2nTyMzFvox2b6IkKHKXfbtx/VWWlVYcywFOAOiQ1Xa7dXDx1ebuGowE=","Jamoy887kQjyTKjHwgFGxOKugcGIxdUhK9pE/nDTFttU6ndo5qm04AVB5n4WUaFurrKlNSIICheAXI10kIy37Ogr1N4Ge/7TbyZ/hXB8DBzoJbD3MVpXblq9hrhEkb+yyJ9uipnKckflQBWGzl+grXV17SWVhd5TKpUrMw1cDYs=","YGogpko2T4xWQjipZN691tpWJYffyX5evzh2EJAZSpP3evnMbro0Et5Bk+2NY9yt/GoJW8qkVkwEdaYU0jQiGS27F3aJ5e00VOCnZ6bIXJKgcTTxqc5c9NrpJVWNX9n5G590OVTNqlLUOFw3/mIY26A2MKxsa56j2K0V4IM0FI4=","Jca8++mT6d93MK0S8Fb6rtu7TpV9TGqM0mSvO0JKuyRvEro3anRbvZ8sHRLt2q2ePIyCQHz2eUc4iJ1vQLnzMxVavQ3xS5AAS27Tw+xM64JhWV6BFDqZgaEcu22jEi+Rrjjqss2nmC6CQYJZt5g5P0dXGV2JKDcrUaGCtzc4cNE=","ZIV6MWKglRL5B9vv5RmBigbieiuebmy/mcpycXlyQcoZEeNCzuGs/JgRnGr05umbcsQ5ZNSS3TKiL5CM/Z4fanuSu6jNnVoHvSkxI3x28ZpMV8C43CXkS6smmiZP+2SSL419Q247ZbP04T5wHcZ6GooCLxnfx5DeEtRze3UU1Wk=","IbwQtY9iF7C/rNKkTilHP5jEj9r3aI1tRVU9WeMzE9yxrE0mggzpcoCM0lJFLcqVyWhKD3PWssuXwNiLJipUL+sH/u8Qk8Bu6sv/USlUU7sgSJ4akl2Lp+5oYSkzHiZTeJtLg0OVGZnka3pGxzg0ihkkT6Bdk8K2OicTNxlHzgI=","ZQ9/qIgvOx/8dyXlAFeZH+2lriSPaj/NDzPCxR9sXqBYJskSkSrdGogxP2RZeAGyDh7NvwUtvBDQ/vLKz/O3ANPUOnaRx1n4uBF+uBdt0h3Ml/DckhL5k2+nHQsnZWPFxkdpatCIFWcvYuldx+gXLePBaRmNnKMoxAgT+tJnJcw=","ZGfBOqHujseUhLZdfs8kq+/kmG3yMwUAmQrGgTdNej8npNsOyD/Am/SoPdSjpr1enuMgBzva/bjn3/z8nncpia65+v9Pn5831UuFp8h53/1WaEHvN/yctnIKb8k1IRtPlSvnfq7qwC/sIGvHq+ZTj3/ie57rTSkSMrmdFL8PMM0=","TM38T4ekWiNWICCgry7GsppfVt2ImPv4SL//f/J3beP34K1afJCsHk50XJwi8qyMz8HqEVK2sWvMQzJ8Amct4sAfRYIZNmqH7mSR7LwIXvihwv1dUlJv2R7MLTjEGkEnJHE5cCR0K5GxjeQSSgNHAu33MOth3ipsK9ZmF+slSkI=","YwMb/IVn2NsA4y8ZiiBxCWoOg0tsqyYKTakxDZnRhw+wHwhnA3+T87X4tOSAx+dYlmtj3UQzUAeFRYztr2YTrF2boS/YFeAiVh6swPgFOScvmOuf5O4fJn7z+iXr+ivgFccswxBhxqa9MdF8ReqHaVouj8LLyk33fZgWduwfnA=="],"EpochLength":432000,"Params":{"LePrime":120,"Lh":256,"Lm":256,"Ln":1024,"Lstatzk":80,"Le":597,"LeCommit":456,"LmCommit":592,"LRA":1104,"LsCommit":593,"Lv":1700,"LvCommit":2036,"LvPrime":1104,"LvPrimeCommit":1440},"Issuer":"","ECDSA":"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKvmUz3HIZy890jE78CC9V9BuN8taO+L8GjAeS14v0CL7GCFZ1GMnaSZi4WG3mOjJlJ80CnMowIbUT3Fw1TluFw==","NonrevPk":null}'
);

Attestation

Before the attester can sign the claimer's credential, they have to have initiate the attestation session by generating a nonce which will prevent replay attacks. After the claim has been sent to the attester, they sign it and send the signature over to the claimer. Then, the claimer can build their credential using the claim and the attester's signature.

sequenceDiagram
    participant A as Attester
    participant C as Claimer

    note left of A: The attester chooses two random numbers (context and nonce) and sends them to the claimer.
    A->>C: Initiate Attestation
    note right of C: The claimer commits on their master secret and send their claim to the attester.
    C->>A: Attestation Request
    note left of A: The attester validates and signs the claim and sends the signature back to the claimer.
    A->>C: Attestation Response
    note right of C: The claimer builds their credential using the signature.

Before an attester can create attestations, they have to generate a key pair and publish their public key.

const attester = new portablegabi.Attester(pubKey, privKey);

// Create a new accumulator (which is used for revocation).
const accumulator = await attester.createAccumulator();
console.log("Accumulator:\n\t", accumulator.toString());

// Build a new claimer and generate a new master key.
// const claimer = await portablegabi.Claimer.create()
// or use a mnemonic:
const claimer = await portablegabi.Claimer.buildFromMnemonic(
  "siege decrease quantum control snap ride position strategy fire point airport include"
);

// The attester initiates the attestation session.
const {
  message: startAttestationMsg,
  session: attestationSession,
} = await attester.startAttestation();

// The claimer answers with an attestation request.
const claim = {
  age: 15,
  name: "George",
};

const {
  message: attestationRequest,
  session: claimerSession,
} = await claimer.requestAttestation({
  // The received attestation message.
  startAttestationMsg,
  // The claim which should get attested.
  claim,
  // The public key of the attester.
  attesterPubKey: attester.publicKey,
});

// The attester should check the claim they are about to attest.
const receivedClaim = attestationRequest.getClaim();
console.log("Claim built from attestation\n\t", receivedClaim);

// Do checks on receivedClaim.
// If everything checks out the attester issues an attestation.
const {
  // The attestation should be sent over to the claimer.
  attestation,
  // The witness should be stored for later revocation.
  witness,
} = await attester.issueAttestation({
  attestationSession,
  attestationRequest,
  // The update is used to generate a non-revocation witness.
  accumulator,
});
console.log("Witness:\n\t", witness.toString());

// After the claimer has received their attestation, they can build their credential.
const credential = await claimer.buildCredential({
  claimerSession,
  attestation,
});
console.log("Credential:\n\t", credential.toString());

Upon completion of an attestation session, the attester receives a witness which can be used to revoke the attestation and the claimer receives a credential with which they can generate presentations for an arbitrary amount of verifiers. Note that an attester should store each witness and keep track of the matching claim.

Verification

During the verification, the verifier requests a set of attributes from the claimer. The claimer discloses their attributes and provides a proof (referred to as presentation). The verifier can then validate that the attributes are signed by a trusted attester using the claimer's presentation.

sequenceDiagram
    participant V as Verifier
    participant C as Claimer

    V->>C: (Combined-)Presentation Request
    note right of C: The Claimer selects a credential and confirms the requested attributes.
    C->>V: (Combined-)Presentation Response
    Note left of V: The verifier validates the proof and reads the attributes.

The claimer is required to have both a credential and the public key of the attester who signed it. On the other hand, the verifier needs to trust the attester who signed the credential and their latest accumulator.

Accumulator timestamps

However, new versions of accumulators could be added to the chain quite frequently. The worst case would be, that the claimer needs to update their credential before every verification. To prevent this, a verifier can signal the claimer that they accept not only the newest accumulator, but also accumulators which are more recent than a specified point in time. This timestamp is set using the reqUpdatedAfter parameter.

If the verifier sends the current time, the claimer most likely has to check for the newest accumulator. During the verification, the verifier needs to decide on a latest accumulator. This is done using the latestAccumulator parameter. If the claimer sends a newer accumulator than latestAccumulator, the verifier accepts this accumulator automatically as it's timestamp is greater than the required one.

In short: the verifier accepts an accumulator if it is newer than the newest accumulator they know of or if it was created after reqUpdatedAfter.

For more details about the accumulator, have a look at the next section.

Example

const portablegabi = require("@kiltprotocol/portablegabi");

const claimer = await portablegabi.Claimer.buildFromMnemonic(
  "siege decrease quantum control snap ride position strategy fire point airport include"
);

const credential = new portablegabi.Credential("<The credential created during the attestation>");
const accumulator = new portablegabi.Accumulator("<The accumulator created during the attestation>");
const pubKey = new portablegabi.AttesterPublicKey("<The pre-generated public key of the attester>");

// The verifier requests a presentation.
const {
  // Local information used to verify the presentation later.
  session: verifierSession,
  // The request which should be sent to the claimer containing the requested attributes.
  message: presentationReq,
} = await portablegabi.Verifier.requestPresentation({
  // Specify which attributes should be disclosed.
  requestedAttributes: ["age"],
  // The threshold for the age of the accumulator.
  // If the accumulator was created before this date, the proof will be rejected
  // except if the accumulator is the newest available accumulator.
  reqUpdatedAfter: new Date(),
});

// After the claimer has received the presentationRequest, they build a presentation:
const presentation = await claimer.buildPresentation({
  credential,
  presentationReq,
  attesterPubKey: pubKey,
});
console.log("Presentation:\n\t", presentation.toString());

// The presentation is sent over to the verifier who validates the proof and extracts the claim.
const {
  // The contained claim, this value is undefined if the proof could not be validated.
  claim: publicClaim,
  // A boolean which indicates whether the presentation was valid.
  verified,
} = await portablegabi.Verifier.verifyPresentation({
  // The presentation which was sent over by the claimer.
  proof: presentation,
  verifierSession,
  // The public key which was used by the attester to sign the credential.
  attesterPubKey: pubKey,
  // This accumulator is used to check whether the claimer provided the newest available accumulator.
  latestAccumulator: accumulator,
});
console.log("Public claim:\n\t", publicClaim);
console.log("Verified?", verified);

Revocation

An attester can revoke any credential they attested. This is achieved by using a witness which is contained inside a credential and a whitelist containing all non-revoked witnesses. If an attester revokes a credential, they remove the associated witness from the whitelist and publish a new version of this whitelist. Note that witnesses are added to the whitelist implicitly. Therefore, adding witnesses to the whitelist requires no change. Since this whitelist is implemented using accumulators, it is called accumulator. Further documentation on how this accumulator works can be found in the IRMA docs.

Example

In order to revoke a credential, the attester needs their key pair, the witness of credential they want to revoke (created in issueAttestation) and the accumulator.

const portablegabi = require("@kiltprotocol/portablegabi");

const pubKey = new portablegabi.AttesterPrivateKey("<The pre-generated public key of the attester>");
const privKey = new portablegabi.AttesterPrivateKey("<The pre-generated private key of the attester>");
const attester = new portablegabi.Attester(pubKey, privKey);
const accPreRevo = new portablegabi.Accumulator("<The accumulator created during the attestation>");
const witnessToBeRevoked = new portablegabi.Witness("<The witness created during the attestation>");

// Issue attestations and store witnesses.
const accPostRevo = await attester.revokeAttestation({
  accumulator: accPreRevo,
  // The list of witnesses associated with the credentials which should get revoked.
  witnesses: [witnessToBeRevoked],
});
console.log("Accumulator after revocation:\n\t", accPostRevo.toString());
// Publish the accumulator after revocation.

After an attester publishes a new accumulator, all claimers should update their credential attested by this specific attester to their newest available accumulator. In order to update the credential, the claimer needs the complete history of all new accumulators since their last update.

const claimer = await portablegabi.Claimer.buildFromMnemonic('siege decrease quantum control snap ride position strategy fire point airport include')
let credential = () => {
    // Request an attestation from an attester.
    // Build a credential.
    // ...
    return credential
}()

// How to update your credential?
// The Credential is updated to accumulator 55, the newest accumulator has index 59.
// Note that a user does not have to input the accumulators in a sorted way - Portablegabi takes care of this.
const newCredential = await credential.update({
    attesterPubKey: attestersPublicKey,
    accumulators: [accumulator56, accumulator57, accumulator58, accumulator59],
})

Blockchain

Although you are free to choose how you would like to provide access to accumulators, we suggest that you use a blockchain for that. The advantage of using a blockchain is that you have a decentralized database. If each attester operated their own servers for providing accumulators, these servers would become a single point of failure. If a server is not reachable, the verifier cannot check if a credential was recently revoked. Another concern of a centralised approach is the privacy of the claimer. Since the attester operates the server where the accumulator is stored, they can also track how often a claimer updates their credential. A blockchain can prevent attesters from tracking claimers and provides a redundant storage.

Build the chain

If you want to use a blockchain, you can integrate our portablegabi pallet into your substrate blockchain. We also provide a blockchain template which includes our pallet and can be used to store accumulators. In order to use that, just clone and set up the portablegabi-node or use the template to create your own project.

git clone https://github.com/KILTprotocol/portablegabi-node.git
cd portablegabi-node
./scripts/init.sh
./scripts/build.sh
cargo build

You might want to grab a cup of tee! 🍵 Building the chain might take up to 30 min.

Start the chain

cargo run -- --dev

Run examples

The purpose of the chain is to both store each attester's accumulator and give access to old revisions, as these are required when updating older credentials. Therefore, we have added some chain functionality to both the credential and attester classes.

AttesterChain

We extended the attester identity in a sub-class named AttesterChain. This class extends Attester by adding on-chain functionality to the identity's creation, handling of the accumulator and revocation. In terms of the creation, you have the choice between an ed25519 and a sr25519 account key for the chain. Moreover, you can build it from a URI like //Alice. When revoking a credential, the accumulator on the chain gets updated automatically.

Example 1: Complete process for single credential with revocation

In the following, we will run a complete exemplary chain process:

  1. Connect to the chain and add an accumulator.
  2. Attest a claim.
  3. Revoke the attested claim from 2. and (automatically) update the accumulator.
  4. Check out multiple verifications with different timestamps.
const portablegabi = require("@kiltprotocol/portablegabi");

const pubKey = new portablegabi.AttesterPublicKey("<The pre-generated public key of the attester>");
const privKey = new portablegabi.AttesterPrivateKey("<The pre-generated private key of the attester>");

async function exec() {
  /** (1) Chain phase */
  // (1.1) Connect to the chain.
  const chain = await portablegabi.connect({
    pgabiModName: "portablegabiPallet",
  });
  console.log("Successfully connected to the chain");

  // (1.2) Create Alice identity.
  const attester = await portablegabi.AttesterChain.buildFromURI(pubKey, privKey, "//Alice", "sr25519");

  // (1.3) Create a fresh accumulator.
  const accPreRevo = await attester.createAccumulator();

  // (1.4) Put the accumulator on chain.
  console.log("Putting accumulator on the chain for Alice");
  // To update the accumulator on chain, we first create a transaction.
  const accumulatorTx = await attester.buildUpdateAccumulatorTX(accPreRevo);
  // And send the transaction to the blockchain.
  await chain.signAndSend(accumulatorTx, attester.keyringPair);

  // Check whether it has actually been added to chain.
  // We need to wait for next block since updating the accumulator is a transaction.
  console.log("\t Waiting for next block to have the accumulator on the chain");
  console.log(
    "Latest accumulator === accPreRevo? Expected true, received",
    (await chain.getLatestAccumulator(attester.address)).toString() === accPreRevo.toString()
  );

  /** (2) Attestation phase */
  // (2.1) The attester initiates the attestation session.
  const { message: startAttestationMsg, session: attestationSession } = await attester.startAttestation();

  // (2.2) The claimer answers with an attestation request.
  const claimer = await portablegabi.Claimer.buildFromMnemonic(
    "siege decrease quantum control snap ride position strategy fire point airport include"
  );
  const claim = {
    name: "George Ericson",
    age: 24,
    driversLicense: {
      id: "127128204193",
      category: "B2",
      licensingAuthority: "Berlin A52452",
    },
  };
  const { message: attestationRequest, session: claimerSession } = await claimer.requestAttestation({
    // the received attestation message
    startAttestationMsg,
    // the claim which should get attested
    claim,
    // the public key of the attester
    attesterPubKey: attester.publicKey,
  });

  // (2.3) The attester issues an attestation.
  const {
    // The attestation should be sent over to the claimer.
    attestation,
    // The witness should be stored for later revocation.
    witness,
  } = await attester.issueAttestation({
    attestationSession,
    attestationRequest,
    // The update is used to generate a non-revocation witness.
    accumulator: accPreRevo,
  });
  const credential = await claimer.buildCredential({
    claimerSession,
    attestation,
  });

  /** (3) Revocation phase */

  // Revoke the attestation and receive a new accumulator whitelist.
  const accPostRevo = await attester.revokeAttestation({
    witnesses: [witness],
    accumulator: accPreRevo,
  });
  // To update the accumulator on chain, we first create a transaction.
  const tx = await attester.buildUpdateAccumulatorTX(accPostRevo);
  // And send the transaction to the blockchain.
  await chain.signAndSend(tx, attester.keyringPair);

  // Check whether accPostRevo is the latest accumulator on chain.
  console.log("\t Waiting for next block to have the updated accumulator on the chain");
  console.log(
    "Latest accumulator === accPostRevo? Expected true, received",
    (await chain.getLatestAccumulator(attester.address)).toString() === accPostRevo.toString()
  );

  /** (4) Verification phase */
  // Get the exact timestamp of the revocation for simplicity, also works for dates after accumulator date.
  const timeAtRev = await accPostRevo.getDate(attester.publicKey);

  // (4.1) The verifier sends a nonce and context to the claimer and requests disclosed attributes.
  // Note: The requested timestamp equals the accumulator date.
  const { session: verifierSession, message: presentationReq } = await portablegabi.Verifier.requestPresentation({
    requestedAttributes: ["age", "driversLicense.category"],
    reqUpdatedAfter: timeAtRev,
  });

  // (4.2) The claimer builds a presentation with the revoked credential.
  // Note: They need to update as the credential was build before timeAtRev.
  const presentation = await claimer.buildPresentation({
    credential,
    presentationReq,
    attesterPubKey: attester.publicKey,
  });

  // (4.3) The verifier checks the presentation for non-revocation, valid data and matching attester's public key.

  // We expect success because the credential is still valid in accPreRevo.
  const { verified: verifiedPreRevo } = await portablegabi.Verifier.verifyPresentation({
    proof: presentation,
    verifierSession,
    attesterPubKey: attester.publicKey,
    latestAccumulator: accPreRevo,
  });
  console.log(
    "Cred verified w/ timestamp at revocation and old accumulator?\n\tExpected true, received",
    verifiedPreRevo
  );

  // We expect failure because the credential is invalid in accPostRevo.
  const { verified: verifiedPostRevo } = await portablegabi.Verifier.verifyPresentation({
    proof: presentation,
    verifierSession,
    attesterPubKey: attester.publicKey,
    latestAccumulator: accPostRevo,
  });
  console.log(
    "Cred verified w/ timestamp at revocation and new accumulator?\n\tExpected false, received",
    verifiedPostRevo
  );

  // Expect failure when updating a credential whose witness was revoked in any of the used accumulators.
  await credential
    .updateSingle({
      attesterPubKey: attester.publicKey,
      accumulator: accPostRevo,
    })
    .catch((e) => {
      if (e.message.includes("updateCredential")) {
        console.log("Caught expected throw when trying to update the revoked credential");
      } else throw e;
    });
}
exec().finally(() => portablegabi.disconnect());

Development

If you want to help develop portablegabi, we would be glad to merge your pull request. But first, you need to set up a development environment for our project. For that, start with installing the following dependencies:

Make sure you set up your GOPATH directory and environment variables.

After you are done, clone the portablegabi project into the correct go path:

mkdir -p $GOPATH/src/github.com/KILTprotocol/
git clone https://github.com/KILTprotocol/portablegabi.git \
  $GOPATH/src/github.com/KILTprotocol/portablegabi
cd $GOPATH/src/github.com/KILTprotocol/portablegabi

Next, you need to install all the node dependencies, build the Portablegabi WASM and transpile the Typescript code:

yarn install
yarn build

To ensure everything went fine, you can execute any of the provided examples:

yarn ts-node docs/examples/exampleSingle.ts

You can use the Portablegabi version you have just built by running yarn link inside the Portablegabi project and yarn link @kiltprotocol/portablegabi in the project where you want to use Portablegabi.

Optional: Test with Substrate chain

If you want to test the examples with a blockchain, you will also need to install rust and Substrate. For more information about setting up a chain to be used with the Portablegabi API, see the exemplary portablegabi-node. For code examples of the Portablegabi chain API, please have a look at our chain examples.