feat: add VC verification

This commit is contained in:
pmigueld
2022-02-18 18:06:05 +08:00
parent ff348f4b47
commit 3743b12b5c
23 changed files with 4955 additions and 823 deletions

View 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});
}
}

View 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};
}
}
}

View 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;
}
};

View 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;
}
}

View 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));
}

View 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;
}

View 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;
}

View File

@@ -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';
}
}

View File

@@ -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
};

View 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]);
}

View 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;
}

View 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';
}
}