Source: utils/vc/crypto/common/CustomLinkedDataSignature.js

import jsigs from 'jsonld-signatures';
import base58btc from 'bs58';
import base64url from 'base64url';
import { createJws } from '../../jws';

const MULTIBASE_BASE58BTC_HEADER = 'z';

export function decodeBase64Url(string) {
  const buffer = base64url.toBuffer(string);
  return new Uint8Array(buffer);
}

export function decodeBase64UrlToString(string) {
  return base64url.decode(string);
}

export default class CustomLinkedDataSignature extends jsigs.suites.LinkedDataSignature {
  /**
   * Creates a new CustomLinkedDataSignature instance
   * @constructor
   * @param {object} config - Configuration options
   */
  constructor(config) {
    super(config);
    this.alg = config.alg;
    this.useProofValue = config.useProofValue || false;
  }

  /**
   * Verifies the proof signature against the given data.
   *
   * @param {object} options - The options to use.
   * @param {Uint8Array} options.verifyData - Canonicalized hashed data.
   * @param {object} options.verificationMethod - Key object.
   * @param {object} options.proof - The proof to be verified.
   *
   * @returns {Promise<boolean>} Resolves with the verification result.
   */
  async verifySignature({ verifyData, verificationMethod, proof }) {
    let signatureBytes;
    let data = verifyData;

    const { proofValue, jws } = proof;
    if (proofValue && typeof proofValue === 'string') {
      signatureBytes = base58btc.decode(CustomLinkedDataSignature.fromJsigProofValue(proofValue));
    } else if (jws && typeof jws === 'string') { // Fallback to older jsonld-signature implementations
      const [encodedHeader, /* payload */, encodedSignature] = jws.split('.');

      let header;
      try {
        header = JSON.parse(decodeBase64UrlToString(encodedHeader));
      } catch (e) {
        throw new Error(`Could not parse JWS header; ${e}`);
      }
      if (!(header && typeof header === 'object')) {
        throw new Error('Invalid JWS header.');
      }

      signatureBytes = decodeBase64Url(encodedSignature);
      data = createJws({ encodedHeader, verifyData });
    }

    let { verifier } = this;
    if (!verifier) {
      const key = await this.LDKeyClass.from(verificationMethod);
      verifier = key.verifier();
    }
    return verifier.verify({ data, signature: signatureBytes });
  }

  /**
   * Adds a signature (proofValue) field to the proof object. Called by
   * LinkedDataSignature.createProof().
   *
   * @param {object} options - The options to use.
   * @param {Uint8Array} options.verifyData - Data to be signed (extracted
   *   from document, according to the suite's spec).
   * @param {object} options.proof - Proof object (containing the proofPurpose,
   *   verificationMethod, etc).
   *
   * @returns {Promise<object>} Resolves with the proof containing the signature
   *   value.
   */
  async sign({ verifyData, proof }) {
    if (!(this.signer && typeof this.signer.sign === 'function')) {
      throw new Error('A signer API has not been specified.');
    }

    const getSigBytes = async (data) => {
      let signatureBytes;
      const signature = await this.signer.sign({ data });
      if (typeof signature === 'string') {
        // Some signers will return a string like: header..signature
        // split apart those strings to get the signature in bytes
        const signatureSplit = signature.split('.');
        const signatureEncoded = signatureSplit[signatureSplit.length - 1];
        signatureBytes = decodeBase64Url(signatureEncoded);
      } else {
        signatureBytes = signature;
      }
      return signatureBytes;
    };

    const finalProof = { ...proof };
    if (this.useProofValue) {
      const signatureBytes = await getSigBytes(verifyData);
      finalProof.proofValue = CustomLinkedDataSignature.encodeProofValue(signatureBytes);
    } else {
      if (!this.alg) {
        throw new Error('Suite doesn\'t contain required alg parameter');
      }

      const header = { alg: this.alg, b64: false, crit: ['b64'] };
      const encodedHeader = base64url.encode(JSON.stringify(header));
      const jwsData = createJws({ encodedHeader, verifyData });
      const signatureBytesJWS = await getSigBytes(jwsData);
      finalProof.jws = `${encodedHeader}..${base64url.encode(signatureBytesJWS)}`;
    }

    return finalProof;
  }

  static encodeProofValue(signatureBytes) {
    return MULTIBASE_BASE58BTC_HEADER + base58btc.encode(signatureBytes);
  }

  /**
   * Json-ld signs prefix signatures with a specific character. Removes that character
   * @param proofValue
   * @returns {*|string}
   */
  static fromJsigProofValue(proofValue) {
    if (proofValue[0] !== MULTIBASE_BASE58BTC_HEADER) {
      throw new Error('Only base58btc multibase encoding is supported.');
    }
    return proofValue.substring(1);
  }
}