rabobank-blockchain/ula-vp-controller

View on GitHub
src/service/verifiable-credential-helper.ts

Summary

Maintainability
A
2 hrs
Test Coverage
/*
 *  Copyright 2020 Coöperatieve Rabobank U.A.
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

import { ChallengeRequest, VerifiableCredential } from 'vp-toolkit-models'
import { EventHandler } from 'universal-ledger-agent'
import { Address } from 'ula-vc-data-management'
import { AddressHelper } from './address-helper'
import { VerifiableCredentialGenerator } from 'vp-toolkit'

export class VerifiableCredentialHelper {

  constructor (private _vcGenerator: VerifiableCredentialGenerator, private _addressHelper: AddressHelper) {
  }

  /**
   * Returns a collection of self-attested VC's to prove ownership over our DID's
   *
   * @param {ChallengeRequest} challengeRequest
   * @param {number} accountId provided by the wallet implementation
   * @param {EventHandler} eventHandler
   * @return {Promise<VerifiableCredential[]>} the self-attested VC's
   */
  public async generateSelfAttestedVCs (challengeRequest: ChallengeRequest, accountId: number, eventHandler: EventHandler):
    Promise<{ accountId: number, keyId: number, vc: VerifiableCredential }[]> {
    const verifiableCredentials: { accountId: number, keyId: number, vc: VerifiableCredential }[] = []
    for (const toAttest of challengeRequest.toAttest) {
      const addressDetails: Address = await this._addressHelper.generateAndSaveAddressDetails(accountId, toAttest.predicate, eventHandler)
      const did = 'did:eth:' + addressDetails.address
      const selfAttestedVc = this._vcGenerator.generateVerifiableCredential({
        type: ['VerifiableCredential', 'DidOwnership'],
        credentialSubject: {
          id: did,
          predicate: toAttest.predicate
        },
        '@context': [toAttest.predicate],
        issuanceDate: new Date(),
        issuer: did
      }, accountId, addressDetails.keyId)
      verifiableCredentials.push({ accountId: accountId, keyId: addressDetails.keyId, vc: selfAttestedVc })
    }

    return verifiableCredentials
  }

  /**
   * The verifier asks one or more VC's
   * using the toVerify field in the
   * ChallengeRequest. This method returns
   * a collection of VC's which match the
   * verifier's needs.
   *
   * @param {ChallengeRequest} challengeRequest
   * @param  {EventHandler} eventHandler
   * @return {Promise<VerifiableCredential[]>} the self-attested VC's
   */
  public async findVCsForChallengeRequest (challengeRequest: ChallengeRequest, eventHandler: EventHandler):
    Promise<{ matching: VerifiableCredential[], missing: { predicate: string, reason: string }[] }> {
    if (challengeRequest.toVerify.length === 0) {
      return {
        matching: [],
        missing: []
      }
    }

    const matchingVerifiableCredentials: { credential: VerifiableCredential, predicate: string }[] = []
    const failedCredentials: { credential: VerifiableCredential, predicate: string }[] = [] // These credentials do not pass the whitelist check
    const missingVCs: { predicate: string, reason: string }[] = []
    const regex = new RegExp('(' + challengeRequest.toVerify.map(value => value.predicate).join(')|(') + ')', 'g')
    await new Promise(async (resolve) => {
      // Get all credentials that match contain the predicate in their context arrays
      await eventHandler.processMsg(
        {
          type: 'get-vcs-by-context',
          contextRegex: regex
        },
        async (credentials: VerifiableCredential[]) => {
          // The challengeRequest.toVerify array contains predicate+whitelist

          // Step 1: Check which credentials do and don't pass the issuer whitelist check
          for (const toVerify of challengeRequest.toVerify) {
            let markAsMissing = true
            for (const credential of credentials) {
              const credentialSubjects = Object.keys(credential.credentialSubject)
              if (credentialSubjects.includes(toVerify.predicate)) {

                // Step 1.1: Incase the issuer whitelist is not present or it matches the whitelist,
                // mark the credential as 'keep this one'
                if ((toVerify.allowedIssuers === undefined
                  || toVerify.allowedIssuers.length === 0
                  || toVerify.allowedIssuers.includes(credential.issuer))) {
                  markAsMissing = false
                  matchingVerifiableCredentials.push({ credential: credential, predicate: toVerify.predicate })
                } else {
                  // Step 1.2: If the credential does not pass the whitelist check, mark it as 'remove this'.
                  // The credential can contain multiple subjects (claims), so one claim might pass the check
                  // while other claims might not. It is imperative that the credentials from step 2.1 are
                  // not removed. Only remove a credential if it doesn't pass for all claims completely, after
                  // this loop.
                  markAsMissing = false // Do not mark it as missing, let step 2 mark it differently
                  failedCredentials.push({ credential: credential, predicate: toVerify.predicate })
                }
              }
            }

            // Step 1.3: If there were no matching credentials found, add the predicate to the 'missing' array.
            // The user interface will show that there is no matching credential for this predicate.
            if (markAsMissing) {
              missingVCs.push({ predicate: toVerify.predicate, reason: 'missing' })
            }
          }

          // Step 2: Remove credentials if they don't match the whitelist check completely (for all credSubjects)
          for (const failedCred of failedCredentials) {
            if (!matchingVerifiableCredentials.map(x => x.predicate).includes(failedCred.predicate)
              && !this.containsMissingPredicate(missingVCs, failedCred.predicate)) {
              missingVCs.push({
                predicate: failedCred.predicate,
                reason: 'no-matching-issuer'
              })
            }
          }

          resolve()
        })
    })

    // Finally, return all matching credentials and missing predicates. Make sure not to send duplicated results.
    return {
      matching: this.getUniqueCredentials(matchingVerifiableCredentials),
      missing: missingVCs
    }
  }

  /**
   * Removes revoked credentials, saves issued
   * credentals and saves a Transaction object.
   *
   * @todo remove revoked credentials
   * @param {string} counterpartyId              The id of the counterparty
   * @param {string[]} verifiedVcs               Collection of VerifiableCredential nonces which were sent
   * @param {VerifiableCredential[]} credentials VP sent by the counterparty, containing attested VC's
   * @param {EventHandler} eventHandler          To send messages to the ULA data plugin
   */
  public async processTransaction (
    counterpartyId: string,
    verifiedVcs: string[],
    credentials: VerifiableCredential[],
    eventHandler: EventHandler) {
    await this.saveIssuedVCs(credentials, eventHandler)

    const transaction = {
      created: new Date(),
      counterpartyId: counterpartyId,
      state: 'success',
      issuedVcs: credentials.map(vc => vc.proof.nonce),
      verifiedVcs: verifiedVcs
    }
    await eventHandler.processMsg({ type: 'save-vc-transaction', transaction: transaction }, undefined)
  }

  /**
   * Save the Verifiable Credentials which
   * were sent by the issuer. A VC will only
   * be saved when the DID + predicate
   * matches with the address details in
   * storage.
   * This method does NOT verify any
   * signatures!
   *
   * @param {VerifiableCredential[]} credentials The Verifiable Presentation from the issuer
   * @param {EventHandler} eventHandler
   */
  public async saveIssuedVCs (credentials: VerifiableCredential[], eventHandler: EventHandler) {
    const promises: Promise<void>[] = []
    const vcsToSave: VerifiableCredential[] = []
    for (const vc of credentials) {
      // Todo: Implement universal resolver to resolve the DID
      // Right now, we assume that the given DID is in the following format - did:xx:publicAddress
      const pubAddress = (vc.credentialSubject.id as string).split(':').pop()
      promises.push(new Promise(async (resolve) => {
        await eventHandler.processMsg(
          { type: 'get-address-details', publicAddress: pubAddress },
          async (address: Address) => {
            if (vc.context && vc.context.includes(address.predicate)) {
              vcsToSave.push(vc)
            }
            resolve()
          })
      }))
    }

    await Promise.all(promises)
    await eventHandler.processMsg({ type: 'save-vcs', verifiableCredentials: vcsToSave }, undefined)
  }

  private getUniqueCredentials (array: { credential: VerifiableCredential; predicate: string }[]) {
    return [...new Set(array.map(x => x.credential))]
  }

  private containsMissingPredicate (array: { predicate: string, reason: string }[], predicate: string) {
    return array.filter(x => x.predicate === predicate).length > 0
  }
}