/issuer-toolkit

Library for credential issuers integrated with GlobaliD

Primary LanguageTypeScriptApache License 2.0Apache-2.0

Issuer Toolkit

This is a library for credential issuers integrated with GlobaliD.

Installation

npm install @globalid/issuer-toolkit

Usage

The GidIssuerClient class is the primary component of the toolkit, providing several methods for issuing a credential.

The function to create a GidIssuerClient requires the client ID and secret of a developer app created in GlobaliD's developer portal.

const clientId = '...';
const clientSecret = '...';
const client = createGidIssuerClient(clientId, clientSecret);

The GidIssuerClient supports the typical flow for issuing a credential:

  1. Receive and validate a credential request.
  2. Encrypt and upload file claims (optional).
  3. Build and send a credential offer.

If anything goes wrong in that process, issuers can report an error, which notifies the prospective holder of a problem in the credential issuance.

Validating a Credential Request

The validateRequest method will check the validity of a CredentialRequest, which consists of the following properties:

  • data (optional) - Information about the credential being requested
  • gidUuid - UUID of the holder's GlobaliD identity
  • signature - Result of digitally signing the concatenation of the timestamp, threadId, and (if present) data, using the holder's private key
  • threadId - ID correlating interactions related to this credential request
  • timestamp - Time of the request as the number of milliseconds since the Unix epoch

Of those, the signature and timestamp are validated. The signature is verified using the public key corresponding to the holder's identity (identified by gidUuid). The timestamp must be no more than 5 minutes in the past or 1 minute in the future. If the credential request is invalid, an error is thrown.

This method also handles boilerplate error reporting. An InvalidSignatureError, StaleRequestError, or EagerRequestError is reported as a 600-16. All other errors are reported as a 600-7.

const threadId = '...';
const gidUuid = '...';
const credentialRequest = {
  threadId,
  gidUuid,
  timestamp: 1640995200000,
  signature: 'abcdefghijklmnopqrstuvwxyz',
  data: {
    givenName: 'Neville',
    birthDate: '1980-07-30'
  }
};

try {
  await client.validateRequest(credentialRequest);
} catch (error) {
  if (error instanceof IdentityNotFoundError) {
    // invalid identity (i.e., `gidUuid` does not exist)
  } else if (error instanceof PublicKeyNotFoundError) {
    // user has no public key
  } else if (error instanceof InvalidSignatureError) {
    // `signature` is invalid
  } else if (error instanceof StaleRequestError || error instanceof EagerRequestError) {
    // `timestamp` is outside acceptable range
  }
}

Uploading a File

The uploadFile method allows for encrypting and uploading a file to GlobaliD's S3 instance. The file is encrypted using AES and a randomly-generated 256-bit key, which is itself encrypted using the holder's public key.

const fileClaim = await client.uploadFile(gidUuid, {
  name: '8bfd3afe-8f0b-4583-836e-97cde534e304.foo.jpg',
  type: 'image/jpeg',
  content: Buffer.from(/* ... */)
});

The result of uploadFile is a FileClaimValue intended for use in a CredentialOffer (see Sending a Credential Offer). A FileClaimValue has the following properties:

  • decryptionKey - Symmetric key used to decrypt (via AES) the payload received by dereferencing the url. The key is encrypted using RSA and the holder's public key.
  • sha512sum - Checksum of the file's content
  • type - Media type of the file's content
  • url - Location of the encrypted file

Sending a Credential Offer

The sendOffer method allows sending an offer for a credential following a credential request. The method accepts a CredentialOffer, which has the following properties:

  • claims - Claims about the credential subject
  • contextUri - URI of a JSON-LD context describing the credential subject
  • description (optional) - Descriptive text about the credential being offered
  • name - Name of the credential being offered
  • schemaUri - URI of a JSON Schema describing the data schema of the credential subject's claims
  • subjectType - JSON-LD @type of the credential subject
  • threadId - ID correlating interactions related to this credential request
