mirror of
https://github.com/mosip/inji-wallet.git
synced 2026-01-09 21:48:04 -05:00
feat: add VC verification
This commit is contained in:
13
lib/jsonld-signatures/purposes/AssertionProofPurpose.ts
Normal file
13
lib/jsonld-signatures/purposes/AssertionProofPurpose.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
/*!
|
||||
* Copyright (c) 2018 Digital Bazaar, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
import { ControllerProofPurpose } from './ControllerProofPurpose';
|
||||
|
||||
export class AssertionProofPurpose extends ControllerProofPurpose {
|
||||
constructor({
|
||||
term = 'assertionMethod', controller,
|
||||
date, maxTimestampDelta = Infinity}: any = {}) {
|
||||
super({term, controller, date, maxTimestampDelta});
|
||||
}
|
||||
}
|
||||
138
lib/jsonld-signatures/purposes/ControllerProofPurpose.ts
Normal file
138
lib/jsonld-signatures/purposes/ControllerProofPurpose.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/*!
|
||||
* Copyright (c) 2018 Digital Bazaar, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
import constants from 'jsonld-signatures/lib/constants';
|
||||
import jsonld from 'jsonld';
|
||||
import { ProofPurpose } from './ProofPurpose';
|
||||
|
||||
// DID documents can be specially optimized
|
||||
const DID_CONTEXT_V1 = 'https://www.w3.org/ns/did/v1';
|
||||
// verification relationship terms that are known to appear in DID documents
|
||||
const DID_VR_TERMS = [
|
||||
'assertionMethod',
|
||||
'authentication',
|
||||
'capabilityInvocation',
|
||||
'capabilityDelegation',
|
||||
'keyAgreement',
|
||||
'verificationMethod'
|
||||
];
|
||||
|
||||
export class ControllerProofPurpose extends ProofPurpose {
|
||||
/**
|
||||
* Creates a proof purpose that will validate whether or not the verification
|
||||
* method in a proof was authorized by its declared controller for the
|
||||
* proof's purpose.
|
||||
*
|
||||
* @param term {string} the `proofPurpose` term, as defined in the
|
||||
* SECURITY_CONTEXT_URL `@context` or a URI if not defined in such.
|
||||
* @param [controller] {object} the description of the controller, if it
|
||||
* is not to be dereferenced via a `documentLoader`.
|
||||
* @param [date] {string or Date or integer} the expected date for
|
||||
* the creation of the proof.
|
||||
* @param [maxTimestampDelta] {integer} a maximum number of seconds that
|
||||
* the date on the signature can deviate from, defaults to `Infinity`.
|
||||
*/
|
||||
constructor({term, controller, date, maxTimestampDelta = Infinity}: any = {}) {
|
||||
super({term, date, maxTimestampDelta});
|
||||
if(controller !== undefined) {
|
||||
if(typeof controller !== 'object') {
|
||||
throw new TypeError('"controller" must be an object.');
|
||||
}
|
||||
this.controller = controller;
|
||||
}
|
||||
this._termDefinedByDIDContext = DID_VR_TERMS.includes(term);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the purpose of a proof. This method is called during
|
||||
* proof verification, after the proof value has been checked against the
|
||||
* given verification method (e.g. in the case of a digital signature, the
|
||||
* signature has been cryptographically verified against the public key).
|
||||
*
|
||||
* @param proof
|
||||
* @param verificationMethod
|
||||
* @param documentLoader
|
||||
* @param expansionMap
|
||||
*
|
||||
* @throws {Error} If verification method not authorized by controller
|
||||
* @throws {Error} If proof's created timestamp is out of range
|
||||
*
|
||||
* @returns {Promise<{valid: boolean, error: Error}>}
|
||||
*/
|
||||
async validate(proof, {verificationMethod, documentLoader, expansionMap}) {
|
||||
try {
|
||||
const result = await super.validate(
|
||||
proof, {verificationMethod, documentLoader, expansionMap});
|
||||
if(!result.valid) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
const {id: verificationId} = verificationMethod;
|
||||
const {term, _termDefinedByDIDContext} = this;
|
||||
|
||||
// if no `controller` specified, use verification method's
|
||||
if(this.controller) {
|
||||
result.controller = this.controller;
|
||||
} else {
|
||||
const {controller} = verificationMethod;
|
||||
let controllerId;
|
||||
if(controller) {
|
||||
if(typeof controller === 'object') {
|
||||
controllerId = controller.id;
|
||||
} else if(typeof controller !== 'string') {
|
||||
throw new TypeError(
|
||||
'"controller" must be a string representing a URL.');
|
||||
} else {
|
||||
controllerId = controller;
|
||||
}
|
||||
}
|
||||
|
||||
// apply optimization to controller documents that are DID documents;
|
||||
// if `term` is one of those defined by the DID context
|
||||
let {document} = await documentLoader(controllerId);
|
||||
|
||||
// Try to parse document to JSON
|
||||
if (typeof document !== 'object') {
|
||||
try {
|
||||
document = JSON.parse(document);
|
||||
} catch (e) {
|
||||
throw new Error(`Controller ${controllerId} document JSON parse error: ` + e);
|
||||
}
|
||||
}
|
||||
const mustFrame = !(_termDefinedByDIDContext &&
|
||||
document['@context'] === DID_CONTEXT_V1 ||
|
||||
(Array.isArray(document['@context']) &&
|
||||
document['@context'][0] === DID_CONTEXT_V1));
|
||||
if(mustFrame) {
|
||||
// Note: `expansionMap` is intentionally not passed; we can safely
|
||||
// drop properties here and must allow for it
|
||||
document = await jsonld.frame(document, {
|
||||
'@context': constants.SECURITY_CONTEXT_URL,
|
||||
id: controllerId,
|
||||
// this term must be in the JSON-LD controller document or
|
||||
// verification will fail
|
||||
[term]: {
|
||||
'@embed': '@never',
|
||||
id: verificationId
|
||||
}
|
||||
}, {documentLoader, compactToRelative: false});
|
||||
}
|
||||
result.controller = document;
|
||||
}
|
||||
|
||||
const verificationMethods = jsonld.getValues(result.controller, term);
|
||||
result.valid = verificationMethods.some(vm =>
|
||||
vm === verificationId ||
|
||||
(typeof vm === 'object' && vm.id === verificationId));
|
||||
if(!result.valid) {
|
||||
throw new Error(
|
||||
`Verification method "${verificationMethod.id}" not authorized ` +
|
||||
`by controller for proof purpose "${this.term}".`);
|
||||
}
|
||||
return result;
|
||||
} catch(error) {
|
||||
return {valid: false, error};
|
||||
}
|
||||
}
|
||||
}
|
||||
92
lib/jsonld-signatures/purposes/ProofPurpose.ts
Normal file
92
lib/jsonld-signatures/purposes/ProofPurpose.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
/*!
|
||||
* Copyright (c) 2018 Digital Bazaar, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
export class ProofPurpose {
|
||||
/**
|
||||
* @param term {string} the `proofPurpose` term, as defined in the
|
||||
* SECURITY_CONTEXT_URL `@context` or a URI if not defined in such.
|
||||
* @param [date] {string or Date or integer} the expected date for
|
||||
* the creation of the proof.
|
||||
* @param [maxTimestampDelta] {integer} a maximum number of seconds that
|
||||
* the date on the signature can deviate from, defaults to `Infinity`.
|
||||
*/
|
||||
constructor({term, date, maxTimestampDelta = Infinity}: any = {}) {
|
||||
if(term === undefined) {
|
||||
throw new Error('"term" is required.');
|
||||
}
|
||||
if(maxTimestampDelta !== undefined &&
|
||||
typeof maxTimestampDelta !== 'number') {
|
||||
throw new TypeError('"maxTimestampDelta" must be a number.');
|
||||
}
|
||||
this.term = term;
|
||||
if(date !== undefined) {
|
||||
this.date = new Date(date);
|
||||
if(isNaN(this.date)) {
|
||||
throw TypeError(`"date" "${date}" is not a valid date.`);
|
||||
}
|
||||
}
|
||||
this.maxTimestampDelta = maxTimestampDelta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to validate the purpose of a proof. This method is called during
|
||||
* proof verification, after the proof value has been checked against the
|
||||
* given verification method (e.g. in the case of a digital signature, the
|
||||
* signature has been cryptographically verified against the public key).
|
||||
*
|
||||
* @param proof {object} the proof, in the `constants.SECURITY_CONTEXT_URL`,
|
||||
* with the matching purpose to validate.
|
||||
*
|
||||
* @return {Promise<object>} resolves to an object with `valid` and `error`.
|
||||
*/
|
||||
async validate(
|
||||
proof, {/*document, suite, verificationMethod,
|
||||
documentLoader, expansionMap*/}) {
|
||||
try {
|
||||
// check expiration
|
||||
if(this.maxTimestampDelta !== Infinity) {
|
||||
const expected = (this.date || new Date()).getTime();
|
||||
const delta = this.maxTimestampDelta * 1000;
|
||||
const created = new Date(proof.created).getTime();
|
||||
// comparing this way handles NaN case where `created` is invalid
|
||||
if(!(created >= (expected - delta) && created <= (expected + delta))) {
|
||||
throw new Error('The proof\'s created timestamp is out of range.');
|
||||
}
|
||||
}
|
||||
return {valid: true};
|
||||
} catch(error) {
|
||||
return {valid: false, error};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update a proof when it is being created, adding any properties
|
||||
* specific to this purpose. This method is called prior to the proof
|
||||
* value being generated such that any properties added may be, for example,
|
||||
* included in a digital signature value.
|
||||
*
|
||||
* @param proof {object} the proof, in the `constants.SECURITY_CONTEXT_URL`
|
||||
* to update.
|
||||
*
|
||||
* @return {Promise<object>} resolves to the proof instance (in the
|
||||
* `constants.SECURITY_CONTEXT_URL`.
|
||||
*/
|
||||
async update(proof, {/*document, suite, documentLoader, expansionMap */}) {
|
||||
proof.proofPurpose = this.term;
|
||||
return proof;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the given proof has a purpose that matches this instance,
|
||||
* i.e. this ProofPurpose instance should be used to validate the given
|
||||
* proof.
|
||||
*
|
||||
* @param proof {object} the proof to check.
|
||||
*
|
||||
* @return {Promise<boolean>} `true` if there's a match, `false` if not.
|
||||
*/
|
||||
async match(proof, {/* document, documentLoader, expansionMap */}) {
|
||||
return proof.proofPurpose === this.term;
|
||||
}
|
||||
};
|
||||
22
lib/jsonld-signatures/purposes/PublicKeyProofPurpose.ts
Normal file
22
lib/jsonld-signatures/purposes/PublicKeyProofPurpose.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
/*!
|
||||
* Copyright (c) 2018 Digital Bazaar, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
import { ControllerProofPurpose } from './ControllerProofPurpose';
|
||||
|
||||
export class PublicKeyProofPurpose extends ControllerProofPurpose {
|
||||
constructor({ controller, date, maxTimestampDelta = Infinity }: any = {}) {
|
||||
super({ term: 'publicKey', controller, date, maxTimestampDelta });
|
||||
}
|
||||
|
||||
async update(proof) {
|
||||
// do not add `term` to proof
|
||||
return proof;
|
||||
}
|
||||
|
||||
async match(proof) {
|
||||
// `proofPurpose` must not be present in the proof to match as this
|
||||
// proof purpose is a legacy, non-descript purpose for signing
|
||||
return proof.proofPurpose === undefined;
|
||||
}
|
||||
}
|
||||
15
lib/jsonld-signatures/sha256digest.ts
Normal file
15
lib/jsonld-signatures/sha256digest.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import crypto from 'isomorphic-webcrypto';
|
||||
import 'fast-text-encoding';
|
||||
|
||||
/**
|
||||
* Hashes a string of data using SHA-256.
|
||||
*
|
||||
* @param {string} string - the string to hash.
|
||||
*
|
||||
* @return {Uint8Array} the hash digest.
|
||||
*/
|
||||
export async function sha256digest({ string }: any) {
|
||||
const bytes = new TextEncoder().encode(string);
|
||||
|
||||
return new Uint8Array(await crypto.subtle.digest({ name: 'SHA-256' }, bytes));
|
||||
}
|
||||
233
lib/jsonld-signatures/suites/JwsLinkedDataSignature.ts
Normal file
233
lib/jsonld-signatures/suites/JwsLinkedDataSignature.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
/*!
|
||||
* Copyright (c) 2020-2021 Digital Bazaar, Inc. All rights reserved.
|
||||
*/
|
||||
import { Buffer } from 'buffer';
|
||||
import 'fast-text-encoding';
|
||||
import jsonld from 'jsonld';
|
||||
import LinkedDataSignature from 'jsonld-signatures/lib/suites/LinkedDataSignature';
|
||||
import { encode, decode } from 'base64url-universal';
|
||||
|
||||
export interface JwsLinkedDataSignature {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class JwsLinkedDataSignature extends LinkedDataSignature {
|
||||
/**
|
||||
* @param type {string} Provided by subclass.
|
||||
* @param alg {string} JWS alg provided by subclass.
|
||||
* @param [LDKeyClass] {LDKeyClass} provided by subclass or subclass
|
||||
* overrides `getVerificationMethod`.
|
||||
*
|
||||
* @param [verificationMethod] {string} A key id URL to the paired public key.
|
||||
*
|
||||
* This parameter is required for signing:
|
||||
*
|
||||
* @param [signer] {function} an optional signer.
|
||||
*
|
||||
* Advanced optional parameters and overrides:
|
||||
*
|
||||
* @param [proof] {object} a JSON-LD document with options to use for
|
||||
* the `proof` node (e.g. any other custom fields can be provided here
|
||||
* using a context different from security-v2).
|
||||
* @param [date] {string|Date} signing date to use if not passed.
|
||||
* @param [key] {LDKeyPair} an optional crypto-ld KeyPair.
|
||||
* @param [useNativeCanonize] {boolean} true to use a native canonize
|
||||
* algorithm.
|
||||
*/
|
||||
constructor({
|
||||
type,
|
||||
alg,
|
||||
LDKeyClass,
|
||||
verificationMethod,
|
||||
signer,
|
||||
key,
|
||||
proof,
|
||||
date,
|
||||
useNativeCanonize,
|
||||
}: any = {}) {
|
||||
super({ type, verificationMethod, proof, date, useNativeCanonize });
|
||||
this.alg = alg;
|
||||
this.LDKeyClass = LDKeyClass;
|
||||
this.signer = signer;
|
||||
if (key) {
|
||||
if (verificationMethod === undefined) {
|
||||
const publicKey = key.export({ publicKey: true });
|
||||
this.verificationMethod = publicKey.id;
|
||||
}
|
||||
this.key = key;
|
||||
if (typeof key.signer === 'function') {
|
||||
this.signer = key.signer();
|
||||
}
|
||||
if (typeof key.verifier === 'function') {
|
||||
this.verifier = key.verifier();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param verifyData {Uint8Array}.
|
||||
* @param proof {object}
|
||||
*
|
||||
* @returns {Promise<{object}>} 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.');
|
||||
}
|
||||
// JWS header
|
||||
const header = {
|
||||
alg: this.alg,
|
||||
b64: false,
|
||||
crit: ['b64'],
|
||||
};
|
||||
|
||||
/*
|
||||
+-------+-----------------------------------------------------------+
|
||||
| "b64" | JWS Signing Input Formula |
|
||||
+-------+-----------------------------------------------------------+
|
||||
| true | ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || |
|
||||
| | BASE64URL(JWS Payload)) |
|
||||
| | |
|
||||
| false | ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.') || |
|
||||
| | JWS Payload |
|
||||
+-------+-----------------------------------------------------------+
|
||||
*/
|
||||
|
||||
// create JWS data and sign
|
||||
const encodedHeader = encode(JSON.stringify(header));
|
||||
|
||||
const data = _createJws({ encodedHeader, verifyData });
|
||||
|
||||
const signature = await this.signer.sign({ data });
|
||||
|
||||
// create detached content signature
|
||||
const encodedSignature = encode(signature);
|
||||
proof.jws = encodedHeader + '..' + encodedSignature;
|
||||
return proof;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param verifyData {Uint8Array}.
|
||||
* @param verificationMethod {object}.
|
||||
* @param document {object} the document the proof applies to.
|
||||
* @param proof {object} the proof to be verified.
|
||||
* @param purpose {ProofPurpose}
|
||||
* @param documentLoader {function}
|
||||
* @param expansionMap {function}
|
||||
*
|
||||
* @returns {Promise<{boolean}>} Resolves with the verification result.
|
||||
*/
|
||||
async verifySignature({ verifyData, verificationMethod, proof }) {
|
||||
if (
|
||||
!(proof.jws && typeof proof.jws === 'string' && proof.jws.includes('.'))
|
||||
) {
|
||||
throw new TypeError('The proof does not include a valid "jws" property.');
|
||||
}
|
||||
// add payload into detached content signature
|
||||
const [encodedHeader /*payload*/, , encodedSignature] =
|
||||
proof.jws.split('.');
|
||||
|
||||
let header;
|
||||
try {
|
||||
header = JSON.parse(Buffer.from(encodedHeader, 'base64').toString());
|
||||
} catch (e) {
|
||||
throw new Error(`Could not parse JWS header; ${e}`);
|
||||
}
|
||||
if (!(header && typeof header === 'object')) {
|
||||
throw new Error('Invalid JWS header.');
|
||||
}
|
||||
|
||||
// confirm header matches all expectations
|
||||
if (
|
||||
!(
|
||||
header.alg === this.alg &&
|
||||
header.b64 === false &&
|
||||
Array.isArray(header.crit) &&
|
||||
header.crit.length === 1 &&
|
||||
header.crit[0] === 'b64'
|
||||
) &&
|
||||
Object.keys(header).length === 3
|
||||
) {
|
||||
throw new Error(`Invalid JWS header parameters for ${this.type}.`);
|
||||
}
|
||||
|
||||
// do signature verification
|
||||
const signature = Buffer.from(encodedSignature, 'base64');
|
||||
|
||||
const data = _createJws({ encodedHeader, verifyData });
|
||||
|
||||
let { verifier } = this;
|
||||
if (!verifier) {
|
||||
const key = await this.LDKeyClass.from(verificationMethod);
|
||||
verifier = key.verifier();
|
||||
}
|
||||
return verifier.verify({ data, signature });
|
||||
}
|
||||
|
||||
async assertVerificationMethod({ verificationMethod }) {
|
||||
if (!jsonld.hasValue(verificationMethod, 'type', this.requiredKeyType)) {
|
||||
throw new Error(
|
||||
`Invalid key type. Key type must be "${this.requiredKeyType}".`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getVerificationMethod({ proof, documentLoader }) {
|
||||
if (this.key) {
|
||||
return this.key.export({ publicKey: true });
|
||||
}
|
||||
|
||||
const verificationMethod = await super.getVerificationMethod({
|
||||
proof,
|
||||
documentLoader,
|
||||
});
|
||||
await this.assertVerificationMethod({ verificationMethod });
|
||||
return verificationMethod;
|
||||
}
|
||||
|
||||
async matchProof({ proof, document, purpose, documentLoader, expansionMap }) {
|
||||
if (
|
||||
!(await super.matchProof({
|
||||
proof,
|
||||
document,
|
||||
purpose,
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
}))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
// NOTE: When subclassing this suite: Extending suites will need to check
|
||||
// for the presence their contexts here and in sign()
|
||||
|
||||
if (!this.key) {
|
||||
// no key specified, so assume this suite matches and it can be retrieved
|
||||
return true;
|
||||
}
|
||||
|
||||
const { verificationMethod } = proof;
|
||||
|
||||
// only match if the key specified matches the one in the proof
|
||||
if (typeof verificationMethod === 'object') {
|
||||
return verificationMethod.id === this.key.id;
|
||||
}
|
||||
return verificationMethod === this.key.id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the bytes ready for signing.
|
||||
*
|
||||
* @param {string} encodedHeader - base64url encoded JWT header.
|
||||
* @param {Uint8Array} verifyData - Payload to sign/verify.
|
||||
* @returns {Uint8Array} A combined byte array for signing.
|
||||
*/
|
||||
function _createJws({ encodedHeader, verifyData }) {
|
||||
const encodedHeaderBytes = new TextEncoder().encode(encodedHeader + '.');
|
||||
|
||||
// concatenate the two uint8arrays
|
||||
const data = new Uint8Array(encodedHeaderBytes.length + verifyData.length);
|
||||
data.set(encodedHeaderBytes, 0);
|
||||
data.set(verifyData, encodedHeaderBytes.length);
|
||||
return data;
|
||||
}
|
||||
468
lib/jsonld-signatures/suites/LinkedDataSignature.ts
Normal file
468
lib/jsonld-signatures/suites/LinkedDataSignature.ts
Normal file
@@ -0,0 +1,468 @@
|
||||
/*!
|
||||
* Copyright (c) 2017-2021 Digital Bazaar, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
import { sha256digest } from '../sha256digest';
|
||||
import jsonld from 'jsonld';
|
||||
|
||||
import constants from 'jsonld-signatures/lib/constants';
|
||||
import util from 'jsonld-signatures/lib/util';
|
||||
import LinkedDataProof from 'jsonld-signatures/lib/suites/LinkedDataProof';
|
||||
|
||||
export interface LinkedDataSignature {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class LinkedDataSignature extends LinkedDataProof {
|
||||
/**
|
||||
* Parent class from which the various LinkDataSignature suites (such as
|
||||
* `Ed25519Signature2020`) inherit.
|
||||
* NOTE: Developers are never expected to use this class directly, but to
|
||||
* only work with individual suites.
|
||||
*
|
||||
* @param {object} options - Options hashmap.
|
||||
* @param {string} options.type - Suite name, provided by subclass.
|
||||
* @typedef LDKeyPair
|
||||
* @param {LDKeyPair} LDKeyClass - The crypto-ld key class that this suite
|
||||
* will use to sign/verify signatures. Provided by subclass. Used
|
||||
* during the `verifySignature` operation, to create an instance (containing
|
||||
* a `verifier()` property) of a public key fetched via a `documentLoader`.
|
||||
*
|
||||
* @param {string} contextUrl - JSON-LD context URL that corresponds to this
|
||||
* signature suite. Provided by subclass. Used for enforcing suite context
|
||||
* during the `sign()` operation.
|
||||
*
|
||||
* For `sign()` operations, either a `key` OR a `signer` is required.
|
||||
* For `verify()` operations, you can pass in a verifier (from KMS), or
|
||||
* the public key will be fetched via documentLoader.
|
||||
*
|
||||
* @param {object} [options.key] - An optional key object (containing an
|
||||
* `id` property, and either `signer` or `verifier`, depending on the
|
||||
* intended operation. Useful for when the application is managing keys
|
||||
* itself (when using a KMS, you never have access to the private key,
|
||||
* and so should use the `signer` param instead).
|
||||
*
|
||||
* @param {{sign: Function, id: string}} [options.signer] - Signer object
|
||||
* that has two properties: an async `sign()` method, and an `id`. This is
|
||||
* useful when interfacing with a KMS (since you don't get access to the
|
||||
* private key and its `signer`, the KMS client gives you only the signer
|
||||
* object to use).
|
||||
*
|
||||
* @param {{verify: Function, id: string}} [options.verifier] - Verifier
|
||||
* object that has two properties: an async `verify()` method, and an `id`.
|
||||
* Useful when working with a KMS-provided verifier.
|
||||
*
|
||||
* Advanced optional parameters and overrides:
|
||||
*
|
||||
* @param {object} [options.proof] - A JSON-LD document with options to use
|
||||
* for the `proof` node (e.g. any other custom fields can be provided here
|
||||
* using a context different from security-v2). If not provided, this is
|
||||
* constructed during signing.
|
||||
* @param {string|Date} [options.date] - Signing date to use (otherwise
|
||||
* defaults to `now()`).
|
||||
* @param {boolean} [options.useNativeCanonize] - Whether to use a native
|
||||
* canonize algorithm.
|
||||
*/
|
||||
constructor({
|
||||
type,
|
||||
proof,
|
||||
LDKeyClass,
|
||||
date,
|
||||
key,
|
||||
signer,
|
||||
verifier,
|
||||
useNativeCanonize,
|
||||
contextUrl,
|
||||
}: any = {}) {
|
||||
super({ type });
|
||||
this.LDKeyClass = LDKeyClass;
|
||||
this.contextUrl = contextUrl;
|
||||
this.proof = proof;
|
||||
const vm = _processSignatureParams({ key, signer, verifier });
|
||||
this.verificationMethod = vm.verificationMethod;
|
||||
this.key = vm.key;
|
||||
this.signer = vm.signer;
|
||||
this.verifier = vm.verifier;
|
||||
if (date) {
|
||||
this.date = new Date(date);
|
||||
if (isNaN(this.date)) {
|
||||
throw TypeError(`"date" "${date}" is not a valid date.`);
|
||||
}
|
||||
}
|
||||
this.useNativeCanonize = useNativeCanonize;
|
||||
this._hashCache = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param document {object} to be signed.
|
||||
* @param purpose {ProofPurpose}
|
||||
* @param documentLoader {function}
|
||||
* @param expansionMap {function}
|
||||
*
|
||||
* @returns {Promise<object>} Resolves with the created proof object.
|
||||
*/
|
||||
async createProof({ document, purpose, documentLoader, expansionMap }) {
|
||||
// build proof (currently known as `signature options` in spec)
|
||||
let proof;
|
||||
if (this.proof) {
|
||||
// shallow copy
|
||||
proof = { ...this.proof };
|
||||
} else {
|
||||
// create proof JSON-LD document
|
||||
proof = {};
|
||||
}
|
||||
|
||||
// ensure proof type is set
|
||||
proof.type = this.type;
|
||||
|
||||
// set default `now` date if not given in `proof` or `options`
|
||||
let date = this.date;
|
||||
if (proof.created === undefined && date === undefined) {
|
||||
date = new Date();
|
||||
}
|
||||
|
||||
// ensure date is in string format
|
||||
if (date && typeof date !== 'string') {
|
||||
date = util.w3cDate(date);
|
||||
}
|
||||
|
||||
// add API overrides
|
||||
if (date) {
|
||||
proof.created = date;
|
||||
}
|
||||
|
||||
proof.verificationMethod = this.verificationMethod;
|
||||
|
||||
// add any extensions to proof (mostly for legacy support)
|
||||
proof = await this.updateProof({
|
||||
document,
|
||||
proof,
|
||||
purpose,
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
});
|
||||
|
||||
// allow purpose to update the proof; the `proof` is in the
|
||||
// SECURITY_CONTEXT_URL `@context` -- therefore the `purpose` must
|
||||
// ensure any added fields are also represented in that same `@context`
|
||||
proof = await purpose.update(proof, {
|
||||
document,
|
||||
suite: this,
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
});
|
||||
|
||||
// create data to sign
|
||||
const verifyData = await this.createVerifyData({
|
||||
document,
|
||||
proof,
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
});
|
||||
|
||||
// sign data
|
||||
proof = await this.sign({
|
||||
verifyData,
|
||||
document,
|
||||
proof,
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
});
|
||||
|
||||
return proof;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param document {object} to be signed.
|
||||
* @param purpose {ProofPurpose}
|
||||
* @param documentLoader {function}
|
||||
* @param expansionMap {function}
|
||||
*
|
||||
* @returns {Promise<object>} Resolves with the created proof object.
|
||||
*/
|
||||
async updateProof({ proof }: any) {
|
||||
// extending classes may do more
|
||||
return proof;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param proof {object} the proof to be verified.
|
||||
* @param document {object} the document the proof applies to.
|
||||
* @param documentLoader {function}
|
||||
* @param expansionMap {function}
|
||||
*
|
||||
* @returns {Promise<{object}>} Resolves with the verification result.
|
||||
*/
|
||||
async verifyProof({ proof, document, documentLoader, expansionMap }) {
|
||||
try {
|
||||
// create data to verify
|
||||
const verifyData = await this.createVerifyData({
|
||||
document,
|
||||
proof,
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
});
|
||||
|
||||
// fetch verification method
|
||||
const verificationMethod = await this.getVerificationMethod({
|
||||
proof,
|
||||
document,
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
});
|
||||
|
||||
// verify signature on data
|
||||
const verified = await this.verifySignature({
|
||||
verifyData,
|
||||
verificationMethod,
|
||||
document,
|
||||
proof,
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
});
|
||||
if (!verified) {
|
||||
throw new Error('Invalid signature.');
|
||||
}
|
||||
|
||||
return { verified: true, verificationMethod };
|
||||
} catch (error) {
|
||||
return { verified: false, error };
|
||||
}
|
||||
}
|
||||
|
||||
async canonize(input, { documentLoader, expansionMap, skipExpansion }: any) {
|
||||
return jsonld.canonize(input, {
|
||||
algorithm: 'URDNA2015',
|
||||
format: 'application/n-quads',
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
skipExpansion,
|
||||
useNative: this.useNativeCanonize,
|
||||
});
|
||||
}
|
||||
|
||||
async canonizeProof(proof, { document, documentLoader, expansionMap }) {
|
||||
// `jws`,`signatureValue`,`proofValue` must not be included in the proof
|
||||
// options
|
||||
proof = {
|
||||
'@context': document['@context'] || constants.SECURITY_CONTEXT_URL,
|
||||
...proof,
|
||||
};
|
||||
delete proof.jws;
|
||||
delete proof.signatureValue;
|
||||
delete proof.proofValue;
|
||||
return this.canonize(proof, {
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
skipExpansion: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param document {object} to be signed/verified.
|
||||
* @param proof {object}
|
||||
* @param documentLoader {function}
|
||||
* @param expansionMap {function}
|
||||
*
|
||||
* @returns {Promise<{Uint8Array}>}.
|
||||
*/
|
||||
async createVerifyData({ document, proof, documentLoader, expansionMap }) {
|
||||
// get cached document hash
|
||||
let cachedDocHash;
|
||||
const { _hashCache } = this;
|
||||
if (_hashCache && _hashCache.document === document) {
|
||||
cachedDocHash = _hashCache.hash;
|
||||
} else {
|
||||
this._hashCache = {
|
||||
document,
|
||||
// canonize and hash document
|
||||
hash: (cachedDocHash = this.canonize(document, {
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
}).then((c14nDocument) => {
|
||||
return sha256digest({ string: c14nDocument });
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
// await both c14n proof hash and c14n document hash
|
||||
const [proofHash, docHash] = await Promise.all([
|
||||
// canonize and hash proof
|
||||
this.canonizeProof(proof, {
|
||||
document,
|
||||
documentLoader,
|
||||
expansionMap,
|
||||
}).then((c14nProofOptions) => {
|
||||
return sha256digest({ string: c14nProofOptions });
|
||||
}),
|
||||
cachedDocHash,
|
||||
]);
|
||||
|
||||
// concatenate hash of c14n proof options and hash of c14n document
|
||||
return util.concat(proofHash, docHash);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param document {object} to be signed.
|
||||
* @param proof {object}
|
||||
* @param documentLoader {function}
|
||||
*/
|
||||
async getVerificationMethod({ proof, documentLoader }: any) {
|
||||
let { verificationMethod } = proof;
|
||||
|
||||
if (typeof verificationMethod === 'object') {
|
||||
verificationMethod = verificationMethod.id;
|
||||
}
|
||||
|
||||
if (!verificationMethod) {
|
||||
throw new Error('No "verificationMethod" found in proof.');
|
||||
}
|
||||
|
||||
// Note: `expansionMap` is intentionally not passed; we can safely drop
|
||||
// properties here and must allow for it
|
||||
const framed = await jsonld.frame(
|
||||
verificationMethod,
|
||||
{
|
||||
'@context': constants.SECURITY_CONTEXT_URL,
|
||||
'@embed': '@always',
|
||||
'id': verificationMethod,
|
||||
},
|
||||
{ documentLoader, compactToRelative: false }
|
||||
);
|
||||
if (!framed) {
|
||||
throw new Error(`Verification method ${verificationMethod} not found.`);
|
||||
}
|
||||
|
||||
// ensure verification method has not been revoked
|
||||
if (framed.revoked !== undefined) {
|
||||
throw new Error('The verification method has been revoked.');
|
||||
}
|
||||
|
||||
return framed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param verifyData {Uint8Array}.
|
||||
* @param document {object} to be signed.
|
||||
* @param proof {object}
|
||||
* @param documentLoader {function}
|
||||
* @param expansionMap {function}
|
||||
*
|
||||
* @returns {Promise<{object}>} the proof containing the signature value.
|
||||
*/
|
||||
async sign(params?: any) {
|
||||
throw new Error('Must be implemented by a derived class.');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param verifyData {Uint8Array}.
|
||||
* @param verificationMethod {object}.
|
||||
* @param document {object} to be signed.
|
||||
* @param proof {object}
|
||||
* @param documentLoader {function}
|
||||
* @param expansionMap {function}
|
||||
*
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
async verifySignature(params?: any): Promise<any> {
|
||||
throw new Error('Must be implemented by a derived class.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures the document to be signed contains the required signature suite
|
||||
* specific `@context`, by either adding it (if `addSuiteContext` is true),
|
||||
* or throwing an error if it's missing.
|
||||
*
|
||||
* @param {object} options - Options hashmap.
|
||||
* @param {object} options.document - JSON-LD document to be signed.
|
||||
* @param {boolean} options.addSuiteContext - Add suite context?
|
||||
*/
|
||||
ensureSuiteContext({ document, addSuiteContext }) {
|
||||
const { contextUrl } = this;
|
||||
|
||||
if (_includesContext({ document, contextUrl })) {
|
||||
// document already includes the required context
|
||||
return;
|
||||
}
|
||||
|
||||
if (!addSuiteContext) {
|
||||
throw new TypeError(
|
||||
`The document to be signed must contain this suite's @context, ` +
|
||||
`"${contextUrl}".`
|
||||
);
|
||||
}
|
||||
|
||||
// enforce the suite's context by adding it to the document
|
||||
const existingContext = document['@context'] || [];
|
||||
|
||||
document['@context'] = Array.isArray(existingContext)
|
||||
? [...existingContext, contextUrl]
|
||||
: [existingContext, contextUrl];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether a provided JSON-LD document includes a context URL in its
|
||||
* `@context` property.
|
||||
*
|
||||
* @param {object} options - Options hashmap.
|
||||
* @param {object} options.document - A JSON-LD document.
|
||||
* @param {string} options.contextUrl - A context URL.
|
||||
*
|
||||
* @returns {boolean} Returns true if document includes context.
|
||||
*/
|
||||
function _includesContext({ document, contextUrl }) {
|
||||
const context = document['@context'];
|
||||
return (
|
||||
context === contextUrl ||
|
||||
(Array.isArray(context) && context.includes(contextUrl))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* See constructor docstring for param details.
|
||||
*
|
||||
* @returns {{verificationMethod: string, key: LDKeyPair,
|
||||
* signer: {sign: Function, id: string},
|
||||
* verifier: {verify: Function, id: string}}} - Validated and initialized
|
||||
* key-related parameters.
|
||||
*/
|
||||
function _processSignatureParams({ key, signer, verifier }) {
|
||||
// We are explicitly not requiring a key or signer/verifier param to be
|
||||
// present, to support the verify() use case where the verificationMethod
|
||||
// is being fetched by the documentLoader
|
||||
|
||||
const vm: any = {};
|
||||
if (key) {
|
||||
vm.key = key;
|
||||
vm.verificationMethod = key.id;
|
||||
if (typeof key.signer === 'function') {
|
||||
vm.signer = key.signer();
|
||||
}
|
||||
if (typeof key.verifier === 'function') {
|
||||
vm.verifier = key.verifier();
|
||||
}
|
||||
if (!(vm.signer || vm.verifier)) {
|
||||
throw new TypeError(
|
||||
'The "key" parameter must contain a "signer" or "verifier" method.'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
vm.verificationMethod = (signer && signer.id) || (verifier && verifier.id);
|
||||
vm.signer = signer;
|
||||
vm.verifier = verifier;
|
||||
}
|
||||
|
||||
if (vm.signer) {
|
||||
if (typeof vm.signer.sign !== 'function') {
|
||||
throw new TypeError('A signer API has not been specified.');
|
||||
}
|
||||
}
|
||||
if (vm.verifier) {
|
||||
if (typeof vm.verifier.verify !== 'function') {
|
||||
throw new TypeError('A verifier API has not been specified.');
|
||||
}
|
||||
}
|
||||
|
||||
return vm;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*!
|
||||
* Copyright (c) 2018 Digital Bazaar, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
// import { Ed25519KeyPair } from 'crypto-ld';
|
||||
import { Ed25519VerificationKey2018 } from './Ed25519VerificationKey2018';
|
||||
import { JwsLinkedDataSignature } from '../JwsLinkedDataSignature';
|
||||
|
||||
export class Ed25519Signature2018 extends JwsLinkedDataSignature {
|
||||
/**
|
||||
* @param type {string} Provided by subclass.
|
||||
*
|
||||
* One of these parameters is required to use a suite for signing:
|
||||
*
|
||||
* @param [creator] {string} A key id URL to the paired public key.
|
||||
* @param [verificationMethod] {string} A key id URL to the paired public key.
|
||||
*
|
||||
* This parameter is required for signing:
|
||||
*
|
||||
* @param [signer] {function} an optional signer.
|
||||
*
|
||||
* Advanced optional parameters and overrides:
|
||||
*
|
||||
* @param [proof] {object} a JSON-LD document with options to use for
|
||||
* the `proof` node (e.g. any other custom fields can be provided here
|
||||
* using a context different from security-v2).
|
||||
* @param [date] {string|Date} signing date to use if not passed.
|
||||
* @param [key] {LDKeyPair} an optional crypto-ld KeyPair.
|
||||
* @param [useNativeCanonize] {boolean} true to use a native canonize
|
||||
* algorithm.
|
||||
*/
|
||||
constructor({
|
||||
signer,
|
||||
key,
|
||||
verificationMethod,
|
||||
proof,
|
||||
date,
|
||||
useNativeCanonize,
|
||||
}: any = {}) {
|
||||
super({
|
||||
type: 'Ed25519Signature2018',
|
||||
alg: 'EdDSA',
|
||||
LDKeyClass: Ed25519VerificationKey2018,
|
||||
verificationMethod,
|
||||
signer,
|
||||
key,
|
||||
proof,
|
||||
date,
|
||||
useNativeCanonize,
|
||||
});
|
||||
this.requiredKeyType = 'Ed25519VerificationKey2018';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
/*!
|
||||
* Copyright (c) 2018-2020 Digital Bazaar, Inc. All rights reserved.
|
||||
*/
|
||||
import * as bs58 from 'base58-universal/main';
|
||||
import * as util from './util';
|
||||
import ed25519 from './ed25519';
|
||||
import {LDKeyPair} from 'crypto-ld';
|
||||
|
||||
const SUITE_ID = 'Ed25519VerificationKey2018';
|
||||
|
||||
class Ed25519VerificationKey2018 extends LDKeyPair {
|
||||
/**
|
||||
* An implementation of the Ed25519VerificationKey spec, for use with
|
||||
* Linked Data Proofs.
|
||||
* @see https://w3c-dvcg.github.io/lds-ed25519-2018/
|
||||
* @see https://github.com/digitalbazaar/jsonld-signatures
|
||||
* @example
|
||||
* > const privateKeyBase58 =
|
||||
* '3Mmk4UzTRJTEtxaKk61LxtgUxAa2Dg36jF6VogPtRiKvfpsQWKPCLesKSV182RMmvM'
|
||||
* + 'JKk6QErH3wgdHp8itkSSiF';
|
||||
* > const options = {
|
||||
* publicKeyBase58: 'GycSSui454dpYRKiFdsQ5uaE8Gy3ac6dSMPcAoQsk8yq',
|
||||
* privateKeyBase58
|
||||
* };
|
||||
* > const EDKey = new Ed25519VerificationKey2018(options);
|
||||
* > EDKey
|
||||
* Ed25519VerificationKey2018 { ...
|
||||
* @param {object} options - Options hashmap.
|
||||
* @param {string} options.controller - Controller DID or document url.
|
||||
* @param {string} [options.id] - The key ID. If not provided, will be
|
||||
* @param {string} options.publicKeyBase58 - Base58btc encoded Public Key.
|
||||
* @param {string} [options.privateKeyBase58] - Base58btc Private Key.
|
||||
* @param {string} [options.revoked] - Timestamp of when the key has been
|
||||
* revoked, in RFC3339 format. If not present, the key itself is considered
|
||||
* not revoked. Note that this mechanism is slightly different than DID
|
||||
* Document key revocation, where a DID controller can revoke a key from
|
||||
* that DID by removing it from the DID Document.
|
||||
*/
|
||||
constructor(options: any = {}) {
|
||||
super(options);
|
||||
this.type = SUITE_ID;
|
||||
this.publicKeyBase58 = options.publicKeyBase58;
|
||||
if(!this.publicKeyBase58) {
|
||||
throw new TypeError('The "publicKeyBase58" property is required.');
|
||||
}
|
||||
this.privateKeyBase58 = options.privateKeyBase58;
|
||||
if(this.controller && !this.id) {
|
||||
this.id = `${this.controller}#${this.fingerprint()}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an instance of LDKeyPair from a key fingerprint.
|
||||
* Note: Only key types that use their full public key in the fingerprint
|
||||
* are supported (so, currently, only 'ed25519').
|
||||
*
|
||||
* @param {string} fingerprint
|
||||
* @returns {LDKeyPair}
|
||||
* @throws Unsupported Fingerprint Type.
|
||||
*/
|
||||
static fromFingerprint({fingerprint} = {}) {
|
||||
if(!fingerprint ||
|
||||
!(typeof fingerprint === 'string' && fingerprint[0] === 'z')) {
|
||||
throw new Error('`fingerprint` must be a multibase encoded string.');
|
||||
}
|
||||
|
||||
// skip leading `z` that indicates base58 encoding
|
||||
const buffer = bs58.decode(fingerprint.substr(1));
|
||||
|
||||
// buffer is: 0xed 0x01 <public key bytes>
|
||||
if(buffer[0] === 0xed && buffer[1] === 0x01) {
|
||||
return new Ed25519VerificationKey2018({
|
||||
publicKeyBase58: bs58.encode(buffer.slice(2))
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported fingerprint "${fingerprint}".`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a KeyPair with an optional deterministic seed.
|
||||
* @example
|
||||
* > const keyPair = await Ed25519VerificationKey2018.generate();
|
||||
* > keyPair
|
||||
* Ed25519VerificationKey2018 { ...
|
||||
* @param {object} [options={}] - See LDKeyPair
|
||||
* docstring for full list.
|
||||
* @param {Uint8Array|Buffer} [options.seed] -
|
||||
* a 32-byte array seed for a deterministic key.
|
||||
*
|
||||
* @returns {Promise<Ed25519VerificationKey2018>} Generates a key pair.
|
||||
*/
|
||||
static async generate(options = {}) {
|
||||
let keyObject;
|
||||
if(options.seed) {
|
||||
keyObject = await ed25519.generateKeyPairFromSeed(options.seed);
|
||||
} else {
|
||||
keyObject = await ed25519.generateKeyPair();
|
||||
}
|
||||
return new Ed25519VerificationKey2018({
|
||||
publicKeyBase58: bs58.encode(keyObject.publicKey),
|
||||
privateKeyBase58: bs58.encode(keyObject.secretKey),
|
||||
...options
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Ed25519 Key Pair from an existing serialized key pair.
|
||||
* @example
|
||||
* > const keyPair = await Ed25519VerificationKey2018.from({
|
||||
* controller: 'did:ex:1234',
|
||||
* type: 'Ed25519VerificationKey2018',
|
||||
* publicKeyBase58,
|
||||
* privateKeyBase58
|
||||
* });
|
||||
*
|
||||
* @returns {Promise<Ed25519VerificationKey2018>} An Ed25519 Key Pair.
|
||||
*/
|
||||
static async from(options) {
|
||||
return new Ed25519VerificationKey2018(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a signer object for use with Linked Data Proofs.
|
||||
* @see https://github.com/digitalbazaar/jsonld-signatures
|
||||
* @example
|
||||
* > const signer = keyPair.signer();
|
||||
* > signer
|
||||
* { sign: [AsyncFunction: sign] }
|
||||
* > signer.sign({data});
|
||||
*
|
||||
* @returns {{sign: Function}} A signer for the json-ld block.
|
||||
*/
|
||||
signer() {
|
||||
const signer = ed25519SignerFactory(this);
|
||||
signer.id = this.id;
|
||||
return signer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a verifier object for use with signature suites.
|
||||
* @see https://github.com/digitalbazaar/jsonld-signatures
|
||||
*
|
||||
* @example
|
||||
* > const verifier = keyPair.verifier();
|
||||
* > verifier
|
||||
* { verify: [AsyncFunction: verify] }
|
||||
* > verifier.verify(key);
|
||||
*
|
||||
* @returns {{verify: Function}} Used to verify jsonld-signatures.
|
||||
*/
|
||||
verifier() {
|
||||
const verifier = ed25519VerifierFactory(this);
|
||||
verifier.id = this.id;
|
||||
return verifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports the serialized representation of the KeyPair
|
||||
* and other information that json-ld Signatures can use to form a proof.
|
||||
*
|
||||
* @param {object} [options={}] - Options hashmap.
|
||||
* @param {boolean} [options.publicKey] - Export public key material?
|
||||
* @param {boolean} [options.privateKey] - Export private key material?
|
||||
* @param {boolean} [options.includeContext] - Include JSON-LD context?
|
||||
*
|
||||
* @returns {object} A public key object
|
||||
* information used in verification methods by signatures.
|
||||
*/
|
||||
export({publicKey = false, privateKey = false, includeContext = false} = {}) {
|
||||
if(!(publicKey || privateKey)) {
|
||||
throw new TypeError(
|
||||
'Export requires specifying either "publicKey" or "privateKey".');
|
||||
}
|
||||
const exportedKey = {
|
||||
id: this.id,
|
||||
type: this.type
|
||||
};
|
||||
if(includeContext) {
|
||||
exportedKey['@context'] = Ed25519VerificationKey2018.SUITE_CONTEXT;
|
||||
}
|
||||
if(this.controller) {
|
||||
exportedKey.controller = this.controller;
|
||||
}
|
||||
if(publicKey) {
|
||||
exportedKey.publicKeyBase58 = this.publicKeyBase58;
|
||||
}
|
||||
if(privateKey) {
|
||||
exportedKey.privateKeyBase58 = this.privateKeyBase58;
|
||||
}
|
||||
if(this.revoked) {
|
||||
exportedKey.revoked = this.revoked;
|
||||
}
|
||||
return exportedKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and returns a multiformats encoded
|
||||
* ed25519 public key fingerprint (for use with cryptonyms, for example).
|
||||
* @see https://github.com/multiformats/multicodec
|
||||
*
|
||||
* @param {string} publicKeyBase58 - The base58 encoded public key material.
|
||||
*
|
||||
* @returns {string} The fingerprint.
|
||||
*/
|
||||
static fingerprintFromPublicKey({publicKeyBase58} = {}) {
|
||||
// ed25519 cryptonyms are multicodec encoded values, specifically:
|
||||
// (multicodec ed25519-pub 0xed01 + key bytes)
|
||||
const pubkeyBytes = util.base58Decode({
|
||||
decode: bs58.decode,
|
||||
keyMaterial: publicKeyBase58,
|
||||
type: 'public'
|
||||
});
|
||||
const buffer = new Uint8Array(2 + pubkeyBytes.length);
|
||||
buffer[0] = 0xed;
|
||||
buffer[1] = 0x01;
|
||||
buffer.set(pubkeyBytes, 2);
|
||||
// prefix with `z` to indicate multi-base base58btc encoding
|
||||
return `z${bs58.encode(buffer)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates and returns a multiformats encoded
|
||||
* ed25519 public key fingerprint (for use with cryptonyms, for example).
|
||||
* @see https://github.com/multiformats/multicodec
|
||||
*
|
||||
* @returns {string} The fingerprint.
|
||||
*/
|
||||
fingerprint() {
|
||||
const {publicKeyBase58} = this;
|
||||
return Ed25519VerificationKey2018
|
||||
.fingerprintFromPublicKey({publicKeyBase58});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the fingerprint was generated from a given key pair.
|
||||
* @example
|
||||
* > edKeyPair.verifyFingerprint({fingerprint: 'z2S2Q6MkaFJewa'});
|
||||
* {valid: true};
|
||||
* @param {string} fingerprint - A Base58 public key.
|
||||
*
|
||||
* @returns {object} An object indicating valid is true or false.
|
||||
*/
|
||||
verifyFingerprint({fingerprint} = {}) {
|
||||
// fingerprint should have `z` prefix indicating
|
||||
// that it's multi-base encoded
|
||||
if(!(typeof fingerprint === 'string' && fingerprint[0] === 'z')) {
|
||||
return {
|
||||
error: new Error('`fingerprint` must be a multibase encoded string.'),
|
||||
valid: false
|
||||
};
|
||||
}
|
||||
let fingerprintBuffer;
|
||||
try {
|
||||
fingerprintBuffer = util.base58Decode({
|
||||
decode: bs58.decode,
|
||||
keyMaterial: fingerprint.slice(1),
|
||||
type: `fingerprint's`
|
||||
});
|
||||
} catch(e) {
|
||||
return {error: e, valid: false};
|
||||
}
|
||||
let publicKeyBuffer;
|
||||
try {
|
||||
publicKeyBuffer = util.base58Decode({
|
||||
decode: bs58.decode,
|
||||
keyMaterial: this.publicKeyBase58,
|
||||
type: 'public'
|
||||
});
|
||||
} catch(e) {
|
||||
return {error: e, valid: false};
|
||||
}
|
||||
|
||||
const buffersEqual = _isEqualBuffer(
|
||||
publicKeyBuffer, fingerprintBuffer.slice(2));
|
||||
|
||||
// validate the first two multicodec bytes 0xed01
|
||||
const valid = fingerprintBuffer[0] === 0xed &&
|
||||
fingerprintBuffer[1] === 0x01 &&
|
||||
buffersEqual;
|
||||
if(!valid) {
|
||||
return {
|
||||
error: new Error('The fingerprint does not match the public key.'),
|
||||
valid: false
|
||||
};
|
||||
}
|
||||
return {valid};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* Returns an object with an async sign function.
|
||||
* The sign function is bound to the KeyPair
|
||||
* and then returned by the KeyPair's signer method.
|
||||
* @param {Ed25519VerificationKey2018} key - A key par instance.
|
||||
* @example
|
||||
* > const mySigner = ed25519SignerFactory(edKeyPair);
|
||||
* > await mySigner.sign({data})
|
||||
*
|
||||
* @returns {{sign: Function}} An object with an async function sign
|
||||
* using the private key passed in.
|
||||
*/
|
||||
function ed25519SignerFactory(key) {
|
||||
if(!key.privateKeyBase58) {
|
||||
return {
|
||||
async sign() {
|
||||
throw new Error('No private key to sign with.');
|
||||
}
|
||||
};
|
||||
}
|
||||
const privateKeyBytes = util.base58Decode({
|
||||
decode: bs58.decode,
|
||||
keyMaterial: key.privateKeyBase58,
|
||||
type: 'private'
|
||||
});
|
||||
return {
|
||||
async sign({data}) {
|
||||
const signature = ed25519.sign(privateKeyBytes, data);
|
||||
return signature;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @ignore
|
||||
* Returns an object with an async verify function.
|
||||
* The verify function is bound to the KeyPair
|
||||
* and then returned by the KeyPair's verifier method.
|
||||
* @param {Ed25519VerificationKey2018} key - An Ed25519VerificationKey2018.
|
||||
* @example
|
||||
* > const myVerifier = ed25519Verifier(edKeyPair);
|
||||
* > await myVerifier.verify({data, signature});
|
||||
*
|
||||
* @returns {{verify: Function}} An async verifier specific
|
||||
* to the key passed in.
|
||||
*/
|
||||
function ed25519VerifierFactory(key) {
|
||||
const publicKeyBytes = util.base58Decode({
|
||||
decode: bs58.decode,
|
||||
keyMaterial: key.publicKeyBase58,
|
||||
type: 'public'
|
||||
});
|
||||
return {
|
||||
async verify({data, signature}) {
|
||||
return ed25519.verify(publicKeyBytes, data, signature);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function _isEqualBuffer(buf1, buf2) {
|
||||
if(buf1.length !== buf2.length) {
|
||||
return false;
|
||||
}
|
||||
for(let i = 0; i < buf1.length; i++) {
|
||||
if(buf1[i] !== buf2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// Used by CryptoLD harness for dispatching.
|
||||
Ed25519VerificationKey2018.suite = SUITE_ID;
|
||||
// Used by CryptoLD harness's fromKeyId() method.
|
||||
Ed25519VerificationKey2018.SUITE_CONTEXT =
|
||||
'https://w3id.org/security/suites/ed25519-2018/v1';
|
||||
|
||||
export {
|
||||
Ed25519VerificationKey2018
|
||||
};
|
||||
143
lib/jsonld-signatures/suites/ed255192018/ed25519.ts
Normal file
143
lib/jsonld-signatures/suites/ed255192018/ed25519.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
/*!
|
||||
* Copyright (c) 2020 Digital Bazaar, Inc. All rights reserved.
|
||||
*/
|
||||
import { Buffer } from 'buffer';
|
||||
|
||||
// FIXME: Some methods is missing from crypto-js.
|
||||
import {
|
||||
sign,
|
||||
verify,
|
||||
createPrivateKey,
|
||||
createPublicKey,
|
||||
randomBytes
|
||||
} from 'crypto-js';
|
||||
|
||||
// used to export node's public keys to buffers
|
||||
const publicKeyEncoding = {format: 'der', type: 'spki'};
|
||||
// used to turn private key bytes into a buffer in DER format
|
||||
const DER_PRIVATE_KEY_PREFIX = Buffer.from(
|
||||
'302e020100300506032b657004220420', 'hex');
|
||||
// used to turn public key bytes into a buffer in DER format
|
||||
const DER_PUBLIC_KEY_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
||||
|
||||
const api = {
|
||||
/**
|
||||
* Generates a key using a 32 byte Uint8Array.
|
||||
*
|
||||
* @param {Uint8Array} seedBytes - The bytes for the private key.
|
||||
*
|
||||
* @returns {object} The object with the public and private key material.
|
||||
*/
|
||||
async generateKeyPairFromSeed(seedBytes) {
|
||||
const privateKey = await createPrivateKey({
|
||||
// node is more than happy to create a new private key using a DER
|
||||
key: _privateKeyDerEncode({seedBytes}),
|
||||
format: 'der',
|
||||
type: 'pkcs8'
|
||||
});
|
||||
// this expects either a PEM encoded key or a node privateKeyObject
|
||||
const publicKey = await createPublicKey(privateKey);
|
||||
const publicKeyBuffer = publicKey.export(publicKeyEncoding);
|
||||
const publicKeyBytes = getKeyMaterial(publicKeyBuffer);
|
||||
return {
|
||||
publicKey: publicKeyBytes,
|
||||
secretKey: Buffer.concat([seedBytes, publicKeyBytes])
|
||||
};
|
||||
},
|
||||
// generates an ed25519 key using a random seed
|
||||
async generateKeyPair() {
|
||||
const seed = randomBytes(32);
|
||||
return api.generateKeyPairFromSeed(seed);
|
||||
},
|
||||
async sign(privateKeyBytes, data) {
|
||||
const privateKey = await createPrivateKey({
|
||||
key: _privateKeyDerEncode({privateKeyBytes}),
|
||||
format: 'der',
|
||||
type: 'pkcs8'
|
||||
});
|
||||
return sign(null, data, privateKey);
|
||||
},
|
||||
async verify(publicKeyBytes, data, signature) {
|
||||
const publicKey = await createPublicKey({
|
||||
key: _publicKeyDerEncode({publicKeyBytes}),
|
||||
format: 'der',
|
||||
type: 'spki'
|
||||
});
|
||||
return verify(null, data, publicKey, signature);
|
||||
}
|
||||
};
|
||||
|
||||
export default api;
|
||||
|
||||
/**
|
||||
* The key material is the part of the buffer after the DER Prefix.
|
||||
*
|
||||
* @param {Buffer} buffer - A DER encoded key buffer.
|
||||
*
|
||||
* @throws {Error} If the buffer does not contain a valid DER Prefix.
|
||||
*
|
||||
* @returns {Buffer} The key material part of the Buffer.
|
||||
*/
|
||||
function getKeyMaterial(buffer) {
|
||||
if(buffer.indexOf(DER_PUBLIC_KEY_PREFIX) === 0) {
|
||||
return buffer.slice(DER_PUBLIC_KEY_PREFIX.length, buffer.length);
|
||||
}
|
||||
if(buffer.indexOf(DER_PRIVATE_KEY_PREFIX) === 0) {
|
||||
return buffer.slice(DER_PRIVATE_KEY_PREFIX.length, buffer.length);
|
||||
}
|
||||
throw new Error('Expected Buffer to match Ed25519 Public or Private Prefix');
|
||||
}
|
||||
/**
|
||||
* Takes a Buffer or Uint8Array with the raw private key and encodes it
|
||||
* in DER-encoded PKCS#8 format.
|
||||
* Allows Uint8Arrays to be interoperable with node's crypto functions.
|
||||
*
|
||||
* @param {object} options - Options to use.
|
||||
* @param {Buffer} [options.privateKeyBytes] - Required if no seedBytes.
|
||||
* @param {Buffer} [options.seedBytes] - Required if no privateKeyBytes.
|
||||
*
|
||||
* @throws {TypeError} Throws if the supplied buffer is not of the right size
|
||||
* or not a Uint8Array or Buffer.
|
||||
*
|
||||
* @returns {Buffer} DER private key prefix + key bytes.
|
||||
*/
|
||||
export function _privateKeyDerEncode({privateKeyBytes, seedBytes}) {
|
||||
if(!(privateKeyBytes || seedBytes)) {
|
||||
throw new TypeError('`privateKeyBytes` or `seedBytes` is required.');
|
||||
}
|
||||
if(!privateKeyBytes && !(seedBytes instanceof Uint8Array &&
|
||||
seedBytes.length === 32)) {
|
||||
throw new TypeError('`seedBytes` must be a 32 byte Buffer.');
|
||||
}
|
||||
if(!seedBytes && !(privateKeyBytes instanceof Uint8Array &&
|
||||
privateKeyBytes.length === 64)) {
|
||||
throw new TypeError('`privateKeyBytes` must be a 64 byte Buffer.');
|
||||
}
|
||||
let p;
|
||||
if(seedBytes) {
|
||||
p = seedBytes;
|
||||
} else {
|
||||
// extract the first 32 bytes of the 64 byte private key representation
|
||||
p = privateKeyBytes.slice(0, 32);
|
||||
}
|
||||
return Buffer.concat([DER_PRIVATE_KEY_PREFIX, p]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a Uint8Array of public key bytes and encodes it in DER-encoded
|
||||
* SubjectPublicKeyInfo (SPKI) format.
|
||||
* Allows Uint8Arrays to be interoperable with node's crypto functions.
|
||||
*
|
||||
* @param {object} options - Options to use.
|
||||
* @param {Uint8Array} options.publicKeyBytes - The keyBytes.
|
||||
*
|
||||
* @throws {TypeError} Throws if the bytes are not Uint8Array or of length 32.
|
||||
*
|
||||
* @returns {Buffer} DER Public key Prefix + key bytes.
|
||||
*/
|
||||
export function _publicKeyDerEncode({publicKeyBytes}) {
|
||||
if(!(publicKeyBytes instanceof Uint8Array && publicKeyBytes.length === 32)) {
|
||||
throw new TypeError('`publicKeyBytes` must be a 32 byte Buffer.');
|
||||
}
|
||||
return Buffer.concat([DER_PUBLIC_KEY_PREFIX, publicKeyBytes]);
|
||||
}
|
||||
38
lib/jsonld-signatures/suites/ed255192018/util.ts
Normal file
38
lib/jsonld-signatures/suites/ed255192018/util.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (c) 2018-2020 Digital Bazaar, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Wraps Base58 decoding operations in
|
||||
* order to provide consistent error messages.
|
||||
* @ignore
|
||||
* @example
|
||||
* > const pubkeyBytes = _base58Decode({
|
||||
* decode: base58.decode,
|
||||
* keyMaterial: this.publicKeyBase58,
|
||||
* type: 'public'
|
||||
* });
|
||||
* @param {object} options - The decoder options.
|
||||
* @param {Function} options.decode - The decode function to use.
|
||||
* @param {string} options.keyMaterial - The Base58 encoded
|
||||
* key material to decode.
|
||||
* @param {string} options.type - A description of the
|
||||
* key material that will be included
|
||||
* in an error message (e.g. 'public', 'private').
|
||||
*
|
||||
* @returns {object} - The decoded bytes. The data structure for the bytes is
|
||||
* determined by the provided decode function.
|
||||
*/
|
||||
export function base58Decode({decode, keyMaterial, type}) {
|
||||
let bytes;
|
||||
try {
|
||||
bytes = decode(keyMaterial);
|
||||
} catch(e) {
|
||||
// do nothing
|
||||
// this helper throws when no result is produced
|
||||
}
|
||||
if(bytes === undefined) {
|
||||
throw new TypeError(`The ${type} key material must be Base58 encoded.`);
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
57
lib/jsonld-signatures/suites/rsa2018/RsaSignature2018.ts
Normal file
57
lib/jsonld-signatures/suites/rsa2018/RsaSignature2018.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/*!s
|
||||
* Copyright (c) 2017-2018 Digital Bazaar, Inc. All rights reserved.
|
||||
*/
|
||||
|
||||
// import { RSAKeyPair } from 'crypto-ld';
|
||||
import { RsaVerificationKey2018 } from '@digitalbazaar/rsa-verification-key-2018/lib/RsaVerificationKey2018';
|
||||
import { JwsLinkedDataSignature } from '../JwsLinkedDataSignature';
|
||||
|
||||
export interface RsaSignature2018 {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class RsaSignature2018 extends JwsLinkedDataSignature {
|
||||
/**
|
||||
* @param type {string} Provided by subclass.
|
||||
*
|
||||
* One of these parameters is required to use a suite for signing:
|
||||
*
|
||||
* @param [creator] {string} A key id URL to the paired public key.
|
||||
* @param [verificationMethod] {string} A key id URL to the paired public key.
|
||||
*
|
||||
* This parameter is required for signing:
|
||||
*
|
||||
* @param [signer] {function} an optional signer.
|
||||
*
|
||||
* Advanced optional parameters and overrides:
|
||||
*
|
||||
* @param [proof] {object} a JSON-LD document with options to use for
|
||||
* the `proof` node (e.g. any other custom fields can be provided here
|
||||
* using a context different from security-v2).
|
||||
* @param [date] {string|Date} signing date to use if not passed.
|
||||
* @param [key] {LDKeyPair} an optional crypto-ld KeyPair.
|
||||
* @param [useNativeCanonize] {boolean} true to use a native canonize
|
||||
* algorithm.
|
||||
*/
|
||||
constructor({
|
||||
signer,
|
||||
key,
|
||||
verificationMethod,
|
||||
proof,
|
||||
date,
|
||||
useNativeCanonize,
|
||||
}: any = {}) {
|
||||
super({
|
||||
type: 'RsaSignature2018',
|
||||
alg: 'PS256',
|
||||
LDKeyClass: RsaVerificationKey2018,
|
||||
verificationMethod,
|
||||
signer,
|
||||
key,
|
||||
proof,
|
||||
date,
|
||||
useNativeCanonize,
|
||||
});
|
||||
this.requiredKeyType = 'RsaVerificationKey2018';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user