1.0.0 • Published 11 months ago

@digitalcredentials/credential-status-manager-git v1.0.0

Weekly downloads
-
License
MIT
Repository
github
Last release
11 months ago

credential-status-manager-git

A Typescript library for managing the status of Verifiable Credentials in Git using Status List 2021

Build status NPM Version

Table of Contents

Background

Credentials are dynamic artifacts with a robust lifecycle that goes well beyond issuance. This lifecycle is liable to span revocation, suspension, and expiry, among other common states. Many proposals have been put forth to capture this model in Verifiable Credentials. One of the most mature specifications for this is Status List 2021. This library provides an implementation of this specification that leverages Git source control services like GitHub and GitLab for storage and authentication.

Install

  • Node.js 16+ is recommended.

NPM

To install via NPM:

npm install @digitalcredentials/credential-status-manager-git

Development

To install locally (for development):

git clone https://github.com/digitalcredentials/credential-status-manager-git.git
cd credential-status-manager-git
npm install

Usage

Create credential status manager

The createStatusManager function is the only exported pure function of this library. It is an asynchronous function that accepts configuration options and returns a credential status manager that aligns with these options. Here are all the possible configuration options:

KeyDescriptionTypeRequired
servicename of the source control service that will host the credential status resourcesgithub | gitlabyes
ownerAccountNamename of the owner account (personal or organization) in the source control service that will host the credential status resourcesstringyes
repoNamename of the credential status repositorystringyes
repoIdID of the credential status repositorystringyes (if service = gitlab)
metaRepoNamename of the credential status metadata repositorystringyes
metaRepoIdID of the credential status metadata repositorystringyes (if service = gitlab)
repoAccessTokenaccess token for the credential status repository in the source control service APIstringyes
metaRepoAccessTokenaccess token for the credential status metadata repository in the source control service APIstringyes
didMethodname of the DID method used for signingkey | webyes
didSeedseed used to deterministically generate DIDstringyes
didWebUrlURL for did:webstringyes (if didMethod = web)
signUserCredentialwhether or not to sign user credentialsbooleanno (default: false)
signStatusCredentialwhether or not to sign status credentialsbooleanno (default: false)

Here is a sample call to createStatusManager:

import { createStatusManager } from '@digitalcredentials/credential-status-manager-git';

const statusManager = await createStatusManager({
  service: 'github',
  ownerAccountName: 'university-xyz', // Please create an owner account (personal or organization) in your source control service of choice
  repoName: 'credential-status', // Please create a unique credential status repository in the owner account
  metaRepoName: 'credential-status-metadata', // Please create a unique credential status metadata repository in the owner account
  repoAccessToken: 'abc123', // Please create your own access token in your source control service of choice (see Dependencies section for detailed instructions)
  metaRepoAccessToken: 'def456', // Please create your own access token in your source control service of choice (see Dependencies section for detailed instructions)
  didMethod: 'key',
  didSeed: 'DsnrHBHFQP0ab59dQELh3uEwy7i5ArcOTwxkwRO2hM87CBRGWBEChPO7AjmwkAZ2', // Please create your own DID seed (see Dependencies section for detailed instructions)
  signStatusCredential: true
});

