├── .eslintrc.cjs ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── benchmark └── benchmark.js ├── docs ├── generate.js └── template.hbs ├── lib ├── CryptoLD.js ├── LDKeyPair.js └── index.js ├── package.json └── tests ├── .eslintrc.cjs ├── karma.conf.cjs └── unit ├── CryptoLD.spec.js └── LDKeyPair.spec.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'digitalbazaar', 8 | 'digitalbazaar/jsdoc', 9 | 'digitalbazaar/module' 10 | ], 11 | rules: { 12 | 'unicorn/prefer-node-protocol': 'error' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | node-version: [16.x] 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Use Node.js ${{ matrix.node-version }} 14 | uses: actions/setup-node@v1 15 | with: 16 | node-version: ${{ matrix.node-version }} 17 | - run: npm install 18 | - name: Run eslint 19 | run: npm run lint 20 | test-node: 21 | needs: [lint] 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | node-version: [14.x, 16.x, 18.x] 26 | steps: 27 | - uses: actions/checkout@v2 28 | - name: Use Node.js ${{ matrix.node-version }} 29 | uses: actions/setup-node@v1 30 | with: 31 | node-version: ${{ matrix.node-version }} 32 | - run: npm install 33 | - name: Run test with Node.js ${{ matrix.node-version }} 34 | run: npm run test-node 35 | test-karma: 36 | needs: [lint] 37 | runs-on: ubuntu-latest 38 | strategy: 39 | matrix: 40 | node-version: [16.x] 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Use Node.js ${{ matrix.node-version }} 44 | uses: actions/setup-node@v1 45 | with: 46 | node-version: ${{ matrix.node-version }} 47 | - run: npm install 48 | - name: Run karma tests 49 | run: npm run test-karma 50 | coverage: 51 | needs: [test-node, test-karma] 52 | runs-on: ubuntu-latest 53 | strategy: 54 | matrix: 55 | node-version: [16.x] 56 | steps: 57 | - uses: actions/checkout@v2 58 | - name: Use Node.js ${{ matrix.node-version }} 59 | uses: actions/setup-node@v1 60 | with: 61 | node-version: ${{ matrix.node-version }} 62 | - run: npm install 63 | - name: Generate coverage report 64 | run: npm run coverage-ci 65 | - name: Upload coverage to Codecov 66 | uses: codecov/codecov-action@v2 67 | with: 68 | file: ./coverage/lcov.info 69 | fail_ci_if_error: true 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sw[op] 2 | *~ 3 | .cproject 4 | .project 5 | .c9 6 | *.lcov 7 | *.sublime-project 8 | *.sublime-workspace 9 | .vscode 10 | .DS_Store 11 | .settings 12 | coverage 13 | node_modules 14 | v8.log 15 | .c9revisions 16 | npm-debug.log 17 | package-lock.json 18 | .nyc_output 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # crypto-ld ChangeLog 2 | 3 | ## 7.0.0 - 2022-06-01 4 | 5 | ### Changed 6 | - **BREAKING**: Convert to module (ESM). 7 | - **BREAKING**: Require Node.js >=14. 8 | - Update dependencies. 9 | - Lint module. 10 | 11 | ## 6.0.0 - 2021-05-05 12 | 13 | ### Changed 14 | - **BREAKING**: `.from()` now routes to `.fromKeyDocument()` if the serialized 15 | key object has a `@context`. This update is to make for more secure behavior 16 | when creating key pair instances from "untrusted" key objects (say, fetched 17 | from the web etc). 18 | 19 | ## 5.1.0 - 2021-04-01 20 | 21 | ### Added 22 | - Implement `CryptoLD.fromKeyId` API. 23 | - Implement `LDKeyPair.fromKeyDocument` API. 24 | - Add support for revoked keys. 25 | 26 | ## 5.0.0 - 2021-03-16 27 | 28 | ### Changed 29 | - **BREAKING**: Remove `LDVerifierKeyPair` subclass. Fold `signer()` and 30 | `verifier()` methods into parent `LDKeyPair` class. 31 | - **BREAKING**: `export()` is now a sync function (no reason for it to be 32 | async). 33 | - **BREAKING**: Remove `keyPair.addPrivateKey()` and `keyPair.addPublicKey()`. 34 | Subclasses will just need to override `export()` directly. 35 | 36 | ### Upgrading from v4.x 37 | The breaking changes in v5 do not affect any application code, they only affect 38 | key pair plugins such as 39 | https://github.com/digitalbazaar/ed25519-verification-key-2020. 40 | No changes necessary in application code upgrading from `v5` from `v4`. 41 | 42 | ## 4.0.3 - 2020-11-25 43 | 44 | ### Changed 45 | - Publish package. 46 | 47 | ## 4.0.2 - 2020-08-01 48 | 49 | ### Changed 50 | - Fix `use()` suite usage. 51 | 52 | ## 4.0.1 - 2020-08-01 53 | 54 | ### Changed 55 | - Removed unused `sodium-native` dependency. 56 | 57 | ## 4.0.0 - 2020-08-01 58 | 59 | ### Changed 60 | - Implement chai-like `.use()` API for installing and specifying individual key 61 | types. 62 | - **BREAKING**: Extracted bundled Ed25519 and RSA key suites to their own 63 | libraries. 64 | - **BREAKING**: Remove deprecated `.owner` instance property 65 | - **BREAKING**: Remove deprecated `.passphrase` instance property, and the 66 | `encrypt()` and `decrypt()` methods (these are no longer used). 67 | - **BREAKING**: Remove deprecated/unused `publicKey` and `privateKey` 68 | properties. 69 | - **BREAKING**: Rename `.publicNode()` to `.export({publicKey: true})`. 70 | - **BREAKING**: `.export()` now requires explicitly stating whether you're 71 | exporting public or private key material. 72 | - **BREAKING**: Changed `verifyFingerprint()` to used named params. 73 | - **BREAKING**: Changed `addPublicKey()` and `addPrivateKey()` to used named 74 | params. 75 | 76 | ### 4.0.0 - Purpose 77 | 78 | The previous design (`v3.7` and earlier) bundled two key types with this 79 | library (RSA and Ed25519), which resulted in extraneous code and bundle size 80 | for projects that only used one of them (or used some other suite). The 81 | decision was made to extract those bundled suites to their own repositories, 82 | and to add a builder-style `.use()` API to `crypto-ld` so that client code 83 | could select just the suites they needed. 84 | 85 | Since this was a comprehensive breaking change in usage, this also gave an 86 | opportunity to clean up and streamline the existing API, change function 87 | signatures to be consistent (for example, to consistently used named 88 | parameters), and to remove deprecated and unused APIs and properties. 89 | 90 | ### Upgrading from v3.7.0 91 | 92 | Since this is a comprehensive breaking change, you will need to audit and change 93 | pretty much all usage of `crypto-ld` and compatible key pairs. Specifically: 94 | 95 | * Ed25519 and RSA keys are no longer imported from `crypto-ld`, they'll need to 96 | be imported from their own packages. 97 | * Since key suites have been decoupled from `crypto-ld`, it means that this 98 | library should only be used when a project is using _multiple_ key suites. 99 | If you're just using a single suite, then you can use that suite directly, 100 | without `crypto-ld`. 101 | * Most function param signatures have been changed to use `{}` style named 102 | params. 103 | 104 | ## 3.7.0 - 2019-09-06 105 | 106 | ### Added 107 | - Add support for Node 12 Ed25519 generate, sign, and verify. 108 | - Make `sodium-native` an optional dependency. 109 | 110 | ## 3.6.0 - 2019-08-06 111 | 112 | ### Added 113 | - Add `LDKeyPair.fromFingerprint()` to create an Ed25519KeyPair instance 114 | from a fingerprint (for use with `did:key` method code). 115 | 116 | ## 3.5.3 - 2019-07-16 117 | 118 | ### Fixed 119 | - Use base64url-universal@1.0.1 which properly specifies the Node.js engine. 120 | 121 | ## 3.5.2 - 2019-04-16 122 | 123 | ### Fixed 124 | - Fix incorrectly formatted engine tag in package file. 125 | 126 | ## 3.5.1 - 2019-04-09 127 | 128 | ### Fixed 129 | - The `util.base58PublicKeyFingerprint` was released in error. It has been 130 | replaced by the `Ed25519KeyPair.fingerprintFromPublicKey` API contained 131 | in this release. 132 | 133 | ## 3.5.0 - 2019-04-08 134 | 135 | ### Added 136 | - Add `util.base58PublicKeyFingerprint` helper for computing public key 137 | fingerprints. NOTE: this API was released in error, see release 3.5.1. 138 | 139 | ## 3.4.1 - 2019-03-27 140 | 141 | ### Fixed 142 | - Fix Ed25519 fingerprint generation when running in the browser. 143 | 144 | ## 3.4.0 - 2019-02-26 145 | 146 | ### Added 147 | - Enable use of a `seed` to generate deterministic Ed25519 keys. 148 | 149 | ## 3.3.0 - 2019-02-21 150 | 151 | ### Changed 152 | - Improve error handling related to the decoding of key material in 153 | `Ed25519KeyPair`. This is helpful when dealing with key material that may 154 | be provided via command line or web UI. 155 | 156 | ## 3.2.0 - 2019-02-19 157 | 158 | ### Changed 159 | - Remove `sodium-universal` dependency to reduce the size of the browser bundle. 160 | - Ed25519 operations in Node.js use `sodium-native` APIs. 161 | - Ed25519 operations in the browser use `forge` APIs. 162 | - Use `base64url-universal` which eliminates the need for a `Buffer` polyfill 163 | when this module is used in the browser. 164 | 165 | ## 3.1.0 - 2019-02-18 166 | 167 | ### Changed 168 | - Use forge@0.8.0. The new `rsa.generateKeyPair` API automatically uses 169 | native implementation when available in nodejs >= 10.12.0. 170 | 171 | ## 3.0.0 - 2019-01-30 172 | 173 | ### Changed 174 | - **BREAKING**: Make key fingerprints conform to the latest multibase/multicodec 175 | specification. The fingerprints generated by 2.x and 3.x are different due 176 | to encoding changes. 177 | - **BREAKING**: The only exports for this module are the three key classes: 178 | `LDKeyPair`, `Ed25519KeyPair`, and `RSAKeyPair`. 179 | 180 | ## 2.0.1 - 2019-01-24 181 | 182 | ### Fixed 183 | - No need to bring in `util` in browser environment. 184 | 185 | ## 2.0.0 - 2019-01-16 186 | 187 | ### Fixed 188 | - Specify published files. 189 | 190 | ### Changed 191 | - **BREAKING**: `Ed25519KeyPair` now uses `publicKeyBase58` and 192 | `privateKeyBase58` attributes, instead of `publicKeyBase` and 193 | `privateKeyBase` 194 | - **BREAKING**: Changed signature of `LDKeyPair.from()` (one `options` param 195 | instead of a separate `data` and `options`) 196 | - Removed `ursa` support. 197 | - Node.js >= 10.12.0: use generateKeyPair(). 198 | - Earlier Node.js and browsers: use forge. 199 | - **NOTE**: Newer Node.js versions are *much* faster at RSA key generation vs 200 | the forge fallback. It is highly recommended to use a newer Node.js. 201 | - Switch from chloride to sodium-universal. 202 | 203 | ### Added 204 | - Added `controller` attribute (to use instead of the deprecated `owner`) 205 | - Added `sign()` and `verify()` factory functions for use with 206 | `jsonld-signatures` 207 | - Add Karma browser testing support. 208 | 209 | ## 1.0.0 - 2018-11-08 210 | 211 | - Change keyType to type (do match DID Doc usage), add key owner 212 | - Initial NPM release 213 | 214 | ## 0.1.0 - 2018-10-17 215 | 216 | - Moved LDKeyPair code from `did-io` 217 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | You may use the crypto-ld project under the terms of the BSD License. 2 | 3 | You are free to use this project in commercial projects as long as the 4 | copyright header is left intact. 5 | 6 | If you are a commercial entity and use this set of libraries in your 7 | commercial software then reasonable payment to Digital Bazaar, if you can 8 | afford it, is not required but is expected and would be appreciated. If this 9 | library saves you time, then it's saving you money. We are attempting to strike 10 | a balance between helping the development community while not being taken 11 | advantage of by lucrative commercial entities for our efforts. 12 | 13 | ------------------------------------------------------------------------------- 14 | New BSD License (3-clause) 15 | Copyright (c) 2015-2019, Digital Bazaar, Inc. 16 | All rights reserved. 17 | 18 | Redistribution and use in source and binary forms, with or without 19 | modification, are permitted provided that the following conditions are met: 20 | * Redistributions of source code must retain the above copyright 21 | notice, this list of conditions and the following disclaimer. 22 | * Redistributions in binary form must reproduce the above copyright 23 | notice, this list of conditions and the following disclaimer in the 24 | documentation and/or other materials provided with the distribution. 25 | * Neither the name of Digital Bazaar, Inc. nor the 26 | names of its contributors may be used to endorse or promote products 27 | derived from this software without specific prior written permission. 28 | 29 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 30 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 31 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 32 | DISCLAIMED. IN NO EVENT SHALL DIGITAL BAZAAR BE LIABLE FOR ANY 33 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 34 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 35 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 36 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 37 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 38 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cryptographic Key Pair Library for Linked Data _(crypto-ld)_ 2 | 3 | [![Node.js CI](https://github.com/digitalbazaar/crypto-ld/workflows/Node.js%20CI/badge.svg)](https://github.com/digitalbazaar/crypto-ld/actions?query=workflow%3A%22Node.js+CI%22) 4 | [![NPM Version](https://img.shields.io/npm/v/crypto-ld.svg?style=flat-square)](https://npm.im/crypto-ld) 5 | 6 | 7 | > A Javascript library for generating and performing common operations on Linked Data cryptographic key pairs. 8 | 9 | ## Table of Contents 10 | 11 | - [Background](#background) 12 | - [Security](#security) 13 | - [Install](#install) 14 | - [Usage](#usage) 15 | - [Contribute](#contribute) 16 | - [Commercial Support](#commercial-support) 17 | - [License](#license) 18 | 19 | ## Background 20 | 21 | See also (related specs): 22 | 23 | * [Linked Data Cryptographic Suite Registry](https://w3c-ccg.github.io/ld-cryptosuite-registry/) 24 | * [Linked Data Proofs 1.0](https://w3c-ccg.github.io/ld-proofs/) 25 | 26 | ### Supported Key Types (`crypto-ld` versions `4+`) 27 | 28 | This library provides general Linked Data cryptographic key generation 29 | functionality, but does not support any individual key type by default. 30 | 31 | To use it, you must [install individual driver libraries](#usage) for each 32 | cryptographic key type. The following libraries are currently supported. 33 | 34 | | Type | Crypto Suite | Library | Usage | 35 | |-------------|--------------|---------|-------| 36 | | `Ed25519` | **[Ed25519VerificationKey2020](https://w3c-ccg.github.io/lds-ed25519-2020/#ed25519verificationkey2020)** (recommended), [Ed25519VerificationKey2018](https://w3c-ccg.github.io/lds-ed25519-2018/#the-ed25519-key-format) (legacy) | **[`ed25519-verification-key-2020 >=1.0`](https://github.com/digitalbazaar/ed25519-verification-key-2020)** (recommended), [`ed25519-verification-key-2018 >=2.0`](https://github.com/digitalbazaar/ed25519-verification-key-2018) (legacy) | Signatures, VCs, zCaps, DIDAuth | 37 | | `X25519/Curve25519` | X25519KeyAgreementKey2019 | [`x25519-key-agreement-key-2019 >=4.0`](https://github.com/digitalbazaar/x25519-key-agreement-key-2019) | [ECDH](https://en.wikipedia.org/wiki/Elliptic-curve_Diffie%E2%80%93Hellman) key agreement, JWE/CWE encryption with [`minimal-cipher`](https://github.com/digitalbazaar/minimal-cipher) | 38 | | `Secp256k1` | [EcdsaSecp256k1VerificationKey2019](https://w3c-ccg.github.io/ld-cryptosuite-registry/#secp256k1) | [`ecdsa-secp256k1-verification-key-2019`](https://github.com/digitalbazaar/ecdsa-secp256k1-verification-key-2019) | Signatures, VCs, zCaps, DIDAuth, HD Wallets | 39 | 40 | ### Legacy Supported Key Types (`crypto-ld` versions `<=3`) 41 | 42 | In the previous version (v3.x) of `crypto-ld`, the RSA and Ed25519 suites were 43 | bundled with `crypto-ld` (as opposed to residing in standalone packages). 44 | For previous usage instructions of bundled RSA, Ed25519 and standalone 45 | Curve25519/[`x25519-key-pair`](https://github.com/digitalbazaar/x25519-key-agreement-key-2019/tree/v3.1.0) 46 | type keys, see the [README for `crypto-ld` v3.9](https://github.com/digitalbazaar/crypto-ld/tree/v3.9.0). 47 | 48 | ### Choosing a Key Type 49 | 50 | For digital signatures using the 51 | [`jsonld-signatures`](https://github.com/digitalbazaar/jsonld-signatures), 52 | signing of Verifiable Credentials using [`vc-js`](https://github.com/digitalbazaar/vc-js), 53 | authorization capabilities, and DIDAuth operations: 54 | 55 | * Prefer **Ed25519VerificationKey2020** type keys, by default. 56 | * Use **EcdsaSepc256k1** keys if your use case requires it (for example, if 57 | you're developing for a Bitcoin-based or Ethereum-based ledger), or if you 58 | require Hierarchical Deterministic (HD) wallet functionality. 59 | 60 | For key agreement protocols for encryption operations: 61 | 62 | * Use **Curve25519** with the [`minimal-cipher`](https://github.com/digitalbazaar/minimal-cipher) 63 | library. 64 | 65 | ## Security 66 | 67 | As with most security- and cryptography-related tools, the overall security of 68 | your system will largely depend on your design decisions. 69 | 70 | ## Install 71 | 72 | - Node.js 14.0+ is required. 73 | 74 | To install locally (for development): 75 | 76 | ``` 77 | git clone https://github.com/digitalbazaar/crypto-ld.git 78 | cd crypto-ld 79 | npm install 80 | ``` 81 | 82 | ## Usage 83 | 84 | ### Installing Support for Key Types 85 | 86 | In order to use this library, you will need to import and install driver 87 | libraries for key types you'll be working with via the `use()` method. 88 | 89 | To use the library with one or more supported suites: 90 | 91 | ```js 92 | import {Ed25519VerificationKey2020} from '@digitalbazaar/ed25519-verification-key-2020'; 93 | import {X25519KeyAgreementKey2020} from '@digitalbazaar/x25519-key-agreement-key-2020'; 94 | 95 | import {CryptoLD} from 'crypto-ld'; 96 | const cryptoLd = new CryptoLD(); 97 | 98 | cryptoLd.use(Ed25519VerificationKey2020); 99 | cryptoLd.use(X25519KeyAgreementKey2020); 100 | 101 | const edKeyPair = await cryptoLd.generate({type: 'Ed25519VerificationKey2020'}); 102 | ``` 103 | 104 | ### Generating a new public/private key pair 105 | 106 | To generate a new public/private key pair: `cryptoLd.generate(options)`: 107 | 108 | * `{string} [type]` Suite name, required. 109 | * `{string} [controller]` Optional controller URI or DID to initialize the 110 | generated key. (This will also init the key id.) 111 | * `{string} [seed]` Optional deterministic seed value (only supported by some 112 | key types, such as `ed25519`) from which to generate the key. 113 | 114 | ### Importing a key pair from storage 115 | 116 | To create an instance of a public/private key pair from data imported from 117 | storage, use `cryptoLd.from()`: 118 | 119 | ```js 120 | const serializedKeyPair = { ... }; 121 | 122 | const keyPair = await cryptoLd.from(serializedKeyPair); 123 | ``` 124 | 125 | Note that only installed key types are supported, if you try to create a 126 | key pair via `from()` for an unsupported type, an error will be thrown. 127 | 128 | ### Common individual key pair operations 129 | 130 | The full range of operations will depend on key type. Here are some common 131 | operations supported by all key types. 132 | 133 | #### Exporting the public key only 134 | 135 | To export just the public key of a pair - use `export()`: 136 | 137 | ```js 138 | keyPair.export({publicKey: true}); 139 | // -> 140 | { 141 | type: 'Ed25519VerificationKey2020', 142 | id: 'did:example:1234#z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3', 143 | controller: 'did:example:1234', 144 | publicKeyMultibase: 'zEYJrMxWigf9boyeJMTRN4Ern8DJMoCXaLK77pzQmxVjf' 145 | } 146 | ``` 147 | 148 | #### Exporting the full public-private key pair 149 | 150 | To export the full key pair, including private key (warning: this should be a 151 | carefully considered operation, best left to dedicated Key Management Systems): 152 | 153 | ```js 154 | keyPair.export({publicKey: true, privateKey: true}); 155 | // -> 156 | { 157 | type: 'Ed25519VerificationKey2020', 158 | id: 'did:example:1234#z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3', 159 | controller: 'did:example:1234', 160 | publicKeyMultibase: 'zEYJrMxWigf9boyeJMTRN4Ern8DJMoCXaLK77pzQmxVjf', 161 | privateKeyMultibase: 'z4E7Q4neNHwv3pXUNzUjzc6TTYspqn9Aw6vakpRKpbVrCzwKWD4hQDHnxuhfrTaMjnR8BTp9NeUvJiwJoSUM6xHAZ' 162 | } 163 | ``` 164 | 165 | #### Generating and verifying key fingerprint 166 | 167 | To generate a fingerprint: 168 | 169 | ```js 170 | keyPair.fingerprint(); 171 | // -> 172 | 'z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3' 173 | ``` 174 | 175 | To verify a fingerprint: 176 | 177 | ```js 178 | keyPair.verifyFingerprint({ 179 | fingerprint: 'z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3' 180 | }); 181 | // -> 182 | { valid: true } 183 | ``` 184 | 185 | ### Operations on signature-related key pairs 186 | 187 | For key pairs that are related to signature and verification (that extend from 188 | the `LDVerifierKeyPair` class), two additional operations must be supported: 189 | 190 | #### Creating a signer function 191 | 192 | In order to perform a cryptographic signature, you need to create a `sign` 193 | function, and then invoke it. 194 | 195 | ```js 196 | const keyPair = await cryptoLd.generate({type: 'Ed25519VerificationKey2020'}); 197 | 198 | const {sign} = keyPair.signer(); 199 | 200 | const data = 'test data to sign'; 201 | const signatureValue = await sign({data}); 202 | ``` 203 | 204 | #### Creating a verifier function 205 | 206 | In order to verify a cryptographic signature, you need to create a `verify` 207 | function, and then invoke it (passing it the data to verify, and the signature). 208 | 209 | ```js 210 | const keyPair = await cryptoLd.generate({type: 'Ed25519VerificationKey2020'}); 211 | 212 | const {verify} = keyPair.verifier(); 213 | 214 | const {valid} = await verify({data, signature}); 215 | ``` 216 | 217 | ## Contribute 218 | 219 | See [the contribute file](https://github.com/digitalbazaar/bedrock/blob/master/CONTRIBUTING.md)! 220 | 221 | PRs accepted. 222 | 223 | If editing the Readme, please conform to the 224 | [standard-readme](https://github.com/RichardLitt/standard-readme) specification. 225 | 226 | ## Commercial Support 227 | 228 | Commercial support for this library is available upon request from 229 | Digital Bazaar: support@digitalbazaar.com 230 | 231 | ## License 232 | 233 | [New BSD License (3-clause)](LICENSE) © Digital Bazaar 234 | -------------------------------------------------------------------------------- /benchmark/benchmark.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Benchmark runner for crypto-ld. 3 | * 4 | * @author Digital Bazaar 5 | * 6 | * Copyright (c) 2019-2022 Digital Bazaar, Inc. All rights reserved. 7 | */ 8 | import Benchmark from 'benchmark'; 9 | import {/*LDKeyPair, */Ed25519KeyPair, RSAKeyPair} from '../lib/index.js'; 10 | 11 | // run tests 12 | const suite = new Benchmark.Suite; 13 | 14 | suite 15 | .add({ 16 | name: 'Ed25519 generation', 17 | defer: true, 18 | fn: function(deferred) { 19 | Ed25519KeyPair 20 | .generate() 21 | .then(() => deferred.resolve()); 22 | } 23 | }) 24 | .add({ 25 | name: 'RSA generation', 26 | defer: true, 27 | fn: function(deferred) { 28 | RSAKeyPair 29 | .generate() 30 | .then(() => deferred.resolve()); 31 | } 32 | }) 33 | .on('start', () => { 34 | console.log('Benchmarking...'); 35 | }) 36 | .on('cycle', event => { 37 | console.log(String(event.target)); 38 | /* 39 | const s = event.target.stats; 40 | console.log(` min:${Math.min(...s.sample)} max:${Math.max(...s.sample)}`); 41 | console.log(` deviation:${s.deviation} mean:${s.mean}`); 42 | console.log(` moe:${s.moe} rme:${s.rme}% sem:${s.sem} var:${s.variance}`); 43 | */ 44 | }) 45 | .on('complete', () => { 46 | console.log('Done.'); 47 | }) 48 | .run({async: true}); 49 | -------------------------------------------------------------------------------- /docs/generate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import jsdoc2md from 'jsdoc-to-markdown'; 5 | import fs from 'node:fs'; 6 | 7 | const template = fs.readFileSync('./docs/template.hbs', 'utf-8'); 8 | const opFactory = file => ({template, files: `./lib/${file}`}); 9 | 10 | // we only generate docs for files which end with keypair or index. 11 | const docs = /(keypair|cryptold|index).js$/i; 12 | const files = fs.readdirSync('./lib') 13 | .filter(p => docs.test(p)); 14 | files.forEach(filePath => { 15 | const options = opFactory(filePath); 16 | const doc = jsdoc2md.renderSync(options); 17 | // if the doc is smaller than the template there was 18 | // no actual content inserted into main. 19 | if(!doc || doc.length < template.length) { 20 | return false; 21 | } 22 | const fpath = filePath; 23 | const docFileName = './docs/' + fpath.replace(/.js/, '.md'); 24 | fs.writeFileSync(docFileName, doc); 25 | }); 26 | -------------------------------------------------------------------------------- /docs/template.hbs: -------------------------------------------------------------------------------- 1 | ## Usage 2 | 3 | {{>main}} 4 | 5 | --- 6 | Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved. 7 | -------------------------------------------------------------------------------- /lib/CryptoLD.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | 5 | /** 6 | * General purpose key generation driver for Linked Data cryptographic key 7 | * pairs. 8 | * 9 | * @param {Map} [suites] - Optional map of supported suites, by suite id. 10 | */ 11 | export class CryptoLD { 12 | constructor({suites} = {}) { 13 | this.suites = suites || new Map(); 14 | } 15 | 16 | /** 17 | * Installs support for a key type (suite). 18 | * 19 | * @param {LDKeyPair} keyPairLib - Conforming key pair library for a suite. 20 | */ 21 | use(keyPairLib) { 22 | this.suites.set(keyPairLib.suite, keyPairLib); 23 | } 24 | 25 | /** 26 | * Generates a public/private LDKeyPair. 27 | * 28 | * @param {object} options - Suite-specific key options. 29 | * @param {string} options.type - Key suite id (for example, 30 | * 'Ed25519VerificationKey2020'). 31 | * @param {string} [options.controller] - Controller DID or URL for the 32 | * generated key pair. If present, used to auto-initialize the key.id. 33 | * 34 | * @returns {Promise} Generated key pair. 35 | */ 36 | async generate(options = {}) { 37 | const Suite = this._suiteForType(options); 38 | return Suite.generate(options); 39 | } 40 | 41 | /** 42 | * Imports a public/private key pair from serialized data. 43 | * 44 | * @param {object} serialized - Serialized key object. 45 | * 46 | * @throws {Error} - On missing or invalid serialized key data. 47 | * 48 | * @returns {Promise} Imported key pair. 49 | */ 50 | async from(serialized = {}) { 51 | const Suite = this._suiteForType(serialized); 52 | 53 | if(serialized['@context']) { 54 | // presume this may be an untrusted (fetched, etc) key document 55 | return Suite.fromKeyDocument({document: serialized}); 56 | } 57 | 58 | return Suite.from(serialized); 59 | } 60 | 61 | /** 62 | * Imports a key pair instance from a provided externally fetched key 63 | * document (fetched via a secure JSON-LD `documentLoader` or via 64 | * `cryptoLd.fromKeyId()`), optionally checking it for revocation and required 65 | * context. 66 | * 67 | * @param {object} options - Options hashmap. 68 | * @param {string} options.document - Externally fetched key document. 69 | * @param {boolean} [options.checkContext=true] - Whether to check that the 70 | * fetched key document contains the context required by the key's crypto 71 | * suite. 72 | * @param {boolean} [options.checkRevoked=true] - Whether to check the key 73 | * object for the presence of the `revoked` timestamp. 74 | * 75 | * @returns {Promise} Resolves with the resulting key pair 76 | * instance. 77 | */ 78 | async fromKeyDocument({ 79 | document, checkContext = true, checkRevoked = true 80 | } = {}) { 81 | if(!document) { 82 | throw new TypeError('The "document" parameter is required.'); 83 | } 84 | const Suite = this._suiteForType(document); 85 | 86 | return Suite.fromKeyDocument({document, checkContext, checkRevoked}); 87 | } 88 | 89 | /** 90 | * Imports a key pair instance via the provided `documentLoader` function, 91 | * optionally checking it for revocation and required context. 92 | * 93 | * @param {object} options - Options hashmap. 94 | * @param {string} options.id - Key ID or URI. 95 | * @param {Function} options.documentLoader - JSON-LD Document Loader. 96 | * @param {boolean} [options.checkContext=true] - Whether to check that the 97 | * fetched key document contains the context required by the key's crypto 98 | * suite. 99 | * @param {boolean} [options.checkRevoked=true] - Whether to check the key 100 | * object for the presence of the `revoked` timestamp. 101 | * 102 | * @returns {Promise} Resolves with the appropriate key pair 103 | * instance. 104 | */ 105 | async fromKeyId({ 106 | id, documentLoader, checkContext = true, checkRevoked = true 107 | } = {}) { 108 | if(!id) { 109 | throw new TypeError('The "id" parameter is required.'); 110 | } 111 | if(!documentLoader) { 112 | throw new TypeError('The "documentLoader" parameter is required.'); 113 | } 114 | let keyDocument; 115 | try { 116 | ({document: keyDocument} = await documentLoader(id)); 117 | // the supplied documentLoader may not be properly implemented 118 | if(!keyDocument) { 119 | throw new Error( 120 | 'The "documentLoader" function must return a "document" object.'); 121 | } 122 | } catch(e) { 123 | const error = new Error('Error fetching document: ' + e.message); 124 | error.cause = e; 125 | throw error; 126 | } 127 | const fetchedType = keyDocument.type; 128 | if(!fetchedType) { 129 | throw new Error('Key suite type not found in fetched document.'); 130 | } 131 | const keySuite = this.suites.get(fetchedType); 132 | if(!keySuite) { 133 | throw new Error(`Support for suite "${fetchedType}" is not installed.`); 134 | } 135 | 136 | return keySuite.fromKeyDocument({document: keyDocument, checkContext, 137 | checkRevoked}); 138 | } 139 | 140 | /** 141 | * Tests if a given key type is currently installed. 142 | * 143 | * @param {string} [type] - Key suite id ('Ed25519VerificationKey2020'). 144 | * @private 145 | * 146 | * @returns {boolean} True if key type installed. 147 | */ 148 | _installed({type}) { 149 | return this.suites.has(type); 150 | } 151 | 152 | /** 153 | * Returns the installed crypto suite class for a given document's type. 154 | * 155 | * @param {object} document - A serialized key document (or options document). 156 | * @param {string} document.type - Key suite id (for example, 157 | * 'Ed25519VerificationKey2020'). 158 | * 159 | * @returns {object} LDKeyPair (crypto suite) class. 160 | */ 161 | _suiteForType(document) { 162 | const type = document && document.type; 163 | 164 | if(!type) { 165 | throw new TypeError('Missing key type.'); 166 | } 167 | if(!this._installed({type})) { 168 | throw new Error(`Support for key type "${type}" is not installed.`); 169 | } 170 | 171 | return this.suites.get(type); 172 | } 173 | } 174 | 175 | /** 176 | * @typedef LDKeyPair 177 | */ 178 | -------------------------------------------------------------------------------- /lib/LDKeyPair.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | 5 | /** 6 | * When adding support for a new suite type for `crypto-ld`, developers should 7 | * do the following: 8 | * 9 | * 1. Create their own npm package / github repo, such as `example-key-pair`. 10 | * 2. Subclass LDKeyPair. 11 | * 3. Override relevant methods (such as `export()` and `fingerprint()`). 12 | * 4. Add to the key type table in the `crypto-ld` README.md (that's this repo). 13 | */ 14 | export class LDKeyPair { 15 | /* eslint-disable jsdoc/require-description-complete-sentence */ 16 | /** 17 | * Creates a public/private key pair instance. This is an abstract base class, 18 | * actual key material and suite-specific methods are handled in the subclass. 19 | * 20 | * To generate or import a key pair, use the `cryptoLd` instance. 21 | * 22 | * @see CryptoLD.js 23 | * 24 | * @param {object} options - The options to use. 25 | * @param {string} options.id - The key id, typically composed of controller 26 | * URL and key fingerprint as hash fragment. 27 | * @param {string} options.controller - DID/URL of the person/entity 28 | * controlling this key. 29 | * @param {string} [options.revoked] - Timestamp of when the key has been 30 | * revoked, in RFC3339 format. If not present, the key itself is 31 | * considered not revoked. (Note that this mechanism is slightly different 32 | * than DID Document key revocation, where a DID controller can revoke a 33 | * key from that DID by removing it from the DID Document.) 34 | */ 35 | /* eslint-enable */ 36 | constructor({id, controller, revoked} = {}) { 37 | this.id = id; 38 | this.controller = controller; 39 | this.revoked = revoked; 40 | // this.type is set in subclass constructor 41 | } 42 | 43 | /* eslint-disable jsdoc/check-param-names */ 44 | /** 45 | * Generates a new public/private key pair instance. 46 | * Note that this method is not typically called directly by client code, 47 | * but instead is used through a `cryptoLd` instance. 48 | * 49 | * @param {object} options - Suite-specific options for the KeyPair. For 50 | * common options, see the `LDKeyPair.constructor()` docstring. 51 | * 52 | * @returns {Promise} An LDKeyPair instance. 53 | */ 54 | /* eslint-enable */ 55 | static async generate(/* options */) { 56 | throw new Error('Abstract method, must be implemented in subclass.'); 57 | } 58 | 59 | /** 60 | * Imports a key pair instance from a provided externally fetched key 61 | * document (fetched via a secure JSON-LD `documentLoader` or via 62 | * `cryptoLd.fromKeyId()`), optionally checking it for revocation and required 63 | * context. 64 | * 65 | * @param {object} options - Options hashmap. 66 | * @param {string} options.document - Externally fetched key document. 67 | * @param {boolean} [options.checkContext=true] - Whether to check that the 68 | * fetched key document contains the context required by the key's crypto 69 | * suite. 70 | * @param {boolean} [options.checkRevoked=true] - Whether to check the key 71 | * object for the presence of the `revoked` timestamp. 72 | * 73 | * @returns {Promise} Resolves with the resulting key pair 74 | * instance. 75 | */ 76 | static async fromKeyDocument({ 77 | document, checkContext = true, checkRevoked = true 78 | } = {}) { 79 | if(!document) { 80 | throw new TypeError('The "document" parameter is required.'); 81 | } 82 | 83 | if(checkContext) { 84 | const fetchedDocContexts = [].concat(document['@context']); 85 | if(!fetchedDocContexts.includes(this.SUITE_CONTEXT)) { 86 | throw new Error('Key document does not contain required context "' + 87 | this.SUITE_CONTEXT + '".'); 88 | } 89 | } 90 | if(checkRevoked && document.revoked) { 91 | throw new Error(`Key has been revoked since: "${document.revoked}".`); 92 | } 93 | return this.from(document); 94 | } 95 | 96 | /* eslint-disable jsdoc/check-param-names */ 97 | /** 98 | * Generates a KeyPair from some options. 99 | * 100 | * @param {object} options - Will generate a key pair in multiple different 101 | * formats. 102 | * @example 103 | * > const options = { 104 | * type: 'Ed25519VerificationKey2020' 105 | * }; 106 | * > const edKeyPair = await LDKeyPair.from(options); 107 | * 108 | * @returns {Promise} A LDKeyPair. 109 | * @throws Unsupported Key Type. 110 | */ 111 | /* eslint-enable */ 112 | static async from(/* options */) { 113 | throw new Error('Abstract method from() must be implemented in subclass.'); 114 | } 115 | 116 | /** 117 | * Exports the serialized representation of the KeyPair 118 | * and other information that json-ld Signatures can use to form a proof. 119 | * 120 | * NOTE: Subclasses MUST override this method (and add the exporting of 121 | * their public and private key material). 122 | * 123 | * @param {object} [options={}] - Options hashmap. 124 | * @param {boolean} [options.publicKey] - Export public key material? 125 | * @param {boolean} [options.privateKey] - Export private key material? 126 | * 127 | * @returns {object} A public key object 128 | * information used in verification methods by signatures. 129 | */ 130 | export({publicKey = false, privateKey = false} = {}) { 131 | if(!publicKey && !privateKey) { 132 | throw new Error( 133 | 'Export requires specifying either "publicKey" or "privateKey".'); 134 | } 135 | const key = { 136 | id: this.id, 137 | type: this.type, 138 | controller: this.controller 139 | }; 140 | if(this.revoked) { 141 | key.revoked = this.revoked; 142 | } 143 | 144 | return key; 145 | } 146 | 147 | /** 148 | * Returns the public key fingerprint, multibase+multicodec encoded. The 149 | * specific fingerprint method is determined by the key suite, and is often 150 | * either a hash of the public key material (such as with RSA), or the 151 | * full encoded public key (for key types with sufficiently short 152 | * representations, such as ed25519). 153 | * This is frequently used in initializing the key id, or generating some 154 | * types of cryptonym DIDs. 155 | * 156 | * @returns {string} The fingerprint. 157 | */ 158 | fingerprint() { 159 | throw new Error('Abstract method, must be implemented in subclass.'); 160 | } 161 | 162 | /* eslint-disable jsdoc/check-param-names */ 163 | /** 164 | * Verifies that a given key fingerprint matches the public key material 165 | * belonging to this key pair. 166 | * 167 | * @param {string} fingerprint - Public key fingerprint. 168 | * 169 | * @returns {{verified: boolean}} An object with verified flag. 170 | */ 171 | /* eslint-enable */ 172 | verifyFingerprint(/* {fingerprint} */) { 173 | throw new Error('Abstract method, must be implemented in subclass.'); 174 | } 175 | 176 | /* eslint-disable max-len */ 177 | /** 178 | * Returns a signer object for use with 179 | * [jsonld-signatures]{@link https://github.com/digitalbazaar/jsonld-signatures}. 180 | * NOTE: Applies only to verifier type keys (like ed25519). 181 | * 182 | * @example 183 | * > const signer = keyPair.signer(); 184 | * > signer 185 | * { sign: [AsyncFunction: sign] } 186 | * > signer.sign({data}); 187 | * 188 | * @returns {{sign: Function}} A signer for json-ld usage. 189 | */ 190 | /* eslint-enable */ 191 | signer() { 192 | return { 193 | async sign({/* data */}) { 194 | throw new Error('Abstract method, must be implemented in subclass.'); 195 | } 196 | }; 197 | } 198 | 199 | /* eslint-disable max-len */ 200 | /** 201 | * Returns a verifier object for use with 202 | * [jsonld-signatures]{@link https://github.com/digitalbazaar/jsonld-signatures}. 203 | * NOTE: Applies only to verifier type keys (like ed25519). 204 | * 205 | * @example 206 | * > const verifier = keyPair.verifier(); 207 | * > verifier 208 | * { verify: [AsyncFunction: verify] } 209 | * > verifier.verify(key); 210 | * 211 | * @returns {{verify: Function}} Used to verify jsonld-signatures. 212 | */ 213 | /* eslint-enable */ 214 | verifier() { 215 | return { 216 | async verify({/* data, signature */}) { 217 | throw new Error('Abstract method, must be implemented in subclass.'); 218 | } 219 | }; 220 | } 221 | } 222 | 223 | // Implementers must override this in subclasses 224 | LDKeyPair.SUITE_CONTEXT = 'INVALID LDKeyPair CONTEXT'; 225 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2018-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | export {CryptoLD} from './CryptoLD.js'; 5 | export {LDKeyPair} from './LDKeyPair.js'; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crypto-ld", 3 | "version": "7.0.1-0", 4 | "description": "A Javascript library for generating and performing common operations on Linked Data cryptographic key pairs.", 5 | "homepage": "https://github.com/digitalbazaar/crypto-ld", 6 | "author": { 7 | "name": "Digital Bazaar, Inc.", 8 | "email": "support@digitalbazaar.com", 9 | "url": "https://digitalbazaar.com/" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/digitalbazaar/crypto-ld" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/digitalbazaar/crypto-ld/issues", 17 | "email": "support@digitalbazaar.com" 18 | }, 19 | "license": "BSD-3-Clause", 20 | "type": "module", 21 | "exports": "./lib/index.js", 22 | "files": [ 23 | "lib/**/*.js" 24 | ], 25 | "dependencies": {}, 26 | "devDependencies": { 27 | "benchmark": "^2.1.4", 28 | "c8": "^7.11.3", 29 | "chai": "^4.3.6", 30 | "cross-env": "^7.0.3", 31 | "eslint": "^8.16.0", 32 | "eslint-config-digitalbazaar": "^3.0.0", 33 | "eslint-plugin-jsdoc": "^39.3.2", 34 | "eslint-plugin-unicorn": "^42.0.0", 35 | "jsdoc-to-markdown": "^7.1.1", 36 | "karma": "^6.3.20", 37 | "karma-chai": "^0.1.0", 38 | "karma-chrome-launcher": "^3.1.1", 39 | "karma-mocha": "^2.0.1", 40 | "karma-mocha-reporter": "^2.2.5", 41 | "karma-sourcemap-loader": "^0.3.8", 42 | "karma-webpack": "^5.0.0", 43 | "mocha": "^10.0.0", 44 | "mocha-lcov-reporter": "^1.3.0", 45 | "webpack": "^5.72.1" 46 | }, 47 | "c8": { 48 | "reporter": [ 49 | "lcov", 50 | "text-summary", 51 | "text" 52 | ] 53 | }, 54 | "engines": { 55 | "node": ">=14" 56 | }, 57 | "keywords": [ 58 | "Decentralized", 59 | "DID", 60 | "Credential", 61 | "Cryptography", 62 | "Linked Data" 63 | ], 64 | "scripts": { 65 | "test": "npm run test-node", 66 | "test-node": "cross-env NODE_ENV=test mocha --preserve-symlinks -t 30000 -A -R ${REPORTER:-spec} tests/**/*.spec.js", 67 | "test-karma": "karma start tests/karma.conf.cjs", 68 | "benchmark": "node benchmark/benchmark.js", 69 | "coverage": "cross-env NODE_ENV=test c8 npm run test-node", 70 | "coverage-ci": "cross-env NODE_ENV=test c8 --reporter=lcovonly --reporter=text-summary --reporter=text npm run test-node", 71 | "coverage-report": "c8 report", 72 | "lint": "eslint ." 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | mocha: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/karma.conf.cjs: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | module.exports = config => { 5 | const browsers = ['ChromeHeadless']; 6 | const files = ['**/*.spec.js']; 7 | const frameworks = ['mocha']; 8 | const preprocessors = ['webpack', 'sourcemap']; 9 | const reporters = ['mocha']; 10 | const client = { 11 | mocha: { 12 | timeout: 2000 13 | } 14 | }; 15 | 16 | return config.set({ 17 | frameworks, 18 | files, 19 | reporters, 20 | basePath: '', 21 | port: 9876, 22 | colors: true, 23 | browsers, 24 | client, 25 | singleRun: true, 26 | preprocessors: { 27 | 'unit/*.js': preprocessors 28 | }, 29 | webpack: { 30 | mode: 'development', 31 | devtool: 'inline-source-map' 32 | } 33 | }); 34 | }; 35 | -------------------------------------------------------------------------------- /tests/unit/CryptoLD.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import chai from 'chai'; 5 | chai.should(); 6 | const {expect} = chai; 7 | 8 | import {CryptoLD} from '../../lib/index.js'; 9 | 10 | describe('CryptoLD', () => { 11 | let cryptoLd; 12 | beforeEach(() => { 13 | cryptoLd = new CryptoLD(); 14 | }); 15 | 16 | describe('constructor', async () => { 17 | it('should init a custom suite map', async () => { 18 | const suites = {}; 19 | cryptoLd = new CryptoLD({suites}); 20 | 21 | expect(cryptoLd.suites).to.equal(suites); 22 | }); 23 | }); 24 | 25 | describe('use()', () => { 26 | it('should install library driver for suite', async () => { 27 | const keyLibrary = {suite: 'Ed25519VerificationKey2018'}; 28 | 29 | cryptoLd.use(keyLibrary); 30 | 31 | expect(cryptoLd._installed({type: 'Ed25519VerificationKey2018'})) 32 | .to.equal(true); 33 | }); 34 | }); 35 | 36 | describe('generate()', () => { 37 | it('should error on missing type', async () => { 38 | let error; 39 | try { 40 | await cryptoLd.generate(); 41 | } catch(e) { 42 | error = e; 43 | } 44 | expect(error.message).to 45 | .equal('Missing key type.'); 46 | }); 47 | 48 | it('should error on unsupported type', async () => { 49 | let error; 50 | try { 51 | await cryptoLd.generate({type: 'Ed25519VerificationKey2018'}); 52 | } catch(e) { 53 | error = e; 54 | } 55 | expect(error.message).to 56 | .match(/Support for key type "Ed25519VerificationKey2018"/); 57 | }); 58 | 59 | it('should generate based on key type', async () => { 60 | const Suite = { 61 | suite: 'Ed25519VerificationKey2018', 62 | generate: async options => options 63 | }; 64 | cryptoLd.use(Suite); 65 | const result = await cryptoLd.generate({ 66 | type: 'Ed25519VerificationKey2018', controller: 'did:ex:1234' 67 | }); 68 | expect(result).to.eql({ 69 | controller: 'did:ex:1234', type: 'Ed25519VerificationKey2018' 70 | }); 71 | }); 72 | }); 73 | 74 | describe('from()', () => { 75 | it('should error on missing type param', async () => { 76 | let error; 77 | try { 78 | await cryptoLd.from({}); 79 | } catch(e) { 80 | error = e; 81 | } 82 | expect(error.message).to.equal('Missing key type.'); 83 | }); 84 | 85 | it('should error on uninstalled key type', async () => { 86 | let error; 87 | try { 88 | await cryptoLd.from({type: 'Ed25519VerificationKey2018'}); 89 | } catch(e) { 90 | error = e; 91 | } 92 | expect(error.message).to 93 | .equal( 94 | 'Support for key type "Ed25519VerificationKey2018" is not installed.' 95 | ); 96 | }); 97 | 98 | it('should return an instance from serialized data', async () => { 99 | const Suite = { 100 | suite: 'Ed25519VerificationKey2018', 101 | from: async data => data 102 | }; 103 | cryptoLd.use(Suite); 104 | 105 | const serializedKey = { 106 | type: 'Ed25519VerificationKey2018' 107 | }; 108 | 109 | const result = await cryptoLd.from(serializedKey); 110 | 111 | expect(result).to.equal(serializedKey); 112 | }); 113 | }); 114 | 115 | describe('fromKeyId()', () => { 116 | const EXAMPLE_KEY_DOCUMENT = { 117 | id: 'did:example:1234#z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3', 118 | controller: 'did:example:1234', 119 | type: 'Ed25519VerificationKey2020', 120 | publicKeyMultibase: 'zEYJrMxWigf9boyeJMTRN4Ern8DJMoCXaLK77pzQmxVjf' 121 | }; 122 | 123 | it('should error on missing keyId', async () => { 124 | let error; 125 | try { 126 | await cryptoLd.fromKeyId(); 127 | } catch(e) { 128 | error = e; 129 | } 130 | expect(error).to.exist; 131 | expect(error.message).to.equal('The "id" parameter is required.'); 132 | }); 133 | 134 | it('should error on missing documentLoader', async () => { 135 | let error; 136 | try { 137 | await cryptoLd.fromKeyId({id: 'did:ex:123#fingerprint'}); 138 | } catch(e) { 139 | error = e; 140 | } 141 | expect(error).to.exist; 142 | expect(error.message).to 143 | .equal('The "documentLoader" parameter is required.'); 144 | }); 145 | 146 | it('error if documentLoader is not properly implemented', async () => { 147 | const keyDocument = { 148 | ...EXAMPLE_KEY_DOCUMENT, type: 'ExampleKeySuite202X' 149 | }; 150 | const keyId = keyDocument.id; 151 | const documentLoader = async url => { 152 | return { 153 | url, 154 | // a properly implemented loader returns `document` not `doc` 155 | doc: keyDocument 156 | }; 157 | }; 158 | 159 | let error; 160 | try { 161 | await cryptoLd.fromKeyId({id: keyId, documentLoader}); 162 | } catch(e) { 163 | error = e; 164 | } 165 | expect(error).to.exist; 166 | expect(error.message).to.contain('function must return'); 167 | expect(error.cause.message).to.contain('function must return'); 168 | }); 169 | 170 | it('error if key document not found via documentLoader', async () => { 171 | const keyDocument = {...EXAMPLE_KEY_DOCUMENT}; 172 | const keyId = keyDocument.id; 173 | const documentLoader = async () => { 174 | throw new Error('Example fetching error.'); 175 | }; 176 | 177 | let error; 178 | try { 179 | await cryptoLd.fromKeyId({id: keyId, documentLoader}); 180 | } catch(e) { 181 | error = e; 182 | } 183 | expect(error).to.exist; 184 | expect(error.message).to 185 | .equal('Error fetching document: Example fetching error.'); 186 | expect(error.cause.message).to 187 | .equal('Example fetching error.'); 188 | }); 189 | 190 | it('should error if key suite was not installed', async () => { 191 | const keyDocument = {...EXAMPLE_KEY_DOCUMENT}; 192 | const keyId = keyDocument.id; 193 | const documentLoader = async url => { 194 | return { 195 | url, 196 | document: keyDocument 197 | }; 198 | }; 199 | 200 | let error; 201 | try { 202 | await cryptoLd.fromKeyId({id: keyId, documentLoader}); 203 | } catch(e) { 204 | error = e; 205 | } 206 | expect(error).to.exist; 207 | expect(error.message).to.equal( 208 | 'Support for suite "Ed25519VerificationKey2020" is not installed.'); 209 | }); 210 | 211 | it('should return key pair via a suite `fromKeyDocument()`', async () => { 212 | const mockKeyPair = {}; 213 | const keyDocument = { 214 | ...EXAMPLE_KEY_DOCUMENT, type: 'ExampleKeySuite202X' 215 | }; 216 | const keyId = keyDocument.id; 217 | const documentLoader = async url => { 218 | return { 219 | url, 220 | document: keyDocument 221 | }; 222 | }; 223 | const ExampleKeySuite202X = {}; 224 | ExampleKeySuite202X.suite = 'ExampleKeySuite202X'; 225 | ExampleKeySuite202X.fromKeyDocument = async () => { 226 | return mockKeyPair; 227 | }; 228 | 229 | cryptoLd.use(ExampleKeySuite202X); 230 | const result = await cryptoLd.fromKeyId({id: keyId, documentLoader}); 231 | expect(result).to.equal(mockKeyPair); 232 | }); 233 | }); 234 | }); 235 | -------------------------------------------------------------------------------- /tests/unit/LDKeyPair.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import chai from 'chai'; 5 | chai.should(); 6 | const {expect} = chai; 7 | 8 | import {LDKeyPair} from '../../lib/index.js'; 9 | 10 | describe('LDKeyPair', () => { 11 | let keyPair; 12 | beforeEach(() => { 13 | keyPair = new LDKeyPair(); 14 | }); 15 | 16 | describe('constructor', () => { 17 | it('should initialize id and controller', async () => { 18 | const controller = 'did:ex:1234'; 19 | const id = 'did:ex:1234#fingerprint'; 20 | const keyPair = new LDKeyPair({id, controller}); 21 | 22 | expect(keyPair.id).to.equal(id); 23 | expect(keyPair.controller).to.equal(controller); 24 | }); 25 | }); 26 | 27 | describe('generate()', () => { 28 | it('should throw an abstract method error', async () => { 29 | let error; 30 | 31 | try { 32 | await LDKeyPair.generate(); 33 | } catch(e) { 34 | error = e; 35 | } 36 | expect(error.message).to.match(/Abstract method/); 37 | }); 38 | }); 39 | 40 | describe('from()', () => { 41 | it('should throw an abstract method error', async () => { 42 | let error; 43 | 44 | try { 45 | await LDKeyPair.from(); 46 | } catch(e) { 47 | error = e; 48 | } 49 | expect(error.message).to.match(/Abstract method/); 50 | }); 51 | }); 52 | 53 | describe('fingerprint()', () => { 54 | it('should throw an abstract method error', async () => { 55 | expect(() => keyPair.fingerprint()).to.throw(/Abstract method/); 56 | }); 57 | }); 58 | 59 | describe('verifyFingerprint()', () => { 60 | it('should throw an abstract method error', async () => { 61 | expect(() => keyPair.verifyFingerprint('z1234')) 62 | .to.throw(/Abstract method/); 63 | }); 64 | }); 65 | 66 | describe('export()', () => { 67 | it('should error if neither private or public key specified', async () => { 68 | expect(() => { 69 | keyPair.export(); 70 | }).to.throw(/Export requires/); 71 | }); 72 | }); 73 | 74 | describe('signer()', () => { 75 | it('should return an abstract signer function', async () => { 76 | const vKeyPair = new LDKeyPair(); 77 | 78 | const {sign} = vKeyPair.signer(); 79 | let error; 80 | 81 | try { 82 | await sign({data: 'test data'}); 83 | } catch(e) { 84 | error = e; 85 | } 86 | 87 | expect(error.message).to.match(/Abstract method/); 88 | }); 89 | }); 90 | 91 | describe('verifier()', () => { 92 | it('should return an abstract verifier function', async () => { 93 | const vKeyPair = new LDKeyPair(); 94 | 95 | const {verify} = vKeyPair.verifier(); 96 | const key = {}; 97 | const signature = 'test signature'; 98 | let error; 99 | 100 | try { 101 | await verify({key, signature}); 102 | } catch(e) { 103 | error = e; 104 | } 105 | 106 | expect(error.message).to.match(/Abstract method/); 107 | }); 108 | }); 109 | 110 | describe('fromKeyDocument()', async () => { 111 | const EXAMPLE_KEY_DOCUMENT = { 112 | id: 'did:example:1234#z6MkszZtxCmA2Ce4vUV132PCuLQmwnaDD5mw2L23fGNnsiX3', 113 | controller: 'did:example:1234', 114 | type: 'Ed25519VerificationKey2020', 115 | publicKeyMultibase: 'zEYJrMxWigf9boyeJMTRN4Ern8DJMoCXaLK77pzQmxVjf' 116 | }; 117 | 118 | it('should error on missing keyId', async () => { 119 | let error; 120 | try { 121 | await LDKeyPair.fromKeyDocument(); 122 | } catch(e) { 123 | error = e; 124 | } 125 | expect(error).to.exist; 126 | expect(error.message).to.equal('The "document" parameter is required.'); 127 | }); 128 | 129 | it('should invoke the suite from() method', async () => { 130 | const keyDocument = {...EXAMPLE_KEY_DOCUMENT}; 131 | let error; 132 | try { 133 | await LDKeyPair.fromKeyDocument({document: keyDocument, 134 | checkContext: false, checkRevoked: false}); 135 | } catch(e) { 136 | error = e; 137 | } 138 | expect(error).to.exist; 139 | expect(error.message).to 140 | .equal('Abstract method from() must be implemented in subclass.'); 141 | }); 142 | 143 | it('should error on failed context check', async () => { 144 | const keyDocument = {...EXAMPLE_KEY_DOCUMENT}; 145 | let error; 146 | try { 147 | await LDKeyPair.fromKeyDocument({document: keyDocument, 148 | checkContext: true}); 149 | } catch(e) { 150 | error = e; 151 | } 152 | expect(error).to.exist; 153 | 154 | expect(error.message).to 155 | // eslint-disable-next-line max-len 156 | .equal('Key document does not contain required context "INVALID LDKeyPair CONTEXT".'); 157 | }); 158 | 159 | it('should error on failed revoked check', async () => { 160 | const pastDate = new Date(2020, 11, 17).toISOString() 161 | .replace(/\.[0-9]{3}/, ''); 162 | const keyDocument = { 163 | ...EXAMPLE_KEY_DOCUMENT, 164 | revoked: pastDate}; 165 | let error; 166 | try { 167 | await LDKeyPair.fromKeyDocument({document: keyDocument, 168 | checkContext: false, checkRevoked: true}); 169 | } catch(e) { 170 | error = e; 171 | } 172 | expect(error).to.exist; 173 | expect(error.message).to.contain('revoked'); 174 | expect(error.message).to.contain(pastDate); 175 | }); 176 | }); 177 | }); 178 | --------------------------------------------------------------------------------