<file_sep>all: docs

docs:
	npx jsdoc -r -c jsdoc.json -d doc --verbose --pedantic . README.md

lock:
	rm -r node_modules || true
	yarn install
	rm -r node_modules
	pnpm i

clean:
	rm -r doc
<file_sep># Trustedkey Javascript SDK

[![CircleCI](https://circleci.com/gh/trustedkey/trustedkey.js.svg?style=svg)](https://circleci.com/gh/trustedkey/trustedkey.js)
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)


## Generate docs
```
make
```
<file_sep>//
//  claims.js
//
//  Copyright © 2018 Trusted Key Solutions. All rights reserved.
//
const OID = require('./oid')

/**
 * Some well-known OpenID Connect claims, for convenience.
 * @type {Object.<string,Dotted?>}
 *
 * @exports Claims
 */
module.exports = {
  name: OID.commonName,
  family_name: OID.surname,
  given_name: OID.givenName,
  profile: OID.socialProfile,
  picture: OID.documentImageHead, // FIXME: should return URL
  email: OID.emailAddress,
  email_verified: OID.emailAddress,
  address: OID.postalAddress,
  phone_number: OID.telephoneNumber,
  phone_number_verified: OID.telephoneNumber,
  gender: OID.gender,
  birthdate: OID.dateOfBirth,
  middle_name: OID.middleName,
  preferred_username: null,
  'https://auth.trustedkey.com/root': null,
  // These are the known OIDs, excluding claims declared by OIDC spec:
  'https://auth.trustedkey.com/documentID': OID.documentID,
  'https://auth.trustedkey.com/documentType': OID.documentType,
  'https://auth.trustedkey.com/documentClass': OID.documentClass,
  'https://auth.trustedkey.com/documentImageFront': OID.documentImageFront,
  'https://auth.trustedkey.com/documentImageBack': OID.documentImageBack,
  'https://auth.trustedkey.com/documentIssuer': OID.documentIssuer,
  'https://auth.trustedkey.com/documentResult': OID.documentResult,
  'https://auth.trustedkey.com/documentIssueDate': OID.documentIssueDate,
  'https://auth.trustedkey.com/documentDigest': OID.documentDigest,
  'https://auth.trustedkey.com/documentThumb': OID.documentThumb,
  'https://auth.trustedkey.com/country': OID.country,
  'https://auth.trustedkey.com/locality': OID.locality,
  'https://auth.trustedkey.com/postalCode': OID.postalCode,
  'https://auth.trustedkey.com/stateOrProvinceName': OID.stateOrProvinceName,
  'https://auth.trustedkey.com/organization': OID.organization,
  'https://auth.trustedkey.com/placeOfBirth': OID.placeOfBirth,
  'https://auth.trustedkey.com/streetAddress': OID.streetAddress,
  'https://auth.trustedkey.com/courseName': OID.courseName,
  'https://auth.trustedkey.com/courseContents': OID.courseContents,
  'https://auth.trustedkey.com/publicKey': OID.publicKey,
  'https://auth.trustedkey.com/levelOfAssurance': OID.levelOfAssurance,
  'https://auth.trustedkey.com/age21OrUp': OID.age21OrUp,
  'https://auth.trustedkey.com/memberId': OID.memberId,
  'https://auth.trustedkey.com/groupId': OID.groupId,
  'https://auth.trustedkey.com/suffix': OID.suffix,
  'https://auth.trustedkey.com/endpoint': OID.endpoint,
  'https://auth.trustedkey.com/docRef': OID.docRef
}
<file_sep>/* eslint-env mocha */
const Assert = require('assert')
const OID = require('../oid')

describe('OID', function () {
  it('all have dotted notation', function () {
    Object.values(OID).forEach(oid => oid.split('.').forEach(i => Assert.ok(i > 0 && i < 100000000)))
  })
  it('are all unique', function () {
    const inv = {}
    Object.values(OID).forEach(oid => {
      Assert.strictEqual(inv[oid], undefined)
      inv[oid] = oid
    })
  })
})
<file_sep>/* eslint-env mocha */
const Assert = require('assert')
const Express = require('express')
const RP = require('request-promise-native')
const Utils = require('../utils')
const Http = require('../services/http')
const WalletService = require('../services/walletservice')

describe('http server', function () {
  let app, http, server
  const secret = 'secret'
  const appid = 'id'

  before(done => {
    app = Express()
    app.use(require('body-parser').raw({type: '*/*'}))
    server = app.listen(0, (err, p) => {
      Assert.strictEqual(err, undefined)
      http = new Http(`http://localhost:${server.address().port}`, appid, secret)
      done()
    })
  })

  after(() => {
    // server.close() would wait for all keep-alive sockets to timeout.
    server.unref()
  })

  describe('http', function () {
    before(function () {
      function Auth (req, res, next) {
        const fullUrl = req.protocol + '://' + req.get('host') + req.originalUrl
        if (req.headers.authorization) {
          Assert.strictEqual(req.headers.authorization.split(' ')[0], 'Bearer')
          const claims = Utils.verifyJws(req.headers.authorization.split(' ')[1], secret)
          Assert.strictEqual(claims.aud, fullUrl)
          Assert.strictEqual(claims.iss, appid)
          Assert.ok(claims.iat <= Utils.getUnixTime())
          Assert.ok(claims.exp > Utils.getUnixTime())
          if (req.body && req.body.length > 0) {
            Assert.strictEqual(Utils.sha256(req.body, 'hex'), claims.body)
          } else {
            Assert.strictEqual(claims.body, null)
          }
          next()
        } else {
          next(Error('Auth fail'))
        }
      }
      app.get('/get', Auth, (req, res) => res.send('gotten'))
      app.post('/post', Auth, (req, res) => res.send('posted'))
      app.delete('/delete', Auth, (req, res) => res.send('deleted'))
      app.all('/keep', (req, res) => res.send(req.get('connection')))
    })

    it('has Connection: keep-alive', async function () {
      Assert.strictEqual(await http.get('/keep'), 'keep-alive')
      Assert.strictEqual(await http.post('/keep'), 'keep-alive')
    })

    it('can build URL without params', function () {
      const h = http.buildUrl('/test')
      Assert.strictEqual(h, `http://localhost:${server.address().port}/test`)
    })

    it('can build URL with params', function () {
      const h = http.buildUrl('/test', {a: 2})
      Assert.strictEqual(h, `http://localhost:${server.address().port}/test?a=2`)
    })

    it('can create JWT header without body', async function () {
      const h = http.getHeaders('url')
      Assert.strictEqual(h.Authorization.split(' ')[0], 'Bearer')
      const claims = Utils.verifyJws(h.Authorization.split(' ')[1], secret)
      const payload = {
        iss: appid,
        aud: 'url',
        iat: claims.iat,
        exp: claims.exp,
        body: null
      }
      Assert.deepStrictEqual(claims, payload)
    })

    it('can create JWT header with body', async function () {
      const h = http.getHeaders('url', 'body')
      Assert.strictEqual(h.Authorization.split(' ')[0], 'Bearer')
      const claims = Utils.verifyJws(h.Authorization.split(' ')[1], secret)
      const payload = {
        iss: appid,
        aud: 'url',
        iat: claims.iat,
        exp: claims.exp,
        body: Utils.sha256('body', 'hex')
      }
      Assert.deepStrictEqual(claims, payload)
    })

    it('can POST without contents', async function () {
      Assert.strictEqual(await http.post('post'), 'posted')
    })

    it('can POST with query parameters', async function () {
      Assert.strictEqual(await http.post('post', {a: 4}), 'posted')
    })

    it('can POST string', async function () {
      Assert.strictEqual(await http.post('post', {}, 's'), 'posted')
    })

    it('can POST object', async function () {
      Assert.strictEqual(await http.post('post', {}, {a: 2}), 'posted')
    })

    it('can GET', async function () {
      Assert.strictEqual(await http.get('get'), 'gotten')
    })

    it('can GET with query parameters', async function () {
      Assert.strictEqual(await http.get('get', {a: 3}), 'gotten')
    })

    it('can GET with trailing slash', async function () {
      Assert.strictEqual(await http.get('/get', {a: 3}), 'gotten')
    })

    it('can GET with absolute url', async function () {
      Assert.strictEqual(await http.get(`http://localhost:${server.address().port}/get`), 'gotten')
    })

    it('can DELETE', async function () {
      Assert.strictEqual(await http.delete('delete'), 'deleted')
    })

    it('can DELETE with query parameters', async function () {
      Assert.strictEqual(await http.delete('delete', {b: 1}), 'deleted')
    })

    it('can DELETE with trailing slash', async function () {
      Assert.strictEqual(await http.delete('/delete', {b: 1}), 'deleted')
    })

    it('can DELETE with absolute url', async function () {
      Assert.strictEqual(await http.delete(`http://localhost:${server.address().port}/delete`), 'deleted')
    })

    it('reused connections', async function () {
      const count = await new Promise((resolve, reject) => server.getConnections((err, count) => err ? reject(err) : resolve(count)))
      Assert.strictEqual(count, 1)
    })
  })

  context('wallet OAuth', function () {
    const Code = 'codex'
    const State = 'statx'
    const AccessToken = 'access_tokenx'
    const UserInfo = {name: '<NAME>'}
    const RedirectUri = `https://localhost:123/callback`
    let walletservice

    before(function () {
      walletservice = new WalletService(`http://localhost:${server.address().port}`, appid, secret)
      app.get('/oauth/authorize', (req, res) => {
        Assert.deepStrictEqual(req.query, {
          redirect_uri: RedirectUri,
          client_id: appid,
          state: State,
          response_type: 'code',
          scope: 'openid'
        })
        const callback = Utils.mergeQueryParams(req.query.redirect_uri, {
          code: Code
        })
        res.redirect(callback)
      })
      app.post('/oauth/token', (req, res) => {
        const params = req.body.toString().split('&').map(kv => kv.split('=')).reduce((p, [k, v]) => {
          p[k] = decodeURIComponent(v)
          return p
        }, {})
        Assert.deepStrictEqual(params, {
          client_id: appid,
          client_secret: secret,
          grant_type: 'authorization_code',
          code: Code,
          redirect_uri: RedirectUri
        })
        res.json({
          access_token: AccessToken,
          token_type: 'Bearer',
          id_token: 'id_token'
        })
      })
      app.get('/oauth/user', (req, res) => {
        Assert.strictEqual(req.headers.authorization, `Bearer ${AccessToken}`)
        res.json(UserInfo)
      })
    })

    it('can build /authorize URL', function () {
      const url = walletservice.buildAuthorizeUrl(RedirectUri, State)
      Assert.strictEqual(url, `http://localhost:${server.address().port}/oauth/authorize?client_id=id&redirect_uri=${encodeURIComponent(RedirectUri)}&state=${State}&response_type=code&scope=openid`)
    })

    it('can get code from /authorize', async function () {
      const url = walletservice.buildAuthorizeUrl(RedirectUri, State)
      try {
        throw Error(await RP.get(url, {followRedirect: false}))
      } catch (err) {
        Assert.strictEqual(err.message, `302 - "Found. Redirecting to ${RedirectUri}?code=${Code}"`)
      }
    })

    it('can get access_token from /token', async function () {
      const grant = await walletservice.token(RedirectUri, Code)
      Assert.deepStrictEqual(grant, {
        'access_token': AccessToken,
        'id_token': 'id_token',
        'token_type': 'Bearer'
      })
    })

    it('can get user info from /user', async function () {
      const userInfo = await walletservice.userInfo('access_tokenx')
      Assert.deepStrictEqual(userInfo, UserInfo)
    })
  })
})
<file_sep>const Assert = require('assert')
const Utils = require('../utils')
const HttpUtils = require('./http')

module.exports = ValidateService

/**
 * An implementation of a the validation API, used to check to validity of credentials and claims.
 *
 * @constructor
 * @param {String} backendUrl The base backend URL
 */
function ValidateService (backendUrl = 'https://issuer.trustedkey.com') {
  this.httpClient = new HttpUtils(backendUrl)
}

function validate (httpClient, address) {
  Assert.strictEqual(typeof address, 'string', 'address must be of type `string`')

  return httpClient.get('isRevoked', {
    address: address
  }).then(r => {
    return r.data.isRevoked === false
  })
}

function makeAddressList (addresses) {
  if (addresses instanceof Array) {
    return addresses.map(Utils.serialToAddress).join(',')
  } else {
    return Utils.serialToAddress(addresses)
  }
}

/**
 * Check the status of the specified blockchain transaction ID.
 *
 * @param {String} txid Transaction ID to check.
 * @returns {Promise.<string>} Transaction status
*/
ValidateService.prototype.getTransactionStatus = function (txid) {
  Assert.strictEqual(typeof txid, 'string', 'txid must be of type `string`')

  return this.httpClient.get('getTransactionStatus', {
    txid: txid
  }).then(r => {
    return r.data.getTransactionStatus
  })
}

/**
 * Validate the given credential by calling into the smart contract.
 *
 * @param {String} credentialAddressString Credential to check.
 * @returns {Promise.<boolean>} Status indicating valid address
*/
ValidateService.prototype.validateCredential = function (credentialAddressString) {
  return validate(this.httpClient, credentialAddressString)
}

/**
 * Validate given claim(s) by calling into the smart contract.
 *
 * @param {String|Array.<string>} claimSerialNumbers Array of claim serial numbers.
 * @returns {Promise.<boolean>} Status indicating valid address
*/
ValidateService.prototype.validateClaims = function (claimSerialNumbers) {
  const addresses = makeAddressList(claimSerialNumbers)
  return validate(this.httpClient, addresses)
}

/**
 * @typedef KeyInfo
 * @type {object}
 * @property {boolean} isRevoked whether the address was revoked
 * @property {number} timestamp the unix-epoch timestamp of the last operation
 * @property {string} revokedBy the address of the revoker
 * @property {string} replaces the address of the credential that is replaced by this
 * @property {string} recovery the address of the registered recovery key
 * @property {string} rootAddress the root address of this credential
 *
 * @typedef {Object.<string,KeyInfo>} KeyInfoMap
 */

/**
 * Get extensive key information for given address.
 *
 * @param {String|Array.<string>} address blockchain address(es) of claim/credential to query
 * @returns {Promise.<KeyInfoMap>} KeyInfoMap structure from smart contract
*/
ValidateService.prototype.keyInfo = function (address) {
  const addresses = makeAddressList(address)
  return this.httpClient.get('keyInfo', {address: addresses})
    .then(r => r.data)
}
<file_sep>//
//  oid.js
//
//  Copyright © 2017 Trusted Key Solutions. All rights reserved.
//

/**
 * @typedef {string} Dotted
 *
 * Some well-known OIDs, for convenience.
 * @type {Object.<string,Dotted>}
 *
 * @exports oid
 */
module.exports = {

  /**
     * The OID in dotted notation for an identity document's ID number.
     * @constant
     * @default
     */
  documentID: '1.3.6.1.4.1.51341.2',

  /**
     * The OID in dotted notation for an identity document's type, for example "Michigan (MI) Driver License".
     * @constant
     * @default
     */
  documentType: '1.3.6.1.4.1.51341.1',

  /**
     * The OID in dotted notation for an identity document's class, for example "Passport", "Drivers License".
     * @constant
     * @default
     */
  documentClass: '1.3.6.1.4.1.51341.6',

  /**
     * The OID in dotted notation for an identity document's photo.
     * @constant
     * @default
     */
  documentImageFront: '1.3.6.1.4.1.51341.3',

  /**
     * The OID in dotted notation for an identity document's photo.
     * @constant
     * @default
     */
  documentImageBack: '1.3.6.1.4.1.51341.7',

  /**
     * The OID in dotted notation for an identity document's photo (headshot).
     * @constant
     * @default
     */
  documentImageHead: '1.3.6.1.4.1.51341.8',

  /**
     * The OID in dotted notation for an identity document's issuer.
     * @constant
     * @default
     */
  documentIssuer: '1.3.6.1.4.1.51341.4',

  /**
     * The OID in dotted notation for the result of the document verification.
     * @constant
     * @default
     */
  documentResult: '1.3.6.1.4.1.51341.5',

  /**
     * The OID in dotted notation for the original documents issue date.
     * @constant
     * @default
     */
  documentIssueDate: '1.3.6.1.4.1.51341.9',

  /**
     * The OID in dotted notation for the original documents SHA256 digest.
     * @constant
     * @default
     */
  documentDigest: '1.3.6.1.4.1.51341.10',

  /**
     * The OID in dotted notation for the documents Base64 thumbnail.
     * @constant
     * @default
     */
  documentThumb: '1.3.6.1.4.1.51341.11',

  /**
     * The OID in dotted notation for the gender (F/M) on an identity document.
     * @constant
     * @default
     */
  gender: '1.3.6.1.5.5.7.9.3',

  /**
     * The OID in dotted notation for a person's email address.
     * @constant
     * @default
     */
  emailAddress: '1.2.840.113549.1.9.1',

  /**
     * The OID in dotted notation for a person's full name.
     * @constant
     * @default
     */
  commonName: '2.5.4.3',

  /**
     * The OID in dotted notation for the birthday on an identity document.
     * @constant
     * @default
     */
  dateOfBirth: '1.3.6.1.5.5.7.9.1',

  /**
     * The OID in dotted notation for a person's registered phone number.
     * @constant
     * @default
     */
  telephoneNumber: '2.5.4.20',

  /**
     * The OID in dotted notation for a person's last name.
     * @constant
     * @default
     */
  surname: '2.5.4.4',

  /**
     * The OID in dotted notation for a document's country.
     * @constant
     * @default
     */
  country: '2.5.4.6',

  /**
     * The OID in dotted notation for the locality.
     * @constant
     * @default
     */
  locality: '2.5.4.7',

  /**
     * The OID in dotted notation for the postal code.
     * @constant
     * @default
     */
  postalCode: '2.5.4.17',

  /**
     * The OID in dotted notation for the state or province.
     * @constant
     * @default
     */
  stateOrProvinceName: '2.5.4.8',

  /**
     * The OID in dotted notation for the organization's name.
     * @constant
     * @default
     */
  organization: '2.5.4.10',

  /**
     * The OID in dotted notation for a person's first name.
     * @constant
     * @default
     */
  givenName: '2.5.4.42',

  /**
     * The OID in dotted notation for the birth place on an identity document.
     * @constant
     * @default
     */
  placeOfBirth: '1.3.6.1.5.5.7.9.2',

  /**
     * The OID in dotted notation for a person's registered postal address.
     * @constant
     * @default
     */
  postalAddress: '2.5.4.16',

  /**
     * The OID in dotted notation for a person's street address.
     * @constant
     * @default
     */
  streetAddress: '2.5.4.9',

  /**
     * The OID in dotted notation for a person's social profile URL.
     * @constant
     * @default
     */
  socialProfile: '1.3.6.1.4.1.51341.12',

  /**
     * The OID in dotted notation for a completed course's name.
     * @constant
     * @default
     */
  courseName: '1.3.6.1.4.1.51341.13',

  /**
     * The OID in dotted notation for a completed course's contents.
     * @constant
     * @default
     */
  courseContents: '1.3.6.1.4.1.51341.14',

  /**
     * The OID in dotted notation for the user's public key.
     * @constant
     * @default
     */
  publicKey: '1.3.6.1.4.1.51341.15',

  /**
     * The OID in dotted notation for the claim's level of assurance.
     * @constant
     * @default
     */
  levelOfAssurance: '1.3.6.1.4.1.51341.16',

  /**
     * The OID in dotted notation for a claim that the user is at least 21 years old.
     * @constant
     * @default
     */
  age21OrUp: '1.3.6.1.4.1.51341.21',

  /**
     * The OID in dotted notation for the member Id number.
     * @constant
     * @default
     */
  memberId: '1.3.6.1.4.1.51341.22',

  /**
       * The OID in dotted notation for the group Id number.
       * @constant
       * @default
       */
  groupId: '1.3.6.1.4.1.51341.23',

  /**
       * The OID in dotted notation for a person's middle name.
       * @constant
       * @default
       */
  middleName: '1.3.6.1.4.1.51341.17',

  /**
       * The OID in dotted notation for a person's suffix. eg. Jr. Sr.
       * @constant
       * @default
       */
  suffix: '1.3.6.1.4.1.51341.18',

  /**
       * The OID in dotted notation for distributed claim whose actual value needs to be extracted from an endpoint.
       * @constant
       * @default
       */
  endpoint: '1.3.6.1.4.1.51341.24',

  /**
       * THE OID is dotted notion for DocRef which is assigned by Issuer
       * @constant
       * @default
       */
  docRef: '1.3.6.1.4.1.51341.25'
}
<file_sep>/* eslint-env mocha */
const Assert = require('assert')
const Utils = require('../utils')
const FS = require('fs')

describe('Utils', function () {
  const jwkEC = {'kty': 'EC', 'crv': 'P-256', 'x': '<KEY>', 'y': '<KEY>'}
  const pemEC = `-----BEGIN PUBLIC KEY-----
<KEY>
-----END PUBLIC KEY-----`
  const hexEC = '<KEY>'
  const jwkRSA = {'kty': 'RSA', 'n': '<KEY>', 'e': 'AQAB'}
  const pemRSA = `-----BEGIN PUBLIC KEY-----
<KEY>
-----END PUBLIC KEY-----`
  const pemECpk = `-----BEGIN EC PRIVATE KEY-----
<KEY>
-----END EC PRIVATE KEY-----`

  context('serial', function () {
    it('Converts serialNo to address', function () {
      Assert.strictEqual(Utils.serialToAddress('e3b0c44298fc1c149afbf4c8996fb92427ae41e4'), '0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4')
      Assert.strictEqual(Utils.serialToAddress('b0c44298fc1c149afbf4c8996fb92427ae41e4'), '0x00b0c44298fc1c149afbf4c8996fb92427ae41e4')
      Assert.strictEqual(Utils.serialToAddress('4'), '0x0000000000000000000000000000000000000004')
      Assert.strictEqual(Utils.serialToAddress('0x4'), '0x0000000000000000000000000000000000000004')
    })
    it('Is a NOP for addresses', function () {
      Assert.strictEqual(Utils.serialToAddress('0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4'), '0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4')
      Assert.strictEqual(Utils.serialToAddress('0x00b0c44298fc1c149afbf4c8996fb92427ae41e4'), '0x00b0c44298fc1c149afbf4c8996fb92427ae41e4')
      Assert.strictEqual(Utils.serialToAddress('0x00b0c44298fc1c149afbf4c8996fb92427ae41e4,0x00b0c44298fc1c149afbf4c8996fb92427ae41e4'), '0x00b0c44298fc1c149afbf4c8996fb92427ae41e4,0x00b0c44298fc1c149afbf4c8996fb92427ae41e4')
    })
  })

  context('address', function () {
    it('Passes for valid addresses', function () {
      Assert.strictEqual(Utils.isAddress('0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4'), true)
      Assert.strictEqual(Utils.isAddress('0xE3b0c44298fC1c149afbf4D8996fb92427ae41e4'), true)
    })
    it('Fails for invalid addresses', function () {
      Assert.strictEqual(Utils.isAddress('asdf'), false)
      Assert.strictEqual(Utils.isAddress('0xe3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'), false)
    })
  })

  context('jwkToHex', function () {
    it('converts RSA public key', function () {
      const hexRSA = '<KEY>dbea26593ea4a1d617f1ffbaa7ee0e0a7a372c7139c8ce143a41199dcd653b3e55cd46be75e304c242c95e2aae135c106e703ac9a408772f2f0033612038502b121114e1436e4d52d35b0497e39cdd515c63456ca1aa425690047ebd4b0c27ad39ee91c6cbc50c57b1d3d9eebd6e5e8456e38a83af63afe31bed597132816ac0d531614ba040b39587cf0507c18a2a76ad6533596f5c8429c1d3e6f6f5083847d9f58254d5aaa3508f417d82b59eb495f0d8d5d039530a9fec3cd56cf01f8b3721943'
      Assert.strictEqual(Utils.jwkToHex(jwkRSA), hexRSA)
    })
    it('converts EC public key', function () {
      Assert.strictEqual(Utils.jwkToHex(jwkEC), hexEC)
    })
    it('throws on invalid jwk', function () {
      Assert.throws(() => Utils.jwkToHex({kty: 'RSA'}), /Unsupported/)
      Assert.throws(() => Utils.jwkToHex({}), /Unsupported/)
    })
    it('throws on RSA jwk with PK', function () {
      Assert.throws(() => Utils.jwkToHex({d: 's83ZmuWKtcqbpnME5112vxZqpKpCFctE4Jye_BneVxE', ...jwkEC}))
      Assert.throws(() => Utils.jwkToHex({d: '<KEY>', ...jwkRSA}))
    })
  })

  context('hexToJwk', function () {
    it('EC public key', function () {
      const jwk = Utils.hexToJwk(hexEC)
      Assert.deepEqual(jwk, jwkEC)
    })
  })

  context('pemToJwk', function () {
    it('converts RSA public key', function () {
      Assert.deepStrictEqual(Utils.pemToJwk(pemRSA), jwkRSA)
      Assert.deepStrictEqual(Utils.pemToJwk(`-----BEGIN PUBLIC KEY-----
<KEY>
-----END PUBLIC KEY-----`), {
        'e': 'AQAB',
        'kty': 'RSA',
        'n': '<KEY>'
      })
    })
    it('converts EC public key', function () {
      Assert.deepStrictEqual(Utils.pemToJwk(pemEC), jwkEC)
    })
    it('converts EC private key', function () {
      Assert.deepStrictEqual(Utils.pemToJwk(`-----<KEY>`),
      {
        'crv': 'P-256',
        'd': '<KEY>',
        'kty': 'EC',
        'x': '<KEY>',
        'y': '<KEY>'
      })
    })
    it('converts EC private key', function () {
      Assert.deepStrictEqual(Utils.pemToJwk(pemECpk), {
        'crv': 'P-256',
        'd': '<KEY>',
        'kty': 'EC',
        'x': '<KEY>',
        'y': '<KEY>'
      })
    })
    it('converts small RSA public key', function () {
      Assert.deepStrictEqual(Utils.pemToJwk(`-----BEGIN PUBLIC KEY-----
<KEY>
-----END PUBLIC KEY-----`),
      {
        'kty': 'RSA',
        'n': '<KEY>',
        'e': 'AQAB'
      })
    })
    it('converts small RSA public key, e=3', function () {
      Assert.deepStrictEqual(Utils.pemToJwk(`-----BEGIN PUBLIC KEY-----
<KEY>
-----END PUBLIC KEY-----`),
      {
        'kty': 'RSA',
        'n': '<KEY>',
        'e': 'Aw'
      })
    })
    it('throws on invalid PEM', function () {
      Assert.throws(() => Utils.pemToJwk(``), /Unsupported/)
    })
  })

  context('jwkToPem', function () {
    it('converts RSA public key', function () {
      Assert.strictEqual(Utils.jwkToPem(jwkRSA), pemRSA)
    })
    it('converts EC public key', function () {
      Assert.strictEqual(Utils.jwkToPem(jwkEC), pemEC)
    })
    it('converts EC private key', function () {
      const jwk = {...jwkEC, d: 's83ZmuWKtcqbpnME5112vxZqpKpCFctE4Jye_BneVxE'}
      Assert.strictEqual(Utils.jwkToPem(jwk), pemECpk)
    })
    it('converts EC private key jwk->pem->jwk', function () {
      const jwk = {...jwkEC, d: 's83ZmuWKtcqbpnME5112vxZqpKpCFctE4Jye_BneVxE'}
      Assert.deepStrictEqual(Utils.pemToJwk(Utils.jwkToPem(jwk)), jwk)
    })
    it('converts EC private key v.v. pem->jwk->pem', function () {
      Assert.strictEqual(Utils.jwkToPem(Utils.pemToJwk(pemECpk)), pemECpk)
    })
  })

  context('mergeQueryParams', function () {
    it('accepts null arg', function () {
      Assert.strictEqual(Utils.mergeQueryParams('abc', null), 'abc')
    })
    it('merges params', function () {
      Assert.strictEqual(Utils.mergeQueryParams('abc', {a: 2, b: 'b c'}), 'abc?a=2&b=b%20c')
    })
  })

  context('sha256', function () {
    it('accepts and returns buffer', function () {
      Assert.ok(Utils.sha256(Buffer.from('')) instanceof Buffer, 'Expected instanceof Buffer')
    })
    it('takes optional encoding', function () {
      Assert.strictEqual(Utils.sha256('', 'hex'), 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855')
      Assert.strictEqual(Utils.sha256('', 'base64'), '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=')
    })
  })

  context('keccak256', function () {
    it('accepts and returns buffer', function () {
      Assert.ok(Utils.keccak256(Buffer.from('')) instanceof Buffer, 'Expected instanceof Buffer')
    })
    it('takes optional encoding', function () {
      Assert.strictEqual(Utils.keccak256('', 'hex'), 'c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470')
      Assert.strictEqual(Utils.keccak256('', 'base64'), 'xdJGAYb3IzySfn2y3McDwOUAtlPKgic7e/rYBF2FpHA=')
    })
  })

  context('base64url', function () {
    it('takes Buffer, returns string', function () {
      const base64urlTest = '_-w'
      Assert.strictEqual(Utils.base64url(Buffer.from(base64urlTest, 'base64')), base64urlTest)
    })
  })

  context('jws', function () {
    const msg = 'msg'

    context('none', function () {
      it('verifyJws', function () {
        Assert.strictEqual(Utils.verifyJws('eyJhbGciOiJub25lIn0.bXNn.', '').toString(), msg)
      })
      it('verifyJws callback', function () {
        Assert.strictEqual(Utils.verifyJws('<KEY>.bXNn.', jose => {
          Assert.deepStrictEqual(jose, {alg: 'none'})
          return ''
        }).toString(), msg)
      })
      it('verifyJws fail if sig present', function () {
        Assert.strictEqual(Utils.verifyJws('<KEY>', ''), null)
      })
      it('verifyJws fail if secret given', function () {
        Assert.strictEqual(Utils.verifyJws('<KEY>.bXNn.', 'secret'), null)
      })
    })

    context('ES256', function () {
      let cred
      before(async () => {
        cred = await Utils.generateKeyPair()
      })

      it('createEcdsaJws with credential', function () {
        const jws = Utils.createEcdsaJws(msg, cred)
        const [h, m, s] = jws.split('.').map(p => Buffer.from(p, 'base64').toString('binary'))
        Assert.strictEqual(h, '{"alg":"ES256"}')
        Assert.strictEqual(m, msg)
        Assert.strictEqual(s.length, 64)
      })
      it('createEcdsaJws+verifyJws', function () {
        const jws = Utils.createEcdsaJws(msg, cred)
        Assert.strictEqual(Utils.verifyJws(jws, cred).toString(), msg)
      })
      it('createEcdsaJws+verifyJws callback', function () {
        const jws = Utils.createEcdsaJws(msg, cred)
        Assert.strictEqual(Utils.verifyJws(jws, jose => {
          Assert.deepStrictEqual(jose, {alg: 'ES256'})
          return cred.pubKeyHex
        }).toString(), msg)
      })
      it('createEcdsaJws+verifyJws with JWK', function () {
        const jwk = Utils.hexToJwk(cred.pubKeyHex)
        const jws = Utils.createEcdsaJws(msg, cred)
        Assert.strictEqual(Utils.verifyJws(jws, jwk).toString(), msg)
      })
      it('createEcdsaJws+verifyJws JWT', function () {
        const payload = {iat: 1234, msg}
        const jws = Utils.createEcdsaJws(payload, cred, {typ: 'JWT'})
        Assert.deepEqual(Utils.verifyJws(jws, cred), payload)
      })
      it('createEcdsaJws+verifyJws pubKeyHex', function () {
        const jws = Utils.createEcdsaJws(msg, cred)
        Assert.strictEqual(Utils.verifyJws(jws, cred.pubKeyHex).toString(), msg)
      })
    })

    context('HS256', function () {
      const JWS = 'eyJhb<KEY>'
      const secret = 'secret'

      it('createHmacJws with secret', function () {
        const jws = Utils.createHmacJws(msg, secret)
        Assert.strictEqual(jws, JWS)
        const [h, m, s] = jws.split('.').map(p => Buffer.from(p, 'base64').toString('binary'))
        Assert.strictEqual(h, '{"alg":"HS256"}')
        Assert.strictEqual(m, msg)
        Assert.strictEqual(s.length, 32)
      })
      it('verifyJws', function () {
        Assert.strictEqual(Utils.verifyJws(JWS, secret).toString(), msg)
      })
      it('verifyJws callback', function () {
        Assert.strictEqual(Utils.verifyJws(JWS, jose => {
          Assert.deepStrictEqual(jose, {alg: 'HS256'})
          return secret
        }).toString(), msg)
      })
      it('verifyJws with wrong secret', function () {
        Assert.strictEqual(Utils.verifyJws(JWS, 'wrong secret'), null)
      })
      it('createHmacJws+verifyJws JWT', function () {
        const payload = {iat: 1234, msg}
        const jws = Utils.createHmacJws(payload, secret, {typ: 'JWT'})
        Assert.deepStrictEqual(Utils.verifyJws(jws, secret), payload)
      })
    })
  })

  context('parseHexString', function () {
    it('parses hex into string', function () {
      Assert.strictEqual(Utils.parseHexString('00'), '\0')
    })
  })

  context('parseX509Date', function () {
    it('parses string into date', function () {
      Assert.ok(Utils.parseX509Date('141213110908Z') instanceof Date)
    })
    it('parses short string into date', function () {
      Assert.strictEqual(Utils.parseX509Date('141213110908Z').toUTCString(), 'Sat, 13 Dec 2014 11:09:08 GMT')
    })
    it('parses long string into date', function () {
      Assert.strictEqual(Utils.parseX509Date('19141213110908Z').toUTCString(), 'Sun, 13 Dec 1914 11:09:08 GMT')
    })
  })

  context('dateToString', function () {
    it('turns current Date into string', function () {
      Assert.strictEqual(typeof Utils.dateToString(new Date()), 'string')
    })
    it('turns past Date into short string', function () {
      Assert.strictEqual(Utils.dateToString(new Date(Date.UTC(1970, 0, 1, 0, 2, 3))), '700101000203Z')
    })
    it('turns future Date into long string', function () {
      Assert.strictEqual(Utils.dateToString(new Date(Date.UTC(2080, 0, 1, 0, 0, 0))), '20800101000000Z')
    })
  })

  it('has path to tkroot', function () {
    const tkroot = Utils.getRootPemPath()
    Assert.ok(/^\/.+\/tkroot.pem$/.test(tkroot), 'Not an absolute path to tkroot.pem')
  })

  context('getJwkThumbprint', function () {
    it('getJwkThumbprint(jwkEC)', function () {
      const thumbprint = Utils.getJwkThumbprint(jwkEC)
      Assert.strictEqual(thumbprint, 't0SeLs6tHEgrSplyGDH-Sb600WfOgDWFbxfJsWnFlVQ')
    })

    it('getJwkThumbprint(jwkRSA)', function () {
      const thumbprint = Utils.getJwkThumbprint(jwkRSA)
      Assert.strictEqual(thumbprint, 'E74oqrEZWBKPrFTjsNLeIUtnVKMm2EW5ZhpNEaV-4RY')
    })

    it('ignore optional members (jwkEC)', function () {
      const thumbprint = Utils.getJwkThumbprint({...jwkEC, opt: 123})
      Assert.strictEqual(thumbprint, 't0SeLs6tHEgrSplyGDH-Sb600WfOgDWFbxfJsWnFlVQ')
    })

    it('ignore optional members (jwkRSA)', function () {
      const thumbprint = Utils.getJwkThumbprint({...jwkRSA, opt: 123})
      Assert.strictEqual(thumbprint, 'E74oqrEZWBKPrFTjsNLeIUtnVKMm2EW5ZhpNEaV-4RY')
    })

    it('key order doesn\'t matter', function () {
      const jwkEC2 = Object.entries(jwkEC).reduceRight((p, [k, v]) => ({...p, [k]: v}), {})
      const thumbprint = Utils.getJwkThumbprint(jwkEC2)
      Assert.strictEqual(thumbprint, 't0SeLs6tHEgrSplyGDH-Sb600WfOgDWFbxfJsWnFlVQ')
    })
  })

  const CommonNamePEM = `-----BEGIN CERTIFICATE-----
<KEY>qgAwIB<KEY>
-----END CERTIFICATE-----`

  const IssuerPEM = `-----BEGIN CERTIFICATE-----
<KEY>4zGvBKre32hOAIEluECNSQzwKdjibKzQ0FrLFj3UBeE8
bPCx2Tty0FQNeijKZWRnH7H9dwqFS1eDYm2DRyAt/FDYQPdFz/hRtVo=
-----END CERTIFICATE-----`

  context('parsePem', function () {
    it('parse EC PEM and attributes', function () {
      const parsed = Utils.parsePem(CommonNamePEM)
      Assert.deepStrictEqual(parsed, {
        'subjectaddress': '0xc0a4afdef2b560e61576117d4c8e6b38cdf68467',
        'serialNo': '0x5e2ae12b8c140093e22cc39a2a8366ecb3f2809d',
        'notAfter': new Date('2019-08-15T05:23:19.000Z'),
        'notBefore': new Date('2018-08-15T05:23:19.000Z'),
        'issuer': '/C=US/ST=Seattle/L=Seattle/O=Trusted Key test/OU=IT/CN=trustedkey.com',
        'issuerPem': null,
        'attributes': [{
          'oid': '1.3.6.1.4.1.53318295.1',
          'value': '<EMAIL>'
        },
        {
          'oid': '1.3.6.1.4.1.53318295.6',
          'value': 'Email'
        },
        {
          'oid': '1.2.840.113549.1.9.1',
          'value': '<EMAIL>'
        }],
        'caissuer': [],
        'crl': ['ethereum:0x48624beaad14ea386e21<KEY>'],
        'ocsp': ['http://ocsp.trustedkey.com']
      })
    })

    it('parse RSA PEM', function () {
      Assert.doesNotThrow(() => Utils.parsePem(IssuerPEM))
    })

    it('parse PEM without header', function () {
      Assert.doesNotThrow(() => Utils.parsePem(CommonNamePEM.replace(/-----[^-]+-----|\r|\n/g, '')))
    })

    it('parse PEM and fail without CA certs', function () {
      Assert.throws(() => Utils.parsePem(CommonNamePEM, []), /Signature verification failed$/)
    })

    it('parse PEM and fail with unknown issuer', function () {
      Assert.throws(() => Utils.parsePem(CommonNamePEM, [CommonNamePEM]), /Signature verification failed$/)
    })

    it('parse PEM and succeed with valid signature', function () {
      Assert.doesNotThrow(() => Utils.parsePem(CommonNamePEM, [IssuerPEM]))
    })

    it('parse PEM and succeed with multiple issuers', function () {
      Assert.strictEqual(Utils.parsePem(CommonNamePEM, [CommonNamePEM, IssuerPEM]).issuerPem, IssuerPEM)
    })

    it('parse PEM and fail with bogus issuer', function () {
      Assert.throws(() => Utils.parsePem(CommonNamePEM, ['bogus']), /Invalid PEM/)
    })

    it('parse PEM and fail with bogus cert', function () {
      Assert.throws(() => Utils.parsePem('bogus', [IssuerPEM]), /Invalid PEM/)
    })

    it('parse issuer PEM', function () {
      const parsed = Utils.parsePem(IssuerPEM)
      Assert.deepStrictEqual(parsed, {
        'attributes': [
          {
            'oid': '2.5.4.6',
            'value': 'US'
          },
          {
            'oid': '2.5.4.8',
            'value': 'Seattle'
          },
          {
            'oid': '2.5.4.7',
            'value': 'Seattle'
          },
          {
            'oid': '172.16.31.10',
            'value': 'Trusted Key test'
          },
          {
            'oid': '192.168.3.11',
            'value': 'IT'
          },
          {
            'oid': '2.5.4.3',
            'value': 'trustedkey.com'
          }
        ],
        'issuer': '/C=US/ST=Seattle/L=Seattle/O=Trusted Key test/OU=IT/CN=trustedkey.com',
        'issuerPem': null,
        'notAfter': new Date('2018-08-15T23:06:43.000Z'),
        'notBefore': new Date('2018-08-14T23:06:43.000Z'),
        'serialNo': '0x000000000000000000000000e973395209c6932b',
        'subjectaddress': undefined,
        'caissuer': [],
        'crl': [],
        'ocsp': []
      })
    })

    it('parse tkroot', function () {
      const tkroot = FS.readFileSync(Utils.getRootPemPath(), 'ascii')
      Assert.doesNotThrow(() => Utils.parsePem(tkroot, [tkroot]))
    })

    it('validate self-signed PEM', function () {
      Assert.doesNotThrow(() => Utils.parsePem(IssuerPEM, [IssuerPEM]))
    })
  })

  context('wait*', function () {
    it('wait', async function () {
      const now = new Date()
      await Utils.wait(50)
      Assert.ok(new Date().getTime() - now >= 50)
    })

    it('waitUntil immediate', async function () {
      const now = new Date()
      Assert.strictEqual(await Utils.waitUntil(500, () => 2), 2)
      Assert.ok(new Date().getTime() - now < 500)
    })

    it('waitUntil timeout', async function () {
      const now = new Date()
      Assert.strictEqual(await Utils.waitUntil(50, () => false), false)
      Assert.ok(new Date().getTime() - now >= 50)
    })
  })

  context('validateClaim', function () {
    let claim

    before(function () {
      claim = Utils.parsePem(CommonNamePEM, [IssuerPEM])
    })

    it('validates', () => {
      Assert.strictEqual(Utils.validateClaim(claim, new Date('2018-08-15T15:23:19.000Z')), true)
    })

    it('validates notBefore', () => {
      Assert.strictEqual(Utils.validateClaim(claim, new Date('2018-08-15T04:23:19.000Z')), false)
    })

    it('validates notAfter', () => {
      Assert.strictEqual(Utils.validateClaim(claim, new Date('2019-08-16T05:23:19.000Z')), false)
    })

    it('validates issuer notBefore', () => {
      Assert.strictEqual(Utils.validateClaim(claim, new Date('2018-08-14T05:23:19.000Z')), false)
    })

    it('validates issuer notAfter', () => {
      Assert.strictEqual(Utils.validateClaim(claim, new Date('2018-08-16T05:23:19.000Z')), false)
    })
  })

  context('verifyChain', function () {
    const OtherIssuer = `-----BEGIN CERTIFICATE-----
MIIDKTCCApKgAwIBAgIJAJD3lnm9KEylMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNV
BAYTAlVTMRAwDgYDVQQIDAdTZWF0dGxlMRAwDgYDVQQHDAdTZWF0dGxlMRkwFwYD
VQQKDBBUcnVzdGVkIEtleSB0ZXN0MQswCQYDVQQL<KEY>BUGA1UEAwwOdHJ1
c<KEY>sZTEZ
MBcGA1UE<KEY>
-----END CERTIFICATE-----`

    it('verify chain with self signed (Issuer)', function () {
      Assert.strictEqual(Utils.verifyChain([IssuerPEM]), true)
    })

    it('verify chain with self signed (OtherIssuer)', function () {
      Assert.strictEqual(Utils.verifyChain([OtherIssuer]), true)
    })

    it('verify chain should fail without self signed cert', function () {
      Assert.strictEqual(Utils.verifyChain([CommonNamePEM]), false)
    })

    it('verify chain should fail with invalid chain', function () {
      Assert.strictEqual(Utils.verifyChain([CommonNamePEM, OtherIssuer]), false)
    })

    it('verify chain should fail with incorrect order', function () {
      Assert.strictEqual(Utils.verifyChain([IssuerPEM, CommonNamePEM]), false)
    })

    it('verify chain with correct issuer', function () {
      Assert.strictEqual(Utils.verifyChain([CommonNamePEM, IssuerPEM]), true)
    })

    it('verify TK root with TK root', function () {
      const RootPem = FS.readFileSync(Utils.getRootPemPath(), 'ascii')
      Assert.strictEqual(Utils.verifyChain([RootPem]), true)
    })
  })
})
<file_sep>const HttpUtils = require('./http')
const Crypto = require('crypto')
const Assert = require('assert')

module.exports = CredentialRegistryService

/**
 * Utility class with wrappers for the various Credential Registry API endpoints.
 *
 * @constructor
 * @param {String} backendUrl The base backend URL
 * @param {String} [appId] Application ID, without this only unauthorized APIs can be used
 * @param {String} [appSecret] Application shared secret, without this only unauthorized APIs can be used
 */
function CredentialRegistryService (backendUrl, appId, appSecret) {
  this.httpClient = new HttpUtils(backendUrl, appId, appSecret)
}

/**
 * Revoke our default credential by sending a request to the blockchain. The receiver must have been registered as
 * a delegate in the smart contract. A challenge is signed by the default registered credential, which is
 * verified by the smart contract.
 *
 * @param {string} delegateAddressString The hex-encoded blockchain address of the registered delegate credential.
 * @param {Object} keyPair Jsrsasign keypair object
 * @param {string} [address] When revoking a claim, its address
 * @returns {Promise.<object>} returning JSON from API
 */
CredentialRegistryService.prototype.revokeCredential = function (delegateAddressString, keyPair, address) {
  Assert.strictEqual(typeof delegateAddressString, 'string', 'delegateAddressString must be of type `string`')
  Assert.strictEqual(typeof keyPair, 'object', 'keyPair must be of type `object`')
  Assert.strictEqual(0 in keyPair, false, 'keyPair should not be Buffer or Array-like')

  const addressWithout0x = delegateAddressString.replace('0x', '')
  const hash = Crypto.createHash('sha256')
  const digest = hash.update(addressWithout0x, 'hex').update(address ? 'indirect' : 'revocation').digest('hex')
  const sig = keyPair.signWithMessageHash(digest)

  return this.httpClient.post('revoke', {
    signature: sig,
    pubkey: keyPair.pubKeyHex,
    address: address
  })
}

/**
 * Revoke claim by sending a request to the blockchain.  The receiver must have been registered as
 * a delegate in the smart contract.
 *
 * @param {string} address The address is its serial number
 * @returns {Promise.<object>} return JSON from API
 */
CredentialRegistryService.prototype.revokeClaim = function (address) {
  Assert.strictEqual(typeof address, 'string', 'address must be of type `string`')

  return this.httpClient.post('revokeClaim', {
    address: address
  })
}
<file_sep>/* eslint-env mocha */
const Assert = require('assert')
const OID = require('../oid')
const Claims = require('../claims')

describe('claims', function () {
  it('has all known OIDs', function () {
    const allOidClaims = new Set(Object.values(Claims))
    const missingOids = Object.values(OID).filter(oid => !allOidClaims.has(oid))
    Assert.deepStrictEqual(missingOids, [])
  })
})
<file_sep>//
//  utils.js
//
//  Copyright © 2018 Trusted Key Solutions. All rights reserved.
//

const Crypto = require('crypto')
const CryptoJS = require('crypto-js')
const Assert = require('assert')
const Jsrsasign = require('jsrsasign')
const URL = require('url')
const Moment = require('moment')
const JWT = require('jsonwebtoken')

const CrytoAlg = new Map([
  ['SHA256withRSA', 'RSA-SHA256'],
  ['SHA256withECDSA', 'sha256'],
  ['SHA1withRSA', 'RSA-SHA1']
])
/**
 * Static Trustedkey utility functions
 *
 * @exports utils
*/
const utils = module.exports

/**
 * Add new query parameters to an existing URL.
 * @param {String} path the current url (may be relative)
 * @param {?Object} [params] object with new query parameters
 * @returns {String} new URL with the query parameters merged
 */
utils.mergeQueryParams = function (path, params) {
  Assert.strictEqual(typeof path, 'string', 'path must be of type `string`')
  Assert.strictEqual(typeof params, 'object', 'params must be of type `object`')

  const url = URL.parse(path, true)
  Object.keys(params || {})
    .filter(key => params[key] !== undefined)
    .forEach(key => { url.query[key] = params[key] })
  delete url.search // force recreation from .query
  return url.format()
}

/**
 * Get the SHA256 of the specified blob
 *
 * @param {String|Buffer} blob String or Buffer
 * @param {String} [encoding] Optional encoding for the final digest
 * @returns {Buffer|String} Buffer or string with SHA256
 */
utils.sha256 = function (blob, encoding) {
  const hash = Crypto.createHash('sha256')
  return hash.update(blob).digest(encoding)
}

/**
 * Get the SHA3/KECCAK256 of the specified blob
 *
 * @param {String|Buffer} blob String or Buffer
 * @param {String} [encoding] Optional encoding for the final digest
 * @returns {Buffer|String} Buffer or string with SHA3/KECCAK256
 */
utils.keccak256 = function (blob, encoding) {
  Assert.ok(encoding == null || encoding === 'hex' || encoding === 'base64', 'encoding should be `hex` or undefined')

  if (blob instanceof Buffer) {
    blob = CryptoJS.enc.Base64.parse(blob.toString('base64'))
  }

  const digest = CryptoJS.SHA3(blob, {outputLength: 256})
  if (encoding === 'hex') {
    return digest.toString(CryptoJS.enc.Hex)
  } else if (encoding === 'base64') {
    return digest.toString(CryptoJS.enc.Base64)
  } else {
    return Buffer.from(digest.toString(CryptoJS.enc.Base64), 'base64')
  }
}

/**
 * Convert a certificate serial number to blockchain address
 *
 * @param {String} serialhex Hex encoded serial number
 * @returns {String} 0x prefixed address
 */
utils.serialToAddress = function (serialhex) {
  Assert.strictEqual(typeof serialhex, 'string', 'serialhex must be of type `string`')
  return '0x' + serialhex.replace('0x', '').padStart(40, '0')
}

/**
 * Base64 encode URL string
 *
 * @param {String|Uint8Array|Array} data data to encode
 * @param {String} [encoding] Optional encoding of data
 * @returns {String} base64-encoded URL
 */
utils.base64url = function (data, encoding) {
  return Buffer.from(data, encoding).toString('base64').replace(/=/g, '').replace(/\//g, '_').replace(/\+/g, '-')
}

/**
 * Get UTC seconds since UNIX epoch or convert date into unix time
 *
 * @param {Date} [date] Optional date object
 * @returns {Number} Unix timestamp
 */
utils.getUnixTime = function (date) {
  return Math.floor((date || new Date()).getTime() / 1000)
}

/**
 * Create a JSON Web Signature
 *
 * @deprecated
 * @param {object|String|Buffer} message Message can be string or object. Objects will be JSON stringified
 * @param {String} secret HMAC shared secret
 * @param {object} [header={alg: "HS256"}] JOSE header OPTIONAL
 * @returns {String} Signed JWS
 */
utils.createHmacJws = function (message, secret, header = {}) {
  Assert.strictEqual(typeof secret, 'string', 'secret must be of type `string`')
  Assert.strictEqual(typeof header, 'object', 'header must be of type `object`')

  const options = {header, algorithm: 'HS256'}
  return JWT.sign(message, secret, options)
}

/**
 * Create a JSON Web Signature
 *
 * @deprecated
 * @param {object|String|Buffer} message Message can be string or object. Objects will be JSON stringified
 * @param {object} credential key pair
 * @param {object} [header={alg: "ES256"}] JOSE header OPTIONAL
 * @returns {String} Signed JWS
 */
utils.createEcdsaJws = function (message, credential, header = {}) {
  Assert.strictEqual(typeof credential, 'object', 'credential must be of type `object`')
  Assert.strictEqual(typeof header, 'object', 'header must be of type `object`')

  const jwk = utils.hexToJwk(credential.pubKeyHex)
  // Add the private key to the public key JWK
  jwk.d = utils.base64url(credential.prvKeyHex, 'hex')
  const pem = utils.jwkToPem(jwk)
  const options = {header, algorithm: 'ES256'}
  return JWT.sign(message, pem, options)
}

/**
 * Verify a JSON Web Signature.
 *
 * @deprecated
 * @param {String} jws the JWS or JWT as string
 * @param {String|function|Object} secretOrCallback Shared secret or public key
 * @returns {Object|String|null} the parsed claims or `null` on failure
 */
utils.verifyJws = function (jws, secretOrCallback) {
  Assert.strictEqual(typeof jws, 'string', 'jws must be of type `string`')

  // Convert the given secret into a secret or public key for jsonwebtoken,
  if (typeof secretOrCallback === 'function') {
    // JWT.verify will no longer return a value if we use the callback overload
    const decoded = JWT.decode(jws, {complete: true})
    // Removed promise support since it was never actually used anywhere
    secretOrCallback = secretOrCallback(decoded.header)
  }
  if (typeof secretOrCallback === 'object' && secretOrCallback.pubKeyHex) {
    // Convert jsrsasign credential to hex public key
    secretOrCallback = secretOrCallback.pubKeyHex
  }
  if (typeof secretOrCallback === 'string' && secretOrCallback.length === 130) {
    // Convert uncompressed hex public key to JWK
    secretOrCallback = utils.hexToJwk(secretOrCallback)
  }
  if (typeof secretOrCallback === 'object' && secretOrCallback.kty) {
    // Convert JWK to PEM public key
    secretOrCallback = utils.jwkToPem(secretOrCallback)
  }

  try {
    return JWT.verify(jws, secretOrCallback)
  } catch (err) {
    // Keep the old behavior of returning null instead of throwing
    if (err.name === 'JsonWebTokenError') {
      return null
    } else {
      throw err
    }
  }
}

/**
 * @typedef JwkEC
 * @type {object}
 * @property {'EC'} kty Key Type
 * @property {'P-256'} crv Elliptic Curve
 * @property {string} x Base64-URL encoded X coordinate
 * @property {string} y Base64-URL encoded Y coordinate
 * @property {string} [d] Base64-URL encoded private key
 *
 * @typedef JwkRSA
 * @type {object}
 * @property {'RSA'} kty Key Type
 * @property {string} e Base64-URL encoded exponent
 * @property {string} n Base64-URL encoded modulus
 */

/**
 * Convert a JWK into a hex public key
 * @param {JwkEC} jwk JSON Web Key for public EC key
 * @return {String} string with hex public key
 */
utils.jwkToHex = function (jwk) {
  Assert.strictEqual(jwk.d, undefined, 'jwk.d must be of type `undefined`')
  if (jwk.kty === 'EC' && jwk.crv === 'P-256') {
    // Convert x,y coordinates from JWK to hex encoded public key
    const hex = '04' + Buffer.concat([Buffer.from(jwk.x, 'base64'), Buffer.from(jwk.y, 'base64')]).toString('hex')
    Assert.strictEqual(hex.length, 130)
    return hex
  } else if (jwk.kty === 'RSA' && jwk.e === 'AQAB') {
    return Buffer.from(jwk.n, 'base64').toString('hex')
  }
  throw Error('Unsupported JWK:' + jwk)
}

/**
 * Convert a hex key into JWK
 * @param {String} keyHex hex encoded ECC key
 * @param {String} [crv] curve identifier ("P-256" by default)
 * @return {JwkEC} JSON Web Key
 */
utils.hexToJwk = function (keyHex, crv = 'P-256') {
  Assert.strictEqual(typeof keyHex, 'string', 'pubKeyHex must be of type `string`')

  const buffer = Buffer.from(keyHex, 'hex')
  if (buffer.length === 65) {
    Assert.ok(keyHex.startsWith('04'), 'pubKeyHex must be an uncompressed ECC public key')
    return {
      crv,
      'kty': 'EC',
      'x': utils.base64url(buffer.slice(1, 33)),
      'y': utils.base64url(buffer.slice(33))
    }
  } else {
    throw Error('Unsupported key: ' + keyHex)
  }
}

function createDerChunk (tag, ...nested) {
  let header
  const size = nested.reduce((p, b) => p + b.length, 0)
  if (size < 0x80) {
    header = Buffer.alloc(2)
    header.writeUInt8(size, 1)
  } else if (size < 0x100) {
    header = Buffer.alloc(3)
    header.writeUInt8(0x81, 1)
    header.writeUInt8(size, 2)
  } else {
    Assert.ok(size <= 0xffff, 'Invalid PEM size: ' + size)
    header = Buffer.alloc(4)
    header.writeUInt8(0x82, 1)
    header.writeUInt16BE(size, 2)
  }
  header.writeUInt8(tag)
  return Buffer.concat([header, ...nested])
}

function readDerChunk (buffer, offset = 0) {
  offset++
  let size = buffer.readUInt8(offset++)
  if (size === 0x81) {
    size = buffer.readUInt8(offset++)
    Assert.ok(size >= 0x80, 'Invalid PEM size: ' + size)
  } else if (size === 0x82) {
    size = buffer.readUInt16BE(offset)
    Assert.ok(size >= 0x100, 'Invalid PEM size: ' + size)
    offset += 2
  } else {
    Assert.ok(size < 0x80, 'Invalid PEM size: ' + size)
  }
  return buffer.slice(offset, offset + size)
}

/**
 * Convert a PEM encoded public key to JWK, with minimal checking.
 * @param {string} pem Public key in PEM format
 * @returns {JwkRSA|JwkEC} JSON Web Key
 */
utils.pemToJwk = function (pem) {
  const base64 = pem.match(/^-----BEGIN [A-V ]{6,10} KEY-----([^-]+)-----END [A-V ]{6,10} KEY-----/)
  if (!base64) {
    throw Error('Unsupported PEM:' + pem)
  }
  const der = Buffer.from(base64[1], 'base64')
  return utils.derToJwk(der)
}

/**
 * Convert a DER encoded public key to JWK, with minimal checking.
 * @param {Buffer} der Public key in DER format
 * @returns {JwkRSA|JwkEC} JSON Web Key
 */
utils.derToJwk = function (der) {
  const main = readDerChunk(der)
  const type = readDerChunk(main)
  const key = readDerChunk(main, type.length + 2)
  switch (type.toString('hex')) {
    case '06092a864886f70d0101010500': {
      // RSA public
      const rsa = readDerChunk(key, 1)
      const n = readDerChunk(rsa).slice(1)
      const lenSize = n.length >= 0x100 ? 3 : (n.length >= 0x80 ? 2 : 1)
      const e = readDerChunk(rsa, n.length + lenSize + 2)
      return {
        'kty': 'RSA',
        'e': utils.base64url(e),
        'n': utils.base64url(n)
      }
    }
    case '06072a8648ce3d020106082a8648ce3d030107': {
      // EC public
      Assert.strictEqual(key.length, 66)
      const x = key.slice(2, 34)
      const y = key.slice(34, 66)
      return {
        'crv': 'P-256',
        'kty': 'EC',
        'x': utils.base64url(x),
        'y': utils.base64url(y)
      }
    }
    case '00': {
      // EC private (nested)
      const nested = readDerChunk(main, 5 + key.length)
      return utils.derToJwk(nested)
    }
    case '01': {
      // EC private
      let skip = 5 + key.length
      while (main[skip] !== 0xa1) {
        skip += 2 + readDerChunk(main, skip).length
      }
      const pub = readDerChunk(main, 2 + skip)
      Assert.strictEqual(pub.length, 66)
      const x = pub.slice(2, 34)
      const y = pub.slice(34, 66)
      return {
        'crv': 'P-256',
        'kty': 'EC',
        'd': utils.base64url(key, 'hex'),
        'x': utils.base64url(x),
        'y': utils.base64url(y)
      }
    }
    default:
      throw Error('Unsupported PEM type:' + type.toString('hex'))
  }
}

function wrap (str) {
  return str.replace(/(.{64})/g, '$1\n')
}

/**
 * Convert a JWK into a hex public key
 * @param {JwkEC|JwkRSA} jwk JSON Web Key for public EC key
 * @return {String} string with PEM public key
 */
utils.jwkToPem = function (jwk) {
  if (jwk.kty === 'EC' && jwk.crv === 'P-256') {
    // Convert x,y coordinates from JWK to public key
    const xy = Buffer.concat([Buffer.from(jwk.x, 'base64'), Buffer.from(jwk.y, 'base64')])
    Assert.strictEqual(xy.length, 64)
    if (jwk.d) {
      // Add the private key as well
      const header = Buffer.from('30770201010420', 'hex')
      const pk = Buffer.from(jwk.d, 'base64')
      const xyhdr = Buffer.from('a00a06082a8648ce3d030107a14403420004', 'hex')
      const all = Buffer.concat([header, pk, xyhdr, xy])
      return wrap(`-----BEGIN EC PRIVATE KEY-----
${all.toString('base64')}
-----END EC PRIVATE KEY-----`)
    } else {
      // Basic template for the PEM; we'll overwrite the coordinates in-place
      return wrap(`-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE${xy.toString('base64')}
-----END PUBLIC KEY-----`)
    }
  } else if (jwk.kty === 'RSA') {
    // Convert public key from JWK to base64 encoded public key
    const pub = Buffer.from(jwk.n, 'base64')
    const exp = Buffer.from(jwk.e, 'base64')
    const pubDer = createDerChunk(0x02, Buffer.alloc(1), pub)
    const expDer = createDerChunk(0x02, exp)
    const key = createDerChunk(0x30, pubDer, expDer)
    const pkey = createDerChunk(0x03, Buffer.alloc(1), key)
    const der = createDerChunk(0x30, Buffer.from('300d06092a864886f70d0101010500', 'hex'), pkey)
    return wrap(`-----BEGIN PUBLIC KEY-----
${der.toString('base64')}
-----END PUBLIC KEY-----`)
  }
  throw Error('Unsupported JWK:' + jwk)
}

/**
 * Verify an ECDSA named curve signed message
 *
 * @param {String} curveName Curve name (secp256r1)
 * @param {String} message Message payload
 * @param {String|Object} pubkey Public key to check signature against (hex)
 * @param {Buffer|String} signature Signature payload (hex)
 * @return {boolean} Indicate whether signature is correct
 */
utils.checkECDSA = function (curveName, message, pubkey, signature) {
  Assert.strictEqual(typeof curveName, 'string', 'curveName must be of type `string`')

  // Verify a digest value
  const digest = utils.sha256(message, 'hex')

  if (pubkey.kty) {
    pubkey = utils.jwkToHex(pubkey)
  } else if (pubkey.pubKeyHex) {
    pubkey = pubkey.pubKeyHex
  }
  if (signature instanceof Buffer) {
    signature = signature.toString('hex')
  }

  // Convert r||s signatures to proper DER
  if (signature.length === 128 && !/^303e02/i.test(signature)) {
    const r = Buffer.from(signature.substr(0, 64), 'hex')
    const s = Buffer.from(signature.substr(64), 'hex')
    signature = createDerChunk(0x30, createDerChunk(0x02, r), createDerChunk(0x02, s)).toString('hex')
  }

  const curve = new Jsrsasign.KJUR.crypto.ECDSA({xy: pubkey, curve: curveName})
  return curve.verifyHex(digest, signature, pubkey)
}

/**
 * Convert a user public key to blockchain address
 *
 * @param {String} pubkeyhex User ECC public key (hex encoded)
 * @returns {String} User address with leading 0x
 */
utils.userPubKeyHexToAddress = function (pubkeyhex) {
  Assert.strictEqual(typeof pubkeyhex, 'string', 'pubkeyhex must be of type `string`')
  Assert.ok(pubkeyhex.startsWith('04'), 'pubkeyhex must be an uncompressed ECC public key')
  Assert.strictEqual(pubkeyhex.length, 130)

  // Get the uncompressed public key without prefix, take the sha256 hash, and skip the first 12 bytes
  const blob = Buffer.from(pubkeyhex.substr(2), 'hex')
  const digest = utils.sha256(blob, 'hex')
  return '0x' + digest.substr(2 * 12)
}

/**
 * Wrap the call and change the callback into a promise resolve or reject.
 * @param {function} call A function that takes a callback as last parameter
 * @returns {function} Wrapper function that returns a promise
 */
utils.promisify = function (call) {
  return function () {
    // Save the 'this' reference for use inside the promise
    const self = this
    const args = Array.prototype.slice.call(arguments)
    return new Promise((resolve, reject) => {
      // Append the callback that either rejects or resolves the promise
      args.push((err, a, b, c, d) => err ? reject(err) : resolve(a, b, c, d))
      call.apply(self, args)
    })
  }
}

/**
 * Generate a 32-byte random nonce.
 * @param {String} [encoding] Encoding for result (default base64)
 * @param {Number} [length] Number of bytes for the result (default 32)
 * @returns {String} The encoding of the nonce
 */
utils.generateNonce = function (encoding, length) {
  return Crypto.randomBytes(length || 32).toString(encoding || 'base64')
}

/**
 * Wait for specified number of milliseconds (ms).
 * @param {Number} durationMS Number of milliseconds to wait.
 * @return {Promise} Promise that resolves in due time.
 */
utils.wait = function (durationMS) {
  return new Promise((resolve, reject) => {
    return setTimeout(resolve, durationMS)
  })
}

/**
 * Wait until the callback returns a truthy value (or timeout).
 * @param {Number} ms Number of milliseconds to wait.
 * @param {function} callback Callback to invoke (once a second).
 * @param {Number} [step] Number of milliseconds to wait per try.
 * @return {Promise} Promise that resolves when the callback is truthy
 */
utils.waitUntil = function (ms, callback, step = 1000) {
  Assert.strictEqual(typeof ms, 'number', 'ms must be of type `number`')
  Assert.strictEqual(typeof callback, 'function', 'callback must be of type `function`')
  Assert.ok(step > 0, 'step must be of type `number` > 0')
  return Promise.resolve()
    .then(callback)
    .then(done => {
      if (done || ms <= 0) {
        return done
      }
      return utils.wait(Math.min(step, ms))
        .then(() => utils.waitUntil(ms - step, callback, step))
    })
}

/**
 * Generate a new key pair.
 * @param {string|number} [curveOrLength] The name of the EC curve or RSA modulus length (optional)
 * @return {Promise.<Object>} New jsrsasign key object of given curve
 */
utils.generateKeyPair = async function (curveOrLength) {
  let pair
  try {
    const {promisify} = require('util')
    const keyOptions = Object.assign(curveOrLength > 0 ? { modulusLength: curveOrLength } : { namedCurve: curveOrLength || 'prime256v1' },
      {
        publicKeyEncoding: {
          type: 'spki',
          format: 'pem'
        },
        privateKeyEncoding: {
          type: 'pkcs8',
          format: 'pem'
        }
      })
    const {privateKey} = await promisify(Crypto.generateKeyPair)(
      curveOrLength > 0 ? 'rsa' : 'ec',
      keyOptions
    )
    pair = Jsrsasign.KEYUTIL.getKey(privateKey)
  } catch (err) {
    pair = Jsrsasign.KEYUTIL.generateKeypair(curveOrLength > 0 ? 'RSA' : 'EC', curveOrLength || 'secp256r1').prvKeyObj
  }

  return {
    prvKeyHex: pair.prvKeyHex,
    pubKeyHex: pair.pubKeyHex
  }
}

/**
 * HMAC-based One-time Password Algorithm
 * @param {string|Buffer} key The shared key for the HMAC.
 * @param {string|Buffer} message The message (64-bit counter or 32-bit time/30)
 * @return {string} six digit HOTP code
 */
utils.oneTimePassword = function (key, message) {
  const hash = Crypto.createHmac('sha1', key).update(message).digest()
  const offset = hash[hash.length - 1] & 0xf
  // 4 bytes starting at the offset, remove the most significant bit
  const truncatedHash = hash.readInt32BE(offset) & 0x7FFFFFFF // big endian
  const code = truncatedHash % 1000000
  // pad code with 0 until length of code is 6;
  return String('00000' + code).slice(-6)
}

/**
 * Decode a string with HEX-encoded data into a plain binary string.
 * @param {string} hex String with HEX-encoded data
 * @param {string} [encoding='binary'] Optional encoding of the data
 * @returns {string} String with binary encoded data
 */
utils.parseHexString = function (hex, encoding) {
  Assert.strictEqual(typeof hex, 'string', 'hex must be of type `string`')
  return Buffer.from(hex, 'hex').toString(encoding || 'binary')
}

/**
 * Parse ASN.1 YYMMDDHHMMSSZ or YYYYMMDDHHMMSSZ into a Date object.
 * @param {string} date ASN.1 YYMMDDHHMMSSZ or YYYYMMDDHHMMSSZ date string.
 * @returns {?Date} New date object or `null` for invalid dates
 */
utils.parseX509Date = function (date) {
  Assert.strictEqual(typeof date, 'string', 'date must be of type `string`')

  const match = /^([0-9]{2,4})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})([0-9]{2})Z$/.exec(date)
  if (match === null) {
    return null
  }
  // - Where YY is less than 50, the year shall be interpreted as 20YY.
  const year = match[1].length === 2 && match[1] < 50 ? 2000 + parseInt(match[1]) : match[1]
  return new Date(Date.UTC(year, match[2] - 1, match[3], match[4], match[5], match[6]))
}

/**
 * Create ASN.1 YYMMDDHHMMSSZ or YYYYMMDDHHMMSSZ into a string.
 * @param {Date} date Date object.
 * @returns {string} New ASN.1 YYMMDDHHMMSSZ or YYYYMMDDHHMMSSZ date string
 */
utils.dateToString = function (date) {
  if (date.getFullYear() < 1950 || date.getFullYear() > 2049) {
    return Moment(date).utc().format('YYYYMMDDHHmmss[Z]', date)
  } else {
    return Moment(date).utc().format('YYMMDDHHmmss[Z]', date)
  }
}

/**
 * Check whether the given string is a valid blockchain address.
 * @param {string} str The string to check for 0x-prefixed address
 * @returns {boolean} `true` if the string is a valid address; false otherwise.
 */
utils.isAddress = function (str) {
  Assert.strictEqual(typeof str, 'string', 'str must be of type `string`')
  return /^0x[0-9a-fA-F]{40}$/.test(str)
}

/**
 * @param {string} pem PEM string
 * @returns {Jsrsasign.X509} certificate
 */
function readPEM (pem) {
  Assert.strictEqual(typeof pem, 'string', 'pem must be of type `string`')
  // Ignore PEM headers (if present)
  const base64 = pem.replace(/^-----(BEGIN|END) CERTIFICATE-----/g, '')
  // Load the certificate from PEM string
  const cert = new Jsrsasign.X509()
  cert.readCertHex(Buffer.from(base64, 'base64').toString('hex'))
  return cert
}

/**
 * @param {Jsrsasign.X509} cert certificate
 * @param {Jsrsasign.X509} caCert CA certificate
 * @returns {boolean} success or failure
 */
function verifySignature (cert, caCert) {
  try {
    const algName = cert.getSignatureAlgorithmName()
    const cryptoAlg = CrytoAlg.get(algName)
    if (cryptoAlg == null) {
      throw Error(`Unsupported signature algorithm: ${algName}`)
    }
    const hSigVal = Buffer.from(cert.getSignatureValueHex(), 'hex')
    const tbs = Buffer.from(Jsrsasign.ASN1HEX.getTLVbyList(cert.hex, 0, [0], '30'), 'hex')
    const verify = Crypto.createVerify(cryptoAlg)
    verify.update(tbs)
    const caPem = Jsrsasign.KEYUTIL.getPEM(caCert.getPublicKey())
    return verify.verify(caPem, hSigVal)
  } catch (err) {
    throw Error(`Invalid PEM: ${err}`)
  }
}

/**
 * @typedef Attribute
 * @type {object}
 * @property {string} oid The OID path for this attribute
 * @property {string} value The value of this attribute
 *
 * @typedef Claim
 * @type {object}
 * @property {string} subjectaddress The public key hash
 * @property {string} serialNo
 * @property {string} issuer The X.500 issuer name
 * @property {?string} issuerPem The issuer X.509 PEM
 * @property {Array.<string>} ocsp Array of OCSP responders
 * @property {Array.<string>} caissuer Array of issuers
 * @property {Array.<string>} crl Array of CRL distribution URIs
 * @property {Date} notBefore
 * @property {Date} notAfter
 * @property {Array.<Attribute>} attributes The array of attributes of this claim
 **/

/**
 * Parse a PEM encoded X509 certificate.
 * @param {string} pem The X509 certificate in PEM format
 * @param {Array.<string>} [chain] The X509 certificates of the CA chain
 * @returns {Claim} The parsed X509 certificate
 */
utils.parsePem = function (pem, chain) {
  // Load the certificate from PEM string
  const cert = readPEM(pem)

  // Validate certificate chain (issuer whitelist)
  let issuerPem = null
  if (chain) {
    issuerPem = chain.find(caPem => verifySignature(cert, readPEM(caPem)))
    if (issuerPem == null) {
      throw Error('Signature verification failed')
    }
  }

  const serialNo = utils.serialToAddress(cert.getSerialNumberHex())
  const issuer = cert.getIssuerString()
  const ocsp = cert.getExtAIAInfo() || {caissuer: [], ocsp: []}
  const crl = cert.getExtCRLDistributionPointsURI() || []

  const subjectPubkey = cert.getPublicKey().pubKeyHex
  const subjectaddress = subjectPubkey !== undefined ? utils.userPubKeyHexToAddress(subjectPubkey) : undefined

  const notAfter = utils.parseX509Date(cert.getNotAfter())
  const notBefore = utils.parseX509Date(cert.getNotBefore())

  // Extract the token information from the DName
  const attributes = []
  try {
    for (let d = 0; ; d++) {
      const tlv = Jsrsasign.ASN1HEX.getTLVbyList(cert.hex, 0, [0, 5, d])
      const oidHex = Jsrsasign.ASN1HEX.getVbyList(tlv, 0, [0, 0])
      if (oidHex === '') break
      const valueHex = Jsrsasign.ASN1HEX.getVbyList(tlv, 0, [0, 1])
      // CONSIDER: use encoding based on ASN.1 value type
      const value = utils.parseHexString(valueHex, 'utf-8')

      const oid = Jsrsasign.ASN1HEX.hextooidstr(oidHex)
      attributes.push({oid, value})
    }
  } catch (err) {}

  return Object.assign({subjectaddress, serialNo, notBefore, notAfter, attributes, issuer, crl, issuerPem}, ocsp)
}

/**
 * Validate the parsed claim
 * @param {Claim} claim Parsed claim object
 * @param {Date} [date] Optional date to test against
 * @returns {Boolean} success or failure
 */
utils.validateClaim = function (claim, date) {
  Assert.strictEqual(typeof claim, 'object', 'claim must be of type `object`')

  const now = date || new Date()
  if (claim.notAfter < now) {
    return false
  }
  if (claim.notBefore > now) {
    return false
  }
  if (claim.issuerPem) {
    // Rescursively validate the issuer
    const issuerClaim = utils.parsePem(claim.issuerPem)
    return utils.validateClaim(issuerClaim, now)
  }
  return true
}

/**
 * Return the absolute path to the Trusted Key root CA certificate.
 * @returns {string} Absolute path to the Trusted Key root CA certificate
 */
utils.getRootPemPath = function () {
  return require('path').join(__dirname, 'tkroot.pem')
}

/**
 * Get the JWK thumbprint for the given key.
 * @param {JwkEC|JwkRSA} jwk JSON Web Key
 * @returns {string} Base64-URL encoded thumbprint for the JWK
 */
utils.getJwkThumbprint = function (jwk) {
  // Only the required members of a key's representation are used when computing its JWK Thumbprint value.
  const json = JSON.stringify(jwk, ['crv', 'e', 'k', 'kty', 'n', 'x', 'y'])
  return this.base64url(utils.sha256(json))
}

/**
 * Verify x5c X509 cert chain
 * @param {Array.string} chain encoded BASE64 PEMs in x5c
 * @return {boolean} evaluation of chain validation
 */
utils.verifyChain = function (chain) {
  Assert.ok(Array.isArray(chain), 'chain must be instance of `Array`')

  const certs = chain.map(p => readPEM(p))
  const getCAIdx = (i) => (i === certs.length - 1) ? i : i + 1
  for (let i = 0; i < certs.length; i++) {
    if (!verifySignature(certs[i], certs[getCAIdx(i)])) {
      return false
    }
  }

  return true
}
<file_sep>const Utils = require('../utils')
const HttpUtils = require('./http')

module.exports = DocsigService

/**
 * Submit
 *
 * @constructor
 * @param {String} backendUrl The base backend URL
 * @param {String} appId Wallet application ID
 * @param {String} appSecret Wallet shared secret
 *
 * @param {String} docsigAppId Docsig application ID
 * @param {String} docsigAppSecret Docsig shared secret
 */
function DocsigService (backendUrl, appId, appSecret, docsigAppId, docsigAppSecret) {
  this.httpClient = new HttpUtils(backendUrl, appId, appSecret)
  this._docsigAppId = docsigAppId
  this._docsigAppSecret = docsigAppSecret
}

/**
 * Submit a document to be signed
 *
 * @param {String} signatoryEmail Signatory email address
 * @param {String} callbackUrl Url where signed PDF will be uploaded with PUT
 * @param {String} documentUrl Url where document can be downloaded
 * @param {Array.<Dotted>} objectIds Array of objectIds to request from signatory
 * @returns {Promise.<Object>} JSON response from API
 */
DocsigService.prototype.documentSignRequest = function (signatoryEmail, callbackUrl, documentUrl, objectIds) {
  const appId = this._docsigAppId
  const appSecret = this._docsigAppSecret

  const payload = {
    iss: appId,
    signatory: signatoryEmail,
    callbackUrl: callbackUrl,
    documentUrl: documentUrl,
    objectIds: objectIds
  }
  const header = {typ: 'JWT', iss: appId}
  const jwt = Utils.createHmacJws(payload, appSecret, header)

  return this.httpClient.post('newDocumentSignRequest?jwt=' + jwt)
}
