diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7a0d85f..6a6bc6e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,7 +24,7 @@ jobs: timeout-minutes: 10 strategy: matrix: - node-version: [14.x, 16.x, 18.x] + node-version: [16.x, 18.x] steps: - uses: actions/checkout@v2 - name: Use Node.js ${{ matrix.node-version }} diff --git a/CHANGELOG.md b/CHANGELOG.md index e96259d..d283203 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # did:key driver ChangeLog +## 4.0.0 - + +### Added +- **BREAKING**: Added validators for didDocument creation. +- Added a `DidKeyDriver` options parameter to `didKeyDriver.{get, publicKeyToDidDoc, generate}`. +- Added option `publicKeyFormat` to `DidKeyDriver` options. +- Added option `enableEncryptionKeyDerivation` to `DidKeyDriver` options. +- Added option `enableExperimentalPublicKeyTypes` to `DidKeyDriver` options. +- Added option `defaultContext` to `DidKeyDriver` options. + +### Changed +- **BREAKING**: `DidKeyDriver` now accepts a Map of `verificationMethods` in the constructor. + +### Removed +- **BREAKING**: `DidKeyDriver` no longer takes a `verificationSuite` in the constructor. + ## 3.0.0 - 2022-06-02 ### Changed diff --git a/README.md b/README.md index 76f8602..d0b149c 100644 --- a/README.md +++ b/README.md @@ -184,9 +184,31 @@ To get a DID Document for an existing `did:key` DID: const did = 'did:key:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T'; const didDocument = await didKeyDriver.get({did}); ``` - (Results in the [example DID Doc](#example-did-document) above). +### Options for `get`, `publicKeyToDidDoc`, and `generate` + +`get`, `publicKeyToDidDoc`, and `generate` both take an options object with the following options: + +```js +const options = { + // default publicKeyFormat for the keys in the didDocument + publicKeyFormat: 'Ed25519VerificationKey2020', + // enableExperimentalPublicKeyTypes defaults to false. Setting it to true enables + // the use of key types that are not Multikey, JsonWebKey2020, or Ed25519VerificationKey2020. + enableExperimentalPublicKeyTypes: false, + // the context for the resulting did document + // the default is just the did context + defaultContext: [DID_CONTEXT_URL], + // if false no keyAgreementKey is included + // defaults to true + enableEncryptionKeyDerivation: true +}; + +const did = 'did:key:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T'; +const didDoc = await didKeyDriver.get({did, options}); +``` + #### Getting just the key object by key id You can also use a `.get()` to retrieve an individual key, if you know it's id @@ -254,17 +276,17 @@ If you need DID Documents that are using the 2018/2019 crypto suites, you can customize the driver as follows. ```js -import { - Ed25519VerificationKey2018 -} from '@digitalbazaar/ed25519-verification-key-2018'; import * as didKey from '@digitalbazaar/did-method-key'; -const didKeyDriver2018 = didKey.driver({ - verificationSuite: Ed25519VerificationKey2018 -}); +const didKeyDriver = didKey.driver(); const did = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; -await didKeyDriver2018.get({did}); +const options = { + publicKeyFormat: 'Ed25519VerificationKey2018', + // this defaults to false + enableExperimentalPublicKeyTypes: true +}; +await didKeyDriver.get({did, options}); // -> { '@context': [ diff --git a/lib/DidKeyDriver.js b/lib/DidKeyDriver.js index 7e25886..194a32e 100644 --- a/lib/DidKeyDriver.js +++ b/lib/DidKeyDriver.js @@ -1,64 +1,59 @@ /*! * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved. */ -import { - Ed25519VerificationKey2020 -} from '@digitalbazaar/ed25519-verification-key-2020'; -import { - X25519KeyAgreementKey2020 -} from '@digitalbazaar/x25519-key-agreement-key-2020'; -import { - X25519KeyAgreementKey2019 -} from '@digitalbazaar/x25519-key-agreement-key-2019'; - import * as didIo from '@digitalbazaar/did-io'; - +import { + parseDidKey, + validateDidDocument, + validateDidKey, + validatePublicKeyFormat +} from './validators.js'; +import {DidResolverError} from '@digitalbazaar/did-io'; const DID_CONTEXT_URL = 'https://www.w3.org/ns/did/v1'; -// For backwards compat only, not actually importing this suite -const ED25519_KEY_2018_CONTEXT_URL = - 'https://w3id.org/security/suites/ed25519-2018/v1'; - -const contextsBySuite = new Map([ - [Ed25519VerificationKey2020.suite, Ed25519VerificationKey2020.SUITE_CONTEXT], - ['Ed25519VerificationKey2018', ED25519_KEY_2018_CONTEXT_URL], - [X25519KeyAgreementKey2020.suite, X25519KeyAgreementKey2020.SUITE_CONTEXT], - [X25519KeyAgreementKey2019.suite, X25519KeyAgreementKey2019.SUITE_CONTEXT] -]); +import { + contextsBySuite, + getEncryptionMethod, + methodsByKeyFormat +} from './verificationMethods.js'; export class DidKeyDriver { /** - * @param {object} options - Options hashmap. - * @param {object} [options.verificationSuite=Ed25519VerificationKey2020] - - * Key suite for the signature verification key suite to use. + * @param {object} options - Options to use. + * @param {Map} [options.verificationMethods] - + * A map of verification methods with the key as the publicKeyFormat. */ - constructor({verificationSuite = Ed25519VerificationKey2020} = {}) { + constructor({ + verificationMethods = methodsByKeyFormat, + } = {}) { // used by did-io to register drivers this.method = 'key'; - this.verificationSuite = verificationSuite; + this.verificationMethods = verificationMethods; } /** * Generates a new `did:key` method DID Document (optionally, from a * deterministic seed value). * - * @param {object} options - Options hashmap. + * @param {object} options - Options object. * @param {Uint8Array} [options.seed] - A 32-byte array seed for a * deterministic key. + * @param {object} [options.options] - DID Document creation options. * * @returns {Promise<{didDocument: object, keyPairs: Map, * methodFor: Function}>} Resolves with the generated DID Document, along * with the corresponding key pairs used to generate it (for storage in a * KMS). */ - async generate({seed} = {}) { + async generate({seed, options = {}} = {}) { // Public/private key pair of the main did:key signing/verification key - const verificationKeyPair = await this.verificationSuite.generate({seed}); - + const verificationKeyPair = await this._getVerificationMethod(options). + generate({seed}); // keyPairs is a map of keyId to key pair instance, that includes // the verificationKeyPair above, but also the keyAgreementKey pair that // is derived from the verification key pair. const {didDocument, keyPairs} = await this._keyPairToDidDocument({ - keyPair: verificationKeyPair + keyPair: verificationKeyPair, + options }); // Convenience function that returns the public/private key pair instance @@ -86,7 +81,7 @@ export class DidKeyDriver { * const authPublicKey = await cryptoLd.from(authKeyData); * const {verify} = authPublicKey.verifier(); * - * @param {object} options - Options hashmap. + * @param {object} options - Options object. * @param {object} options.didDocument - DID Document (retrieved via a * `.get()` or from some other source). * @param {string} options.purpose - Verification method purpose, such as @@ -119,27 +114,29 @@ export class DidKeyDriver { * await resolver.get({did}); // -> did document * await resolver.get({url: keyId}); // -> public key node * - * @param {object} options - Options hashmap. + * @param {object} options - Options object. * @param {string} [options.did] - DID URL or a key id (either an ed25519 key * or an x25519 key-agreement key id). * @param {string} [options.url] - Alias for the `did` url param, supported * for better readability of invoking code. + * @param {object} [options.options] - Options for didDocument creation. * * @returns {Promise} Resolves to a DID Document or a * public key node with context. */ - async get({did, url} = {}) { + async get({did, url, options} = {}) { did = did || url; if(!did) { throw new TypeError('"did" must be a string.'); } + validateDidKey({did}); const [didAuthority, keyIdFragment] = did.split('#'); + const {multibase: fingerprint} = parseDidKey({did: didAuthority}); + const keyPair = this._getVerificationMethod(options). + fromFingerprint({fingerprint}); - const fingerprint = didAuthority.substr('did:key:'.length); - const keyPair = this.verificationSuite.fromFingerprint({fingerprint}); - - const {didDocument} = await this._keyPairToDidDocument({keyPair}); + const {didDocument} = await this._keyPairToDidDocument({keyPair, options}); if(keyIdFragment) { // resolve an individual key @@ -155,18 +152,20 @@ export class DidKeyDriver { * Note that unlike `generate()`, a `keyPairs` map is not returned. Use * `publicMethodFor()` to fetch keys for particular proof purposes. * - * @param {object} options - Options hashmap. + * @param {object} options - Options object. * @typedef LDKeyPair * @param {LDKeyPair|object} options.publicKeyDescription - Public key object * used to generate the DID document (either an LDKeyPair instance * containing public key material, or a "key description" plain object * (such as that generated from a KMS)). + * @param {object} [options.options] - Options for didDocument creation. * * @returns {Promise} Resolves with the generated DID Document. */ - async publicKeyToDidDoc({publicKeyDescription} = {}) { + async publicKeyToDidDoc({publicKeyDescription, options} = {}) { const {didDocument} = await this._keyPairToDidDocument({ - keyPair: publicKeyDescription + keyPair: publicKeyDescription, + options }); return {didDocument}; } @@ -174,48 +173,34 @@ export class DidKeyDriver { /** * Converts an Ed25519KeyPair object to a `did:key` method DID Document. * - * @param {object} options - Options hashmap. + * @param {object} options - Options object. * @param {LDKeyPair|object} options.keyPair - Key used to generate the DID * document (either an LDKeyPair instance containing public key material, * or a "key description" plain object (such as that generated from a KMS)). + * @param {object} [options.options = {}] - Options for didDocument creation. * * @returns {Promise<{didDocument: object, keyPairs: Map}>} * Resolves with the generated DID Document, along with the corresponding * key pairs used to generate it (for storage in a KMS). */ - async _keyPairToDidDocument({keyPair} = {}) { - const verificationKeyPair = await this.verificationSuite.from({...keyPair}); + async _keyPairToDidDocument({keyPair, options = {}} = {}) { + const { + publicKeyFormat = 'Ed25519VerificationKey2020', + enableExperimentalPublicKeyTypes = false, + defaultContext = [DID_CONTEXT_URL], + enableEncryptionKeyDerivation = true + } = options; + const verificationKeyPair = await this._getVerificationMethod( + {publicKeyFormat}).from({...keyPair}); const did = `did:key:${verificationKeyPair.fingerprint()}`; verificationKeyPair.controller = did; - const contexts = [DID_CONTEXT_URL]; - - // The KAK pair will use the source key's controller, but will generate - // its own .id - let keyAgreementKeyPair; - if(verificationKeyPair.type === 'Ed25519VerificationKey2020') { - keyAgreementKeyPair = X25519KeyAgreementKey2020 - .fromEd25519VerificationKey2020({keyPair: verificationKeyPair}); - contexts.push(Ed25519VerificationKey2020.SUITE_CONTEXT, - X25519KeyAgreementKey2020.SUITE_CONTEXT); - } else if(verificationKeyPair.type === 'Ed25519VerificationKey2018') { - keyAgreementKeyPair = X25519KeyAgreementKey2019 - .fromEd25519VerificationKey2018({keyPair: verificationKeyPair}); - contexts.push(ED25519_KEY_2018_CONTEXT_URL, - X25519KeyAgreementKey2019.SUITE_CONTEXT); - } else { - throw new Error( - 'Cannot derive key agreement key from verification key type "' + - verificationKeyPair.type + '".' - ); - } - + const contexts = [...defaultContext]; // Now set the source key's id verificationKeyPair.id = `${did}#${verificationKeyPair.fingerprint()}`; // get the public components of each keypair const publicEdKey = verificationKeyPair.export({publicKey: true}); - const publicDhKey = keyAgreementKeyPair.export({publicKey: true}); // Compose the DID Document const didDocument = { @@ -227,22 +212,54 @@ export class DidKeyDriver { authentication: [publicEdKey.id], assertionMethod: [publicEdKey.id], capabilityDelegation: [publicEdKey.id], - capabilityInvocation: [publicEdKey.id], - keyAgreement: [publicDhKey] + capabilityInvocation: [publicEdKey.id] }; - // create the key pairs map const keyPairs = new Map(); keyPairs.set(verificationKeyPair.id, verificationKeyPair); - keyPairs.set(keyAgreementKeyPair.id, keyAgreementKeyPair); + + // don't include an encryption verification method unless the option is true + if(enableEncryptionKeyDerivation) { + // The KAK pair will use the source key's controller, but will generate + // its own .id + const keyAgreementKeyPair = getEncryptionMethod({ + verificationKeyPair, + contexts + }); + keyPairs.set(keyAgreementKeyPair.id, keyAgreementKeyPair); + const publicDhKey = keyAgreementKeyPair.export({publicKey: true}); + didDocument.keyAgreement = [publicDhKey]; + } + validateDidDocument({ + didDocument, + didOptions: { + enableExperimentalPublicKeyTypes, + enableEncryptionKeyDerivation + } + }); return {didDocument, keyPairs}; } - + _getVerificationMethod({ + publicKeyFormat = 'Ed25519VerificationKey2020' + } = {}) { + // ensure public key format is a format we have an implementation of + validatePublicKeyFormat({publicKeyFormat}); + const verificationMethod = this.verificationMethods.get(publicKeyFormat); + // if there is no verificationMethod then the method is not supported by + // the `did:key` spec + if(!verificationMethod) { + throw new DidResolverError({ + message: `Unsupported public key type ${publicKeyFormat}`, + code: 'unsupportedPublicKeyType' + }); + } + return verificationMethod; + } /** * Computes and returns the id of a given key pair. Used by `did-io` drivers. * - * @param {object} options - Options hashmap. + * @param {object} options - Options object. * @param {LDKeyPair} options.keyPair - The key pair used when computing the * identifier. * @@ -256,7 +273,7 @@ export class DidKeyDriver { /** * Returns the public key object for a given key id fragment. * - * @param {object} options - Options hashmap. + * @param {object} options - Options object. * @param {object} options.didDocument - The DID Document to use when generating * the id. * @param {string} options.keyIdFragment - The key identifier fragment. diff --git a/lib/index.js b/lib/index.js index 44ca644..35b0d74 100644 --- a/lib/index.js +++ b/lib/index.js @@ -7,14 +7,14 @@ import {DidKeyDriver} from './DidKeyDriver.js'; /** * Helper method to match the `.driver()` API of other `did-io` plugins. * - * @param {object} options - Options hashmap. - * @param {object} [options.verificationSuite=Ed25519VerificationKey2020] - - * Key suite for the signature verification key suite to use. + * @param {object} options - Options object. + * @param {Map} [options.verificationMethods] - + * A map of verification methods with the key as the publicKeyFormat. * * @returns {DidKeyDriver} Returns an instance of a did:key resolver driver. */ -function driver({verificationSuite} = {}) { - return new DidKeyDriver({verificationSuite}); +function driver({verificationMethods} = {}) { + return new DidKeyDriver({verificationMethods}); } export {driver, DidKeyDriver}; diff --git a/lib/validators.js b/lib/validators.js new file mode 100644 index 0000000..2997a84 --- /dev/null +++ b/lib/validators.js @@ -0,0 +1,176 @@ +/*! + * Copyright (c) 2021-2022 Digital Bazaar, Inc. All rights reserved. + */ +import {DidResolverError} from '@digitalbazaar/did-io'; + +/** + * Throws if the publicKeyFormat is in a format that this library + * does not have an implementation for. + * + * @param {object} options - Options to use. + * @param {string} options.publicKeyFormat - The publicKeyFormat. + * + * @throws {DidResolverError} If there is no supporting library + * for the public key format. + * + * @returns {undefined} Returns on success. + */ +export function validatePublicKeyFormat({publicKeyFormat}) { + // FIXME add JsonWebKey2020 support + // FIXME add Multikey support once Multikey spec and context are finished + const notSupported = new Set(['Multikey', 'JsonWebKey2020']); + if(notSupported.has(publicKeyFormat)) { + throw new DidResolverError({ + message: `Representation NotSupported ${publicKeyFormat}`, + code: 'representationNotSupported' + }); + } +} + +/** + * General validation for did:keys independent + * of key type specific validation. + * + * @param {object} options - Options to use. + * @param {string} options.did - A did:key. + * + * @throws {DidResolverError} Throws general did:key errors. + * + * @returns {undefined} If the didKeyComponents are valid. + */ +export function validateDidKey({did}) { + // the parse step will throw if the did doesn't follow the format: + // did:key or the multibase value doesn't start with z + const {version} = parseDidKey({did}); + // so we just need to validate that the version + // is convertible to a positive integer + _validateVersion({version}); +} + +/** + * Public did:keys can be represented in multiple formats. + * While we don't do any conversion in this library we still make + * the check. + * + * @param {object} options - Options to use. + * @param {object} options.didOptions - The didOptions from searchParams + * and headers. + * @param {string} options.didOptions.enableExperimentalPublicKeyTypes - An + * option that can be passed in to allow experimental key types. + * @param {boolean} options.didOptions.enableEncryptionKeyDerivation - If + * to add the encryption key to the didDocument. + * @param {object} options.didDocument - The didDocument requred by the did + * or didUrl. + * + * @throws {Error} Throws UnsupportedPublicKeyType or InvalidPublicKeyType. + * + * @returns {undefined} Returns on sucess. + */ +export function validateDidDocument({ + didOptions: { + enableExperimentalPublicKeyTypes, + enableEncryptionKeyDerivation + }, + didDocument, +}) { + // all of the other did methods so far are signature verification + if(!enableExperimentalPublicKeyTypes) { + const verificationFormats = ['Multikey', 'JsonWebKey2020']; + if(enableEncryptionKeyDerivation) { + _validateEncryptionMethod({ + method: didDocument.keyAgreement, + verificationFormats + }); + } + _validateSignatureMethod({ + method: didDocument.verificationMethod, + verificationFormats + }); + } +} + +/** + * A version must be convertible to a positive integer. + * + * @param {object} options - Options to use. + * @param {string|number} options.version - A did:key:version. + * + * @throws {Error} Throws InvalidDid. + * + * @returns {undefined} Returns on success. + */ +function _validateVersion({version}) { + const versionNumber = Number.parseInt(version); + if(Number.isNaN(versionNumber)) { + throw new DidResolverError({ + message: 'Version must be a positive integer received ' + + `"${version}"`, + code: 'invalidDid' + }); + } + if(versionNumber <= 0) { + throw new DidResolverError({ + message: 'Version must be a positive integer received ' + + `"${version}"`, + code: 'invalidDid' + }); + } +} + +export function parseDidKey({did}) { + const pchar = '[a-zA-Z0-9\\-\\._~]|%[0-9a-fA-F]{2}|[!$&\'()*+,;=:@]'; + const didKeyPattern = '^(?did):(?key)' + + `(:(?\\d+))?:(?z(${pchar})+)`; + const match = new RegExp(didKeyPattern).exec(did); + if(!match) { + throw new DidResolverError({ + message: `Invalid DID ${did}`, + code: 'invalidDid' + }); + } + const { + groups: { + scheme, + method, + version = '', + multibase + } + } = match; + return { + scheme, + method, + version: version.length === 0 ? '1' : version, + multibase + }; +} + +function _validateEncryptionMethod({method, verificationFormats}) { + //keyAgreement is an encryption verification method + const encryptionFormats = [ + ...verificationFormats, + 'X25519KeyAgreementKey2020' + ]; + for(const {type} of method) { + if(!encryptionFormats.includes(type)) { + throw new DidResolverError({ + message: `Invalid Public Key Type ${type}`, + code: 'invalidPublicKeyType' + }); + } + } +} + +function _validateSignatureMethod({method, verificationFormats}) { + const signatureFormats = [ + ...verificationFormats, + 'Ed25519VerificationKey2020' + ]; + for(const {type} of method) { + if(!signatureFormats.includes(type)) { + throw new DidResolverError({ + message: `Invalid Public Key Type ${method.type}`, + code: 'invalidPublicKeyType' + }); + } + } +} diff --git a/lib/verificationMethods.js b/lib/verificationMethods.js new file mode 100644 index 0000000..2211ca7 --- /dev/null +++ b/lib/verificationMethods.js @@ -0,0 +1,66 @@ +/*! + * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved. + */ + +import { + Ed25519VerificationKey2018 +} from '@digitalbazaar/ed25519-verification-key-2018'; +import { + Ed25519VerificationKey2020 +} from '@digitalbazaar/ed25519-verification-key-2020'; +import { + X25519KeyAgreementKey2019 +} from '@digitalbazaar/x25519-key-agreement-key-2019'; +import { + X25519KeyAgreementKey2020 +} from '@digitalbazaar/x25519-key-agreement-key-2020'; + +// For backwards compat only, not actually importing this suite +const ED25519_KEY_2018_CONTEXT_URL = + 'https://w3id.org/security/suites/ed25519-2018/v1'; + +export const contextsBySuite = new Map([ + [Ed25519VerificationKey2020.suite, Ed25519VerificationKey2020.SUITE_CONTEXT], + ['Ed25519VerificationKey2018', ED25519_KEY_2018_CONTEXT_URL], + [X25519KeyAgreementKey2020.suite, X25519KeyAgreementKey2020.SUITE_CONTEXT], + [X25519KeyAgreementKey2019.suite, X25519KeyAgreementKey2019.SUITE_CONTEXT] +]); + +export const methodsByKeyFormat = new Map([ + ['Ed25519VerificationKey2018', Ed25519VerificationKey2018], + ['Ed25519VerificationKey2020', Ed25519VerificationKey2020], + ['X25519KeyAgreementKey2019', X25519KeyAgreementKey2019], + ['X25519KeyAgreementKey2020', X25519KeyAgreementKey2020], + // this is an exception where we convert an ed key to json web key + ['JsonWebKey2020', Ed25519VerificationKey2020] +]); + +/** + * Gets the encryption method (which should be the keyAgreementKey). + * + * @param {object} options - Options to use. + * @param {object} options.verificationKeyPair - The verification key Pair. + * @param {Array} options.contexts - The contexts for the did + * document and keys. + * + * @returns {object} The encryption key. + */ +export function getEncryptionMethod({verificationKeyPair, contexts}) { + if(verificationKeyPair.type === 'Ed25519VerificationKey2020') { + contexts.push(Ed25519VerificationKey2020.SUITE_CONTEXT, + X25519KeyAgreementKey2020.SUITE_CONTEXT); + return X25519KeyAgreementKey2020 + .fromEd25519VerificationKey2020({keyPair: verificationKeyPair}); + } else if(verificationKeyPair.type === 'Ed25519VerificationKey2018') { + contexts.push(ED25519_KEY_2018_CONTEXT_URL, + X25519KeyAgreementKey2019.SUITE_CONTEXT); + return X25519KeyAgreementKey2019 + .fromEd25519VerificationKey2018({keyPair: verificationKeyPair}); + } else { + throw new Error( + 'Cannot derive key agreement key from verification key type "' + + verificationKeyPair.type + '".' + ); + } +} + diff --git a/package.json b/package.json index e454596..749b25b 100644 --- a/package.json +++ b/package.json @@ -23,18 +23,18 @@ "lib/**/*.js" ], "dependencies": { - "@digitalbazaar/did-io": "^2.0.0", - "@digitalbazaar/ed25519-verification-key-2020": "^4.0.0", + "@digitalbazaar/did-io": "digitalbazaar/did-io#add-did-key-spec-7-validators", + "@digitalbazaar/ed25519-verification-key-2018": "^4.0.0", + "@digitalbazaar/ed25519-verification-key-2020": "^4.1.0", "@digitalbazaar/x25519-key-agreement-key-2019": "^6.0.0", "@digitalbazaar/x25519-key-agreement-key-2020": "^3.0.0" }, "devDependencies": { - "@digitalbazaar/ed25519-verification-key-2018": "^4.0.0", "c8": "^7.11.3", "chai": "^4.3.6", "cross-env": "^7.0.3", "eslint": "^8.16.0", - "eslint-config-digitalbazaar": "^3.0.0", + "eslint-config-digitalbazaar": "^4.1.0", "eslint-plugin-jsdoc": "^39.3.2", "eslint-plugin-unicorn": "^42.0.0", "karma": "^6.3.20", @@ -66,7 +66,7 @@ ], "scripts": { "test": "npm run test-node", - "test-node": "cross-env NODE_ENV=test mocha --preserve-symlinks -t 10000 test/*.spec.js", + "test-node": "cross-env NODE_ENV=test mocha --preserve-symlinks --full-trace --check-leaks -t 10000 test/*.spec.js", "test-karma": "karma start karma.conf.cjs", "coverage": "cross-env NODE_ENV=test c8 npm run test-node", "coverage-ci": "cross-env NODE_ENV=test c8 --reporter=lcovonly --reporter=text-summary --reporter=text npm run test-node", diff --git a/test/driver.spec.js b/test/driver.spec.js index 9078324..c424b06 100644 --- a/test/driver.spec.js +++ b/test/driver.spec.js @@ -2,15 +2,14 @@ * Copyright (c) 2019-20201 Digital Bazaar, Inc. All rights reserved. */ import chai from 'chai'; -chai.should(); -const {expect} = chai; - +import {DidResolverError} from '@digitalbazaar/did-io'; +import {driver} from '../lib/index.js'; import {Ed25519VerificationKey2020} from '@digitalbazaar/ed25519-verification-key-2020'; -import {Ed25519VerificationKey2018} from - '@digitalbazaar/ed25519-verification-key-2018'; -import {driver} from '../lib/index.js'; +import {noKaKDidDoc} from './expected-data.js'; +chai.should(); +const {expect} = chai; const didKeyDriver = driver(); // eslint-disable-next-line max-len @@ -55,14 +54,205 @@ describe('did:key method driver', () => { expect(kak.publicKeyMultibase).to .equal('z6LSotGbgPCJD2Y6TSvvgxERLTfVZxCh9KSrez3WNrNp7vKW'); }); - - it('should get the DID Doc in 2018 mode', async () => { - const didKeyDriver2018 = driver({ - verificationSuite: Ed25519VerificationKey2018 + it('should throw invalidDid if scheme is not did', async () => { + const did = 'notdid:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; + let error; + let didDocument; + try { + didDocument = await didKeyDriver.get({did}); + } catch(e) { + error = e; + } + expect( + didDocument, + 'Expected driver to throw not return a didDocument' + ).to.not.exist; + expect(error).to.exist; + expect(error).to.be.an.instanceof( + DidResolverError, + 'Expected a DidResolverError' + ); + expect(error.code).to.equal('invalidDid'); + }); + it('should throw invalidDid if method is not key', async () => { + const did = 'did:notkey:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; + let error; + let didDocument; + try { + didDocument = await didKeyDriver.get({did}); + } catch(e) { + error = e; + } + expect( + didDocument, + 'Expected driver to throw not return a didDocument' + ).to.not.exist; + expect(error).to.exist; + expect(error).to.be.an.instanceof( + DidResolverError, + 'Expected a DidResolverError' + ); + expect(error.code).to.equal('invalidDid'); + }); + it('should throw invalidDid if identifier doesn\'t begin with z', + async () => { + const did = 'did:key:6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; + let error; + let didDocument; + try { + didDocument = await didKeyDriver.get({did}); + } catch(e) { + error = e; + } + expect( + didDocument, + 'Expected driver to throw not return a didDocument' + ).to.not.exist; + expect(error).to.exist; + expect(error).to.be.an.instanceof( + DidResolverError, + 'Expected a DidResolverError' + ); + expect(error.code).to.equal('invalidDid'); + }); + it('should get a didDocument from a did with a version', + async () => { + const did = 'did:key:3:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpM' + + 'vtuVMy53T'; + let error; + let didDocument; + try { + didDocument = await didKeyDriver.get({did}); + } catch(e) { + error = e; + } + expect(didDocument, 'Expected driver to return a didDocument').exist; + expect(error).to.not.exist; + }); + it('should throw invalidDid if version is negative', + async () => { + const did = 'did:key:-3:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpM' + + 'vtuVMy53T'; + let error; + let didDocument; + try { + didDocument = await didKeyDriver.get({did}); + } catch(e) { + error = e; + } + expect( + didDocument, + 'Expected driver to return a didDocument' + ).not.exist; + expect(error).to.exist; + expect(error).to.be.an.instanceof( + DidResolverError, + 'Expected a DidResolverError' + ); + expect(error.code).to.equal('invalidDid'); + }); + it('should throw representationNotSupported if publicKeyFormat is Multikey', + async () => { + const did = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; + const options = {publicKeyFormat: 'Multikey'}; + let error; + let didDocument; + try { + didDocument = await didKeyDriver.get({did, options}); + } catch(e) { + error = e; + } + expect( + didDocument, + 'Expected driver to throw not return a didDocument' + ).to.not.exist; + expect(error).to.exist; + expect(error).to.be.an.instanceof( + DidResolverError, + 'Expected a DidResolverError' + ); + expect(error.code).to.equal('representationNotSupported'); + }); + it('should throw representationNotSupported if publicKeyFormat is ' + + 'JsonWebKey2020', async () => { + const did = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; + const options = {publicKeyFormat: 'JsonWebKey2020'}; + let error; + let didDocument; + try { + didDocument = await didKeyDriver.get({did, options}); + } catch(e) { + error = e; + } + expect( + didDocument, + 'Expected driver to throw not return a didDocument' + ).to.not.exist; + expect(error).to.exist; + expect(error).to.be.an.instanceof( + DidResolverError, + 'Expected a DidResolverError' + ); + expect(error.code).to.equal('representationNotSupported'); + }); + it('should throw unsupportedPublicKeyType if publicKeyFormat is unknown', + async () => { + const did = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; + const options = {publicKeyFormat: 'UnknownFormat2020'}; + let error; + let didDocument; + try { + didDocument = await didKeyDriver.get({did, options}); + } catch(e) { + error = e; + } + expect( + didDocument, + 'Expected driver to throw not return a didDocument' + ).to.not.exist; + expect(error).to.exist; + expect(error).to.be.an.instanceof( + DidResolverError, + 'Expected a DidResolverError' + ); + expect(error.code).to.equal('unsupportedPublicKeyType'); }); + it('should throw invalidPublicKeyType if publicKeyFormat is experimental' + + ' & enableExperimentalPublicKeyTypes is false', async () => { + // Note: Testing same keys as previous (2020 mode) test + const did = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; + const options = { + publicKeyFormat: 'Ed25519VerificationKey2018', + // this is the default value of false + enableExperimentalPublicKeyTypes: false + }; + let error; + let didDocument; + try { + didDocument = await didKeyDriver.get({did, options}); + } catch(e) { + error = e; + } + expect( + didDocument, + 'Expected driver to throw not return a didDocument' + ).to.not.exist; + expect(error).to.exist; + expect(error).to.be.an.instanceof( + DidResolverError, + 'Expected a DidResolverError' + ); + expect(error.code).to.equal('invalidPublicKeyType'); + }); + it('should get the DID Doc with publicKeyFormat ' + + 'Ed25519VerificationKey2018', async () => { // Note: Testing same keys as previous (2020 mode) test const did = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; - const didDocument = await didKeyDriver2018.get({did}); + const options = { + publicKeyFormat: 'Ed25519VerificationKey2018', + enableExperimentalPublicKeyTypes: true + }; + const didDocument = await didKeyDriver.get({did, options}); const expectedDidDoc = { '@context': [ @@ -127,13 +317,15 @@ describe('did:key method driver', () => { }); }); - it('should resolve an individual key in 2018 mode', async () => { - const didKeyDriver2018 = driver({ - verificationSuite: Ed25519VerificationKey2018 - }); + it('should resolve an individual key with publicKeyFormat ' + + 'Ed25519VerificationKey2018', async () => { const did = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; const keyId = did + '#z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; - const key = await didKeyDriver2018.get({did: keyId}); + const options = { + publicKeyFormat: 'Ed25519VerificationKey2018', + enableExperimentalPublicKeyTypes: true + }; + const key = await didKeyDriver.get({did: keyId, options}); expect(key).to.eql({ '@context': 'https://w3id.org/security/suites/ed25519-2018/v1', @@ -161,14 +353,23 @@ describe('did:key method driver', () => { }); }); - it('should resolve an individual key agreement key (2018)', async () => { - const didKeyDriver2018 = driver({ - verificationSuite: Ed25519VerificationKey2018 - }); + it('should resolve a DID doc with out an encryption method', async () => { + const did = 'did:key:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T'; + const options = {enableEncryptionKeyDerivation: false}; + const key = await didKeyDriver.get({did, options}); + expect(key).to.eql(noKaKDidDoc); + }); + + it('should resolve an individual key agreement key using publicKeyFormat ' + + 'Ed25519VerificationKey2018', async () => { const did = 'did:key:z6MkpTHR8VNsBxYAAWHut2Geadd9jSwuBV8xRoAnwWsdvktH'; const kakKeyId = `${did}#z6LSbysY2xFMRpGMhb7tFTLMpeuPRaqaWM1yECx2AtzE3KCc`; - const key = await didKeyDriver2018.get({did: kakKeyId}); + const options = { + publicKeyFormat: 'Ed25519VerificationKey2018', + enableExperimentalPublicKeyTypes: true + }; + const key = await didKeyDriver.get({did: kakKeyId, options}); expect(key).to.eql({ '@context': 'https://w3id.org/security/suites/x25519-2019/v1', diff --git a/test/expected-data.js b/test/expected-data.js index fb7c904..d3e62b1 100644 --- a/test/expected-data.js +++ b/test/expected-data.js @@ -35,4 +35,33 @@ export const expectedDidDoc = { } ] }; + +export const noKaKDidDoc = { + '@context': [ + 'https://www.w3.org/ns/did/v1' + ], + assertionMethod: [ + 'did:key:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T#z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T' + ], + authentication: [ + 'did:key:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T#z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T' + ], + capabilityDelegation: [ + 'did:key:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T#z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T' + ], + capabilityInvocation: [ + 'did:key:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T#z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T' + ], + id: 'did:key:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T', + verificationMethod: [ + { + controller: 'did:key:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T', + id: 'did:key:z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T#z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T', + publicKeyMultibase: 'z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T', + type: 'Ed25519VerificationKey2020' + } + ] +}; + /* eslint-enable */ +