Note: A Status List 2021 credential can be found in the designated status repository (repoName) of the designated owner account (ownerAccountName) which is populated by createStatusManager. Additionally, relevant historical data can be found in the designated status metadata repository (metaRepoName) in the same owner account. Note that these repositories need to be manually created prior to calling createStatusManager. Finally, you can find a publicly visible version of the aforementioned Status List 2021 credential at the relevant URL for hosted sites in the source control service of choice (e.g., https://ownerAccountName.github.io/repoName/statusListId for GitHub, where statusListId is the name of a file that is automatically generated in repoName).

Allocate status for credential

The allocateStatus is an instance method that is called on a credential status manager initialized by createStatusManager. It is an asynchronous method that accepts a credential as input, records its status in the caller's source control service of choice, and returns the credential with status metadata attached.

Here is a sample call to allocateStatus:

const credential = {
  '@context': [
    'https://www.w3.org/2018/credentials/v1',
    'https://w3id.org/security/suites/ed25519-2020/v1'
  ],
  id: 'https://university-xyz.edu/credentials/3732',
  type: [
    'VerifiableCredential'
  ],
  issuer: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC',
  issuanceDate: '2020-03-10T04:24:12.164Z',
  credentialSubject: {
    id: 'did:example:abcdef'
  }
};
const credentialWithStatus = await statusManager.allocateStatus(credential);
console.log(credentialWithStatus);
/*
{
  '@context': [
    'https://www.w3.org/2018/credentials/v1',
    'https://w3id.org/security/suites/ed25519-2020/v1',
    'https://w3id.org/vc/status-list/2021/v1'
  ],
  id: 'https://university-xyz.edu/credentials/3732',
  type: [ 'VerifiableCredential' ],
  issuer: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC',
  issuanceDate: '2020-03-10T04:24:12.164Z',
  credentialSubject: { id: 'did:example:abcdef' },
  credentialStatus: {
    id: 'https://university-xyz.github.io/credential-status/V27UAUYPNR#1',
    type: 'StatusList2021Entry',
    statusPurpose: 'revocation',
    statusListIndex: 1,
    statusListCredential: 'https://university-xyz.github.io/credential-status/V27UAUYPNR'
  }
}
*/

Note: If the caller invokes allocateStatus multiple times with the same credential ID against the same instance of a credential status manager, the library will not allocate a new status list entry. It will just return a credential with the same status info as it did in the previous invocation.

Update status of credential

The updateStatus is an instance method that is called on a credential status manager initialized by createStatusManager. It is an asynchronous method that accepts a credential ID and desired credential status as input (options: active | revoked), records its new status in the caller's source control service of choice, and returns the status credential.

Here is a sample call to updateStatus:

const statusCredential = await statusManager.updateStatus({
  credentialId: credentialWithStatus.id,
  credentialStatus: 'revoked'
});
console.log(statusCredential);
/*
{
  '@context': [
    'https://www.w3.org/2018/credentials/v1',
    'https://w3id.org/vc/status-list/2021/v1'
  ],
  id: 'https://university-xyz.github.io/credential-status/V27UAUYPNR',
  type: [ 'VerifiableCredential', 'StatusList2021Credential' ],
  credentialSubject: {
    id: 'https://university-xyz.github.io/credential-status/V27UAUYPNR#list',
    type: 'StatusList2021',
    encodedList: 'H4sIAAAAAAAAA-3BMQ0AAAACIGf_0LbwAhoAAAAAAAAAAAAAAIC_AfqBUGnUMAAA',
    statusPurpose: 'revocation'
  },
  issuer: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC',
  issuanceDate: '2023-03-15T19:21:54.093Z'
}
*/

Check status of credential

The checkStatus is an instance method that is called on a credential status manager initialized by createStatusManager. It is an asynchronous method that accepts a credential ID as input and returns status information for the credential.

Here is a sample call to checkStatus:

const credentialStatus = await statusManager.checkStatus(credentialWithStatus.id);
console.log(credentialStatus);
/*
{
  timestamp: '2023-03-15T19:39:06.023Z',
  credentialId: 'https://university-xyz.edu/credentials/3732',
  credentialIssuer: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC',
  credentialSubject: 'did:example:abcdef',
  credentialState: 'revoked',
  verificationMethod: 'did:key:z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC#z6MkhVTX9BF3NGYX6cc7jWpbNnR7cAjH8LUffabZP8Qu4ysC',
  statusListId: 'V27UAUYPNR',
  statusListIndex: 1
}
*/

Check if caller has authority to update status of credentials

The hasStatusAuthority is an instance method that is called on a credential status manager initialized by createStatusManager. It is an asynchronous method that accepts an access token for the API of the caller's source control service of choice, and reports whether the caller has the authority to update the status of credentials.

Here is a sample call to hasStatusAuthority in the context of Express.js middleware:

// retrieves status list manager
export async function getCredentialStatusManager(req, res, next) {
  try {
    req.statusManager = await getStatusManager();
    next();
  } catch (error) {
    return res.send('Failed to retrieve credential status list manager');
  }
}

// extracts access token from request header
function extractAccessToken(headers) {
  if (!headers.authorization) {
    return;
  }
  const [scheme, token] = headers.authorization.split(' ');
  if (scheme === 'Bearer') {
    return token;
  }
}

// verifies whether caller has access to status repo
async function verifyStatusRepoAccess(req, res, next) {
  const { headers } = req;
  // verify that access token was included in request
  const repoAccessToken = extractAccessToken(headers);
  if (!repoAccessToken) {
    return res.send('Failed to provide access token in request');
  }
  // check if caller has access to status repo
  const hasAccess = await req.statusManager.hasStatusAuthority(repoAccessToken);
  if (!hasAccess) {
    return res.send('Caller is unauthorized to access status repo');
  }
  next();
}

Note: This code assumes that getStatusManager either calls createStatusManager or retrieves an existing status manager instance created at an earlier point in time.

Dependencies

Create credential status repositories

GitHub 1. Login to GitHub 2. If you are using an organization as the owner account for the credential status manager and you don't already have an organization, click the plus icon in the top-right corner of the screen, click New organization and follow the instructions for creating a new organization * 3. Click the plus icon in the top-right corner of the screen and click New repository 4. Configure a blank public repository for the credential status repository *, with an optional description, that is owned by your account * 5. Click the plus icon in the top-right corner of the screen and click New repository 6. Configure a blank private repository for the credential status metadata repository *, with an optional description, that is owned by your account *

*Note: The names you choose for the owner account, credential status repository, and credential status metadata repository should be passed in respectively as ownerAccountName, repoName, and metaRepoName in invocations of createStatusManager. When you create these repositories, be sure not to add any files (including common default files like .gitignore, README.md, or LICENSE).

GitLab 1. Login to GitLab 2. If you are using a group as the owner account for the credential status manager and you don't already have a group, click the plus icon in the top-right corner of the screen, click New group and follow the instructions for creating a new group * 3. Click the plus icon in the top-right corner of the screen and click New project/repository 4. Configure a blank public repository for the credential status repository * that is owned by your account * 5. Click the plus icon in the top-right corner of the screen and click New project/repository 6. Configure a blank private repository for the credential status metadata repository * that is owned by your account *

*Note: The names you choose for the owner account, credential status repository, and credential status metadata repository, along with their IDs (which can be found at the main view for the repository/group), should be passed in respectively as ownerAccountName, repoName (ID: repoId), and metaRepoName (ID: metaRepoId) in invocations of createStatusManager. When you create these repositories, be sure not to add any files (including common default files like .gitignore, README.md, or LICENSE).

Generate access tokens

GitHub 1. Login to GitHub as an authorized member/representative of the owner account (ownerAccountName) 2. Click on your profile dropdown icon in the top-right corner of the screen 3. Select the Settings tab 4. Select the Developer settings tab toward the bottom of the left navigation panel 5. Select the Personal access tokens tab 6. Select the Fine-grained tokens tab 7. Click the Generate new token button 8. Enter a name, expiration date, and optional description for the access token 9. Select the appropriate resource owner for the credential status repositories * 10. Select Only select repositories and select the credential status repositories that you created earlier (repoName and/or metaRepoName) * 11. Select the Read and write access level for the Administration, Contents, and Pages permissions and keep the default Read-only access level for the Metadata permission 12. Click the Generate token button 13. Copy the generated token 14. Use the token as the value for repoAccessToken and/or metaRepoAccessToken in invocations of createStatusManager and hasStatusAuthority *

*Note: For the credential status metadata repository, you can either generate a separate access token and use that as the value for metaRepoAccessToken or include it along with repoAccessToken when selecting repositories. Whatever you decide, make sure to pass values for both of these values in invocations of createStatusManager (even though the latter option will result in the same value for these properties). If you are using an organization as the owner account for the credential status manager and ownerAccountName is not listed, follow these instructions to set a personal access token policy for it.

GitLab* 1. Login to GitLab as an authorized member/representative of the owner account (ownerAccountName) 2. Select the credential status repository (repoName) 3. Select the Settings tab in the left navigation panel 4. Select the Access Tokens tab within the Settings dropdown 5. Enter a name and expiration date for the access token 6. Select the Maintainer role 7. Select the api scope 8. Click the Create personal access token button 9. Copy the generated token 10. Use the token as the value for repoAccessToken in invocations of createStatusManager and hasStatusAuthority 11. Repeat these steps for metaRepoName and metaRepoAccessToken

*Note: At the time of this writing, group access tokens are only available in paid GitLab plans (i.e., Premium SaaS and Ultimate SaaS). Additionally, unlike other services, you cannot use the same access token for multiple repositories at this time (hence the need for repoAccessToken and metaRepoAccessToken). Finally, if you are unable to create access tokens, you are either on e free plan or you need to enable project access token creation.

Generate DID seeds

In order to generate a DID seed, you will need to use software that is capable of creating it in a format that corresponds to a valid DID document. Here is sample code that does this:

import { generateSecretKeySeed } from '@digitalcredentials/bnid';

// Set `didSeed` key to this value
const secretKeySeed = await generateSecretKeySeed();

If didMethod = web, you must also generate a DID document and host it at didWebUrl/.well-known/did.json. Here is sample code that does this:

import { decodeSecretKeySeed } from '@digitalcredentials/bnid';
import { Ed25519VerificationKey2020 } from '@digitalcredentials/ed25519-verification-key-2020';
import { X25519KeyAgreementKey2020 } from '@digitalcredentials/x25519-key-agreement-key-2020';
import * as DidWeb from '@interop/did-web-resolver';
import { CryptoLD } from '@digitalcredentials/crypto-ld';

const cryptoLd = new CryptoLD();
cryptoLd.use(Ed25519VerificationKey2020);
cryptoLd.use(X25519KeyAgreementKey2020);
const didWebDriver = DidWeb.driver({ cryptoLd });

const decodedSeed = decodeSecretKeySeed({secretKeySeed});

// Host this document at `didWebUrl`/.well-known/did.json
const didWebUrl = 'https://university-xyz.edu';
const didDocument = didWebDriver.generate({ url: didWebUrl, seed: decodedSeed });

Contribute

PRs accepted.

If editing the Readme, please conform to the standard-readme specification.

License

MIT License © 2023 Digital Credentials Consortium.