const claims = ;

const credentialOffer = {
  threadId,
  name: 'Government ID',
  description: 'Lorem ipsum dolor sit amet',
  contextUri: 'https://example.com/contexts/Person',
  schemaUri: 'https://example.com/schemas/Person',
  subjectType: 'Person',
  claims: {
    givenName: 'Neville',
    birthDate: '1980-07-30',
    avatar: fileClaim
  }
};

await client.sendOffer(credentialOffer);

Reporting an Error

If something goes wrong while fulfilling a credential request, you can report the error using the reportError method.

await client.reportError(threadId, '600-1');

Error Codes

Code Description
300-8 Document unsupported
600-1 General credential request failure
600-3 Verification process was cancelled
600-7 GlobaliD erred or is unavailable
600-8 Issuer is unavailable
600-16 Request validation failed

Downloading a File

The toolkit offers the downloadFile utility function for downloading and optionally decrypting a file from a URL, presumably sent in the initial credential request. This function is essentially the inverse of GidIssuerClient's uploadFile.

In addition to a URL string, downloadFile accepts the following options:

  • decryptionKey - Symmetric key used to decrypt the downloaded file via AES. The file is assumed to be in plaintext if this option is absent.
  • privateKey - Asymmetric private key (typically the issuer's) used to decrypt the decryptionKey via RSA. The decryptionKey is assumed to be plaintext if this option is absent.
  • sha512sum - Checksum used to validate the integrity of the downloaded (and possibly decrypted) file
import { downloadFile } from '@globalid/issuer-toolkit';

const buffer1 = await downloadFile('http://example.com/unencrypted-file');
const buffer2 = await downloadFile('https://example.com/encrypted-file', {
  decryptionKey: request.data.avatar.key,
  privateKey: process.env.PRIVATE_KEY,
  sha512sum: request.data.avatar.checksum
});

Testing Utilities

Nock

The @globalid/issuer-toolkit/testing module provides functions for mocking the HTTP requests (using nock) made by GidIssuerClient. There are mock* functions for each GidIssuerClient method, as well as a clearMocks function for cleanup.

import * as GidIssuerClient from '@globalid/issuer-toolkit/testing';

afterEach(() => {
  GidIssuerClient.clearMocks();
});

test('request validation', async () => {
  GidIssuerClient.mockValidateRequest(gidUuid, publicKey);

  // call your code that uses GidIssuerClient#validateRequest...

  // assertions...
});

test('sending an offer', async () => {
  GidIssuerClient.mockSendOffer();
  // ...
});

Sinon

The @globalid/issuer-toolkit/testing/sinon allows Sinon users to create a GidIssuerClient stub.

import stubGidIssuerClient from '@globalid/issuer-toolkit/testing/sinon';
import sinon from 'sinon';

const GidIssuerClientStub = stubGidIssuerClient();

afterEach(() => {
  sinon.restore();
});

test('request validation', async () => {
  GidIssuerClientStub.validateRequest.withArgs(/* ... */).resolves();

  // call your code that uses GidIssuerClient#validateRequest...

  // assertions...
});

test('sending an offer', async () => {
  GidIssuerClientStub.sendOffer.withArgs(/* ... */).resolves();
  // ...
});

TypeScript

The issuer toolkit is written in TypeScript, so type declarations are bundled with the package.

Development

The following NPM scripts are available for development:

  • build – Runs the clean, genver, compile, lint, and format:check scripts to build the project
  • clean – Removes the output directory for a clean build
  • compile – Compiles TypeScript files with tsc
  • format – Formats the files with Prettier
  • format:check – Checks the formatting of the files with Prettier
  • genver - Generates a version module with genversion
  • lint – Lints the code with ESLint
  • lint:fix – Attempts to fix problems found by the linter
  • test – Tests the code with Jest
  • test:watch – Tests the code in watch mode