├── .eslintrc.cjs ├── .github └── workflows │ └── main.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── karma.conf.cjs ├── lib ├── Ed25519Signature2020.js └── index.js ├── package.json └── test ├── .eslintrc.cjs ├── Ed25519Signature2020.spec.js ├── documentLoader.js ├── mock-data.js ├── test-mocha.js ├── test-vectors.js └── test-vectors.spec.js /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | module.exports = { 5 | root: true, 6 | extends: [ 7 | 'digitalbazaar', 8 | 'digitalbazaar/jsdoc', 9 | 'digitalbazaar/module' 10 | ], 11 | env: { 12 | node: true 13 | }, 14 | rules: { 15 | 'jsdoc/check-examples': 0, 16 | 'jsdoc/require-description-complete-sentence': 0, 17 | 'unicorn/prefer-node-protocol': 'error' 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | timeout-minutes: 10 9 | strategy: 10 | matrix: 11 | node-version: [20.x] 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Use Node.js ${{ matrix.node-version }} 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: ${{ matrix.node-version }} 18 | - run: npm install 19 | - name: Run eslint 20 | run: npm run lint 21 | test-node: 22 | needs: [lint] 23 | runs-on: ubuntu-latest 24 | timeout-minutes: 10 25 | strategy: 26 | matrix: 27 | node-version: [18.x, 20.x] 28 | steps: 29 | - uses: actions/checkout@v3 30 | - name: Use Node.js ${{ matrix.node-version }} 31 | uses: actions/setup-node@v3 32 | with: 33 | node-version: ${{ matrix.node-version }} 34 | - run: npm install 35 | - name: Run test with Node.js ${{ matrix.node-version }} 36 | run: npm run test-node 37 | test-karma: 38 | needs: [lint] 39 | runs-on: ubuntu-latest 40 | timeout-minutes: 10 41 | strategy: 42 | matrix: 43 | node-version: [20.x] 44 | steps: 45 | - uses: actions/checkout@v3 46 | - name: Use Node.js ${{ matrix.node-version }} 47 | uses: actions/setup-node@v3 48 | with: 49 | node-version: ${{ matrix.node-version }} 50 | - run: npm install 51 | - name: Run karma tests 52 | run: npm run test-karma 53 | coverage: 54 | needs: [test-node, test-karma] 55 | timeout-minutes: 10 56 | runs-on: ubuntu-latest 57 | strategy: 58 | matrix: 59 | node-version: [20.x] 60 | steps: 61 | - uses: actions/checkout@v3 62 | - name: Use Node.js ${{ matrix.node-version }} 63 | uses: actions/setup-node@v3 64 | with: 65 | node-version: ${{ matrix.node-version }} 66 | - run: npm install 67 | - name: Generate coverage report 68 | run: npm run coverage-ci 69 | - name: Upload coverage to Codecov 70 | uses: codecov/codecov-action@v4 71 | with: 72 | file: ./coverage/lcov.info 73 | fail_ci_if_error: true 74 | token: ${{ secrets.CODECOV_TOKEN }} 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # Editor files 107 | *~ 108 | *.sw[nop] 109 | 110 | # VSCode 111 | .vscode 112 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @digitalbazaar/ed25519-signature-2020 Changelog 2 | 3 | ## 5.4.0 - 2024-08-01 4 | 5 | ### Changed 6 | - Use `jsonld-signature@11.3` to get `RDFC-1.0` implementation. 7 | 8 | ## 5.3.0 - 2024-06-15 9 | 10 | ### Added 11 | - Add support for `Multikey` verification methods. 12 | 13 | ### Changed 14 | - Loosen restrictions on verification methods that do not have 15 | contexts, allowing processing of well-known types in those cases. 16 | - Allow `publiKeyJwk` to be used to express key material. 17 | 18 | ## 5.2.0 - 2023-02-13 19 | 20 | ### Removed 21 | - Remove unused `expansionMap` from `matchProof()` as it was removed 22 | from `jsonld-signatures@11` which is required since version `5.0`. 23 | 24 | ## 5.1.0 - 2023-02-07 25 | 26 | ### Added 27 | - Allow custom `canonizeOptions` to be passed in the construction of 28 | a suite as a stop-gap until hard requirements for canonize options 29 | are either set or advised to be certain values by a W3C working group. 30 | 31 | ## 5.0.0 - 2022-08-23 32 | 33 | ### Changed 34 | - **BREAKING**: Use `jsonld-signatures@11` to get better safe mode 35 | protections when canonizing. 36 | 37 | ## 4.0.1 - 2022-06-06 38 | 39 | ### Changed 40 | - Update to jsonld-signatures@10. 41 | 42 | ## 4.0.0 - 2022-06-06 43 | 44 | ### Changed 45 | - **BREAKING**: Convert to module (ESM). 46 | - **BREAKING**: Require Node.js >=14. 47 | - Update dependencies. 48 | - Lint module. 49 | 50 | ## 3.0.0 - 2021-06-19 51 | 52 | ### Fixed 53 | 54 | - **BREAKING**: Update to use new Ed25519VerificationKey2020 multicodec 55 | encoded key formats. 56 | 57 | ## 2.2.0 - 2021-05-26 58 | 59 | ### Added 60 | - It is now possible to verify `Ed25519Signature2020` proofs using using 61 | 2018 keys. 62 | 63 | ### Changed 64 | - Replace `@transmute/jsonld-document-loader` with 65 | `@digitalbazaar/security-document-loader` in test. 66 | 67 | ## 2.1.0 - 2021-04-09 68 | 69 | ### Added 70 | - Export the suite's context (and related objects such as context url, 71 | documentLoader, etc), and also set them as a property of the suite class. 72 | - Set the `contextUrl` property on suite instance, to support context 73 | enforcement during the `sign()` operation that was added to `jsonld-signatures` 74 | `v9.0.1`. 75 | 76 | ## 2.0.1 - 2021-04-09 77 | 78 | ### Changed 79 | - Use `ed25519-verification-key-2020@2.1.1`. Signer now has an "id" property. 80 | 81 | ## 2.0.0 - 2021-04-06 82 | 83 | ### Changed 84 | - **BREAKING**: Update to use `jsonld-signatures` v9.0 (removes 85 | `verificationMethod` suite constructor param, makes key and signer validation 86 | stricter). 87 | - Fix initializing signer and verifier object by passing it to superclass. 88 | 89 | ## 1.0.0 - 2021-03-19 90 | 91 | ### Added 92 | - Initial files. 93 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Digital Bazaar, Inc. 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ed25519Signature2020 suite _(@digitalbazaar/ed25519-signature-2020)_ 2 | 3 | [![Build status](https://img.shields.io/github/actions/workflow/status/digitalbazaar/ed25519-signature-2020/main.yml?branch=main)](https://github.com/digitalbazaar/ed25519-signature-2020/actions?query=workflow%3A%22Node.js+CI%22) 4 | [![Coverage status](https://img.shields.io/codecov/c/github/digitalbazaar/ed25519-signature-2020)](https://codecov.io/gh/digitalbazaar/ed25519-signature-2020) 5 | [![NPM Version](https://img.shields.io/npm/v/@digitalbazaar/ed25519-signature-2020.svg)](https://npm.im/digitalbazaar/ed25519-signature-2020) 6 | 7 | > Ed25519Signature2020 Linked Data Proof suite for use with jsonld-signatures. 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 | For use with https://github.com/digitalbazaar/jsonld-signatures v9.0 and above. 22 | 23 | See also related specs: 24 | 25 | * [Ed25519Signature2020 Crypto Suite](https://w3c.github.io/vc-di-eddsa/) 26 | 27 | ## Security 28 | 29 | TBD 30 | 31 | ## Install 32 | 33 | - Browsers and Node.js 14+ are supported. 34 | 35 | To install from NPM: 36 | 37 | ``` 38 | npm install @digitalbazaar/ed25519-signature-2020 39 | ``` 40 | 41 | To install locally (for development): 42 | 43 | ``` 44 | git clone https://github.com/digitalbazaar/ed25519-signature-2020.git 45 | cd ed25519-signature-2020 46 | npm install 47 | ``` 48 | 49 | ## Usage 50 | 51 | The following code snippet provides a complete example of digitally signing 52 | a verifiable credential using this library: 53 | 54 | ```javascript 55 | import jsigs from 'jsonld-signatures'; 56 | const {purposes: {AssertionProofPurpose}} = jsigs; 57 | import {Ed25519VerificationKey2020} from 58 | '@digitalbazaar/ed25519-verification-key-2020'; 59 | import {Ed25519Signature2020, suiteContext} from 60 | '@digitalbazaar/ed25519-signature-2020'; 61 | 62 | // create the unsigned credential 63 | const unsignedCredential = { 64 | '@context': [ 65 | 'https://www.w3.org/2018/credentials/v1', 66 | { 67 | AlumniCredential: 'https://schema.org#AlumniCredential', 68 | alumniOf: 'https://schema.org#alumniOf' 69 | } 70 | ], 71 | id: 'http://example.edu/credentials/1872', 72 | type: [ 'VerifiableCredential', 'AlumniCredential' ], 73 | issuer: 'https://example.edu/issuers/565049', 74 | issuanceDate: '2010-01-01T19:23:24Z', 75 | credentialSubject: { 76 | id: 'https://example.edu/students/alice', 77 | alumniOf: 'Example University' 78 | } 79 | }; 80 | 81 | // create the keypair to use when signing 82 | const controller = 'https://example.edu/issuers/565049'; 83 | const keyPair = await Ed25519VerificationKey2020.from({ 84 | type: 'Ed25519VerificationKey2020', 85 | controller, 86 | id: controller + '#z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T', 87 | publicKeyMultibase: 'z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T', 88 | privateKeyMultibase: 'zrv2EET2WWZ8T1Jbg4fEH5cQxhbUS22XxdweypUbjWVzv1YD6VqYu' + 89 | 'W6LH7heQCNYQCuoKaDwvv2qCWz3uBzG2xesqmf' 90 | }); 91 | 92 | const suite = new Ed25519Signature2020({key: keyPair}); 93 | suite.date = '2010-01-01T19:23:24Z'; 94 | 95 | signedCredential = await jsigs.sign(unsignedCredential, { 96 | suite, 97 | purpose: new AssertionProofPurpose(), 98 | documentLoader 99 | }); 100 | 101 | // results in the following signed VC 102 | { 103 | "@context": [ 104 | "https://www.w3.org/2018/credentials/v1", 105 | { 106 | "AlumniCredential": "https://schema.org#AlumniCredential", 107 | "alumniOf": "https://schema.org#alumniOf" 108 | }, 109 | "https://w3id.org/security/suites/ed25519-2020/v1" 110 | ], 111 | "id": "http://example.edu/credentials/1872", 112 | "type": ["VerifiableCredential", "AlumniCredential"], 113 | "issuer": "https://example.edu/issuers/565049", 114 | "issuanceDate": "2010-01-01T19:23:24Z", 115 | "credentialSubject": { 116 | "id": "https://example.edu/students/alice", 117 | "alumniOf": "Example University" 118 | }, 119 | "proof": { 120 | "type": "Ed25519Signature2020", 121 | "created": "2010-01-01T19:23:24Z", 122 | "verificationMethod": "https://example.edu/issuers/565049#z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T", 123 | "proofPurpose": "assertionMethod", 124 | "proofValue": "z3MvGcVxzRzzpKF1HA11EjvfPZsN8NAb7kXBRfeTm3CBg2gcJLQM5hZNmj6Ccd9Lk4C1YueiFZvkSx4FuHVYVouQk" 125 | } 126 | } 127 | ``` 128 | 129 | ## Contribute 130 | 131 | See [the contribute file](https://github.com/digitalbazaar/bedrock/blob/master/CONTRIBUTING.md)! 132 | 133 | PRs accepted. 134 | 135 | If editing the Readme, please conform to the 136 | [standard-readme](https://github.com/RichardLitt/standard-readme) specification. 137 | 138 | ## Commercial Support 139 | 140 | Commercial support for this library is available upon request from 141 | Digital Bazaar: support@digitalbazaar.com 142 | 143 | ## License 144 | 145 | [New BSD License (3-clause)](LICENSE) © 2020 Digital Bazaar 146 | -------------------------------------------------------------------------------- /karma.conf.cjs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | module.exports = function(config) { 5 | 6 | config.set({ 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: '', 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ['mocha', 'chai'], 13 | 14 | // list of files / patterns to load in the browser 15 | files: [ 16 | 'test/*.spec.js' 17 | ], 18 | 19 | // list of files to exclude 20 | exclude: [], 21 | 22 | // preprocess matching files before serving them to the browser 23 | // preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 24 | preprocessors: { 25 | 'test/*.js': ['webpack', 'sourcemap'] 26 | }, 27 | 28 | webpack: { 29 | mode: 'development', 30 | devtool: 'inline-source-map' 31 | }, 32 | 33 | // test results reporter to use 34 | // possible values: 'dots', 'progress' 35 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 36 | //reporters: ['progress'], 37 | reporters: ['mocha'], 38 | 39 | // web server port 40 | port: 9876, 41 | 42 | // enable / disable colors in the output (reporters and logs) 43 | colors: true, 44 | 45 | // level of logging 46 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || 47 | // config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 48 | logLevel: config.LOG_INFO, 49 | 50 | // enable / disable watching file and executing tests whenever any 51 | // file changes 52 | autoWatch: false, 53 | 54 | // start these browsers 55 | // browser launchers: https://npmjs.org/browse/keyword/karma-launcher 56 | //browsers: ['ChromeHeadless', 'Chrome', 'Firefox', 'Safari'], 57 | browsers: ['ChromeHeadless'], 58 | 59 | // Continuous Integration mode 60 | // if true, Karma captures browsers, runs the tests and exits 61 | singleRun: true, 62 | 63 | // Concurrency level 64 | // how many browser should be started simultaneous 65 | concurrency: Infinity, 66 | 67 | // Mocha 68 | client: { 69 | mocha: { 70 | // increase from default 2s 71 | timeout: 10000, 72 | reporter: 'html' 73 | //delay: true 74 | } 75 | } 76 | }); 77 | }; 78 | -------------------------------------------------------------------------------- /lib/Ed25519Signature2020.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import * as base58btc from 'base58-universal'; 5 | import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey'; 6 | import { 7 | Ed25519VerificationKey2020 8 | } from '@digitalbazaar/ed25519-verification-key-2020'; 9 | import jsigs from 'jsonld-signatures'; 10 | const {suites: {LinkedDataSignature}} = jsigs; 11 | import suiteContext2020 from 'ed25519-signature-2020-context'; 12 | 13 | // 'https://w3id.org/security/suites/ed25519-2020/v1' 14 | const SUITE_CONTEXT_URL = suiteContext2020.constants.CONTEXT_URL; 15 | 16 | // multibase base58-btc header 17 | const MULTIBASE_BASE58BTC_HEADER = 'z'; 18 | 19 | export class Ed25519Signature2020 extends LinkedDataSignature { 20 | /** 21 | * @param {object} options - Options hashmap. 22 | * 23 | * Either a `key` OR at least one of `signer`/`verifier` is required: 24 | * 25 | * @param {object} [options.key] - An optional key object (containing an 26 | * `id` property, and either `signer` or `verifier`, depending on the 27 | * intended operation. Useful for when the application is managing keys 28 | * itself (when using a KMS, you never have access to the private key, 29 | * and so should use the `signer` param instead). 30 | * @param {Function} [options.signer] - Signer function that returns an 31 | * object with an async sign() method. This is useful when interfacing 32 | * with a KMS (since you don't get access to the private key and its 33 | * `signer()`, the KMS client gives you only the signer function to use). 34 | * @param {Function} [options.verifier] - Verifier function that returns 35 | * an object with an async `verify()` method. Useful when working with a 36 | * KMS-provided verifier function. 37 | * 38 | * Advanced optional parameters and overrides: 39 | * 40 | * @param {object} [options.proof] - A JSON-LD document with options to use 41 | * for the `proof` node (e.g. any other custom fields can be provided here 42 | * using a context different from security-v2). 43 | * @param {string|Date} [options.date] - Signing date to use if not passed. 44 | * @param {boolean} [options.useNativeCanonize] - Whether to use a native 45 | * canonize algorithm. 46 | * @param {object} [options.canonizeOptions] - Options to pass to 47 | * canonize algorithm. 48 | */ 49 | constructor({ 50 | key, signer, verifier, proof, date, useNativeCanonize, canonizeOptions 51 | } = {}) { 52 | super({ 53 | type: 'Ed25519Signature2020', LDKeyClass: Ed25519VerificationKey2020, 54 | contextUrl: SUITE_CONTEXT_URL, 55 | key, signer, verifier, proof, date, useNativeCanonize, 56 | canonizeOptions 57 | }); 58 | // some operations may be performed with `Ed25519VerificationKey2018` or 59 | // `Multikey`; so, `Ed25519VerificationKey2020` is recommended, but not 60 | // strictly required 61 | this.requiredKeyType = 'Ed25519VerificationKey2020'; 62 | } 63 | 64 | /** 65 | * Adds a signature (proofValue) field to the proof object. Called by 66 | * LinkedDataSignature.createProof(). 67 | * 68 | * @param {object} options - The options to use. 69 | * @param {Uint8Array} options.verifyData - Data to be signed (extracted 70 | * from document, according to the suite's spec). 71 | * @param {object} options.proof - Proof object (containing the proofPurpose, 72 | * verificationMethod, etc). 73 | * 74 | * @returns {Promise} Resolves with the proof containing the signature 75 | * value. 76 | */ 77 | async sign({verifyData, proof}) { 78 | if(!(this.signer && typeof this.signer.sign === 'function')) { 79 | throw new Error('A signer API has not been specified.'); 80 | } 81 | 82 | const signatureBytes = await this.signer.sign({data: verifyData}); 83 | proof.proofValue = 84 | MULTIBASE_BASE58BTC_HEADER + base58btc.encode(signatureBytes); 85 | 86 | return proof; 87 | } 88 | 89 | /** 90 | * Verifies the proof signature against the given data. 91 | * 92 | * @param {object} options - The options to use. 93 | * @param {Uint8Array} options.verifyData - Canonicalized hashed data. 94 | * @param {object} options.verificationMethod - Key object. 95 | * @param {object} options.proof - The proof to be verified. 96 | * 97 | * @returns {Promise} Resolves with the verification result. 98 | */ 99 | async verifySignature({verifyData, verificationMethod, proof}) { 100 | const {proofValue} = proof; 101 | if(!(proofValue && typeof proofValue === 'string')) { 102 | throw new TypeError( 103 | 'The proof does not include a valid "proofValue" property.'); 104 | } 105 | if(proofValue[0] !== MULTIBASE_BASE58BTC_HEADER) { 106 | throw new Error('Only base58btc multibase encoding is supported.'); 107 | } 108 | const signatureBytes = base58btc.decode(proofValue.substr(1)); 109 | 110 | let {verifier} = this; 111 | if(!verifier) { 112 | const key = await Ed25519Multikey.from(verificationMethod); 113 | verifier = key.verifier(); 114 | } 115 | return verifier.verify({data: verifyData, signature: signatureBytes}); 116 | } 117 | 118 | async assertVerificationMethod({verificationMethod}) { 119 | let contextUrl; 120 | if(verificationMethod.type === 'Ed25519VerificationKey2020') { 121 | contextUrl = SUITE_CONTEXT_URL; 122 | } else { 123 | throw new Error(`Unsupported key type "${verificationMethod.type}".`); 124 | } 125 | if(!_includesContext({document: verificationMethod, contextUrl})) { 126 | // For DID Documents, since keys do not have their own contexts, 127 | // the suite context is usually provided by the documentLoader logic 128 | throw new TypeError( 129 | `The verification method (key) must contain "${contextUrl}" context.` 130 | ); 131 | } 132 | 133 | // ensure verification method has not been revoked 134 | if(verificationMethod.revoked !== undefined) { 135 | throw new Error('The verification method has been revoked.'); 136 | } 137 | } 138 | 139 | async getVerificationMethod({proof, documentLoader}) { 140 | if(this.key) { 141 | // This happens most often during sign() operations. For verify(), 142 | // the expectation is that the verification method will be fetched 143 | // by the documentLoader (below), not provided as a `key` parameter. 144 | return this.key.export({publicKey: true}); 145 | } 146 | 147 | let {verificationMethod} = proof; 148 | 149 | if(typeof verificationMethod === 'object') { 150 | verificationMethod = verificationMethod.id; 151 | } 152 | 153 | if(!verificationMethod) { 154 | throw new Error('No "verificationMethod" found in proof.'); 155 | } 156 | 157 | const {document} = await documentLoader(verificationMethod); 158 | 159 | verificationMethod = typeof document === 'string' ? 160 | JSON.parse(document) : document; 161 | 162 | // for maximum compatibility, import using multikey library and convert to 163 | // type `Ed25519Signature2020` 164 | const key = await Ed25519Multikey.from(verificationMethod); 165 | verificationMethod = { 166 | ...await key.export({publicKey: true, includeContext: true}), 167 | '@context': SUITE_CONTEXT_URL, 168 | type: 'Ed25519VerificationKey2020' 169 | }; 170 | await this.assertVerificationMethod({verificationMethod}); 171 | 172 | return verificationMethod; 173 | } 174 | 175 | async matchProof({proof, document, purpose, documentLoader}) { 176 | if(!_includesContext({document, contextUrl: SUITE_CONTEXT_URL})) { 177 | return false; 178 | } 179 | 180 | if(!await super.matchProof({proof, document, purpose, documentLoader})) { 181 | return false; 182 | } 183 | if(!this.key) { 184 | // no key specified, so assume this suite matches and it can be retrieved 185 | return true; 186 | } 187 | 188 | const {verificationMethod} = proof; 189 | 190 | // only match if the key specified matches the one in the proof 191 | if(typeof verificationMethod === 'object') { 192 | return verificationMethod.id === this.key.id; 193 | } 194 | return verificationMethod === this.key.id; 195 | } 196 | } 197 | 198 | /** 199 | * Tests whether a provided JSON-LD document includes a context url in its 200 | * `@context` property. 201 | * 202 | * @param {object} options - Options hashmap. 203 | * @param {object} options.document - A JSON-LD document. 204 | * @param {string} options.contextUrl - A context url. 205 | * 206 | * @returns {boolean} Returns true if document includes context. 207 | */ 208 | function _includesContext({document, contextUrl}) { 209 | const context = document['@context']; 210 | return context === contextUrl || 211 | (Array.isArray(context) && context.includes(contextUrl)); 212 | } 213 | 214 | Ed25519Signature2020.CONTEXT_URL = SUITE_CONTEXT_URL; 215 | Ed25519Signature2020.CONTEXT = suiteContext2020.contexts.get(SUITE_CONTEXT_URL); 216 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2020-2021 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import suiteContext from 'ed25519-signature-2020-context'; 5 | 6 | export {Ed25519Signature2020} from './Ed25519Signature2020.js'; 7 | export {suiteContext}; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@digitalbazaar/ed25519-signature-2020", 3 | "version": "5.4.1-0", 4 | "description": "Ed25519Signature2020 Linked Data Proof suite for use with jsonld-signatures.", 5 | "homepage": "https://github.com/digitalbazaar/ed25519-signature-2020", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/digitalbazaar/ed25519-signature-2020" 9 | }, 10 | "license": "BSD-3-Clause", 11 | "type": "module", 12 | "exports": "./lib/index.js", 13 | "files": [ 14 | "lib/**/*.js" 15 | ], 16 | "dependencies": { 17 | "@digitalbazaar/ed25519-multikey": "^1.2.0", 18 | "@digitalbazaar/ed25519-verification-key-2020": "^4.1.0", 19 | "base58-universal": "^2.0.0", 20 | "ed25519-signature-2020-context": "^1.1.0", 21 | "jsonld-signatures": "^11.3.0" 22 | }, 23 | "devDependencies": { 24 | "@digitalbazaar/ed25519-verification-key-2018": "^4.0.0", 25 | "@digitalbazaar/security-document-loader": "^3.0.0", 26 | "c8": "^7.12.0", 27 | "chai": "^4.3.7", 28 | "cross-env": "^7.0.3", 29 | "ed25519-signature-2018-context": "^1.1.0", 30 | "eslint": "^8.34.0", 31 | "eslint-config-digitalbazaar": "^5.2.0", 32 | "eslint-plugin-jsdoc": "^50.2.2", 33 | "eslint-plugin-unicorn": "^55.0.0", 34 | "karma": "^6.4.1", 35 | "karma-chai": "^0.1.0", 36 | "karma-chrome-launcher": "^3.1.1", 37 | "karma-mocha": "^2.0.1", 38 | "karma-mocha-reporter": "^2.2.5", 39 | "karma-sourcemap-loader": "^0.4.0", 40 | "karma-webpack": "^5.0.0", 41 | "mocha": "^10.2.0", 42 | "mocha-lcov-reporter": "^1.3.0", 43 | "webpack": "^5.75.0" 44 | }, 45 | "engines": { 46 | "node": ">=18" 47 | }, 48 | "scripts": { 49 | "test": "npm run test-node", 50 | "test-karma": "karma start karma.conf.cjs", 51 | "test-node": "cross-env NODE_ENV=test mocha --preserve-symlinks -t 30000 -A -R ${REPORTER:-spec} --require test/test-mocha.js test/*.spec.js", 52 | "coverage": "cross-env NODE_ENV=test c8 npm run test-node", 53 | "coverage-ci": "cross-env NODE_ENV=test c8 --reporter=lcovonly --reporter=text-summary --reporter=text npm run test-node", 54 | "coverage-report": "c8 report", 55 | "lint": "eslint ." 56 | }, 57 | "c8": { 58 | "reporter": [ 59 | "lcov", 60 | "text-summary", 61 | "text" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2022 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | module.exports = { 5 | globals: { 6 | should: true, 7 | assertNoError: true 8 | }, 9 | env: { 10 | node: true, 11 | mocha: true 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /test/Ed25519Signature2020.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import {expect} from 'chai'; 5 | 6 | import jsigs from 'jsonld-signatures'; 7 | const {purposes: {AssertionProofPurpose}} = jsigs; 8 | 9 | import { 10 | Ed25519VerificationKey2020 11 | } from '@digitalbazaar/ed25519-verification-key-2020'; 12 | 13 | import { 14 | controllerDoc2018, 15 | credential, 16 | mockKeyPair2018, 17 | mockKeyPair2020, 18 | mockPublicKey2018 19 | } from './mock-data.js'; 20 | import {Ed25519Signature2020, suiteContext} from '../lib/index.js'; 21 | import { 22 | Ed25519VerificationKey2018 23 | } from '@digitalbazaar/ed25519-verification-key-2018'; 24 | import {loader} from './documentLoader.js'; 25 | 26 | const documentLoader = loader.build(); 27 | 28 | const poisonData = []; 29 | for(let i = 0; i < 10; ++i) { 30 | poisonData.push({ 31 | alumniOf: new Array(10).fill({alumniOf: 'poison'}) 32 | }); 33 | } 34 | 35 | describe('Ed25519Signature2020', () => { 36 | describe('exports', () => { 37 | it('it should have proper exports', async () => { 38 | should.exist(Ed25519Signature2020); 39 | should.exist(suiteContext); 40 | suiteContext.should.have.keys([ 41 | 'appContextMap', 42 | 'constants', 43 | 'contexts', 44 | 'documentLoader', 45 | 'CONTEXT', 46 | 'CONTEXT_URL' 47 | ]); 48 | should.exist(Ed25519Signature2020.CONTEXT_URL); 49 | Ed25519Signature2020.CONTEXT_URL.should 50 | .equal(suiteContext.constants.CONTEXT_URL); 51 | const context = Ed25519Signature2020.CONTEXT; 52 | context.should.exist; 53 | context['@context'].id.should.equal('@id'); 54 | }); 55 | }); 56 | 57 | describe('sign() and verify()', () => { 58 | it('should sign a document with a key pair', async () => { 59 | const unsignedCredential = {...credential}; 60 | const keyPair = await Ed25519VerificationKey2020.from({ 61 | ...mockKeyPair2020 62 | }); 63 | const suite = new Ed25519Signature2020({key: keyPair}); 64 | suite.date = '2010-01-01T19:23:24Z'; 65 | 66 | const signedCredential = await jsigs.sign(unsignedCredential, { 67 | suite, 68 | purpose: new AssertionProofPurpose(), 69 | documentLoader 70 | }); 71 | expect(signedCredential).to.have.property('proof'); 72 | expect(signedCredential.proof.proofValue).to 73 | .equal('z3MvGcVxzRzzpKF1HA11EjvfPZsN8NAb7kXBRfeTm3CBg2gcJLQM5hZNmj6Cc' + 74 | 'd9Lk4C1YueiFZvkSx4FuHVYVouQk'); 75 | }); 76 | 77 | it('should fail to sign a document with a poison graph', async () => { 78 | const unsignedCredential = {...credential}; 79 | const keyPair = await Ed25519VerificationKey2020.from({ 80 | ...mockKeyPair2020 81 | }); 82 | const suite = new Ed25519Signature2020({ 83 | key: keyPair 84 | }); 85 | suite.date = '2010-01-01T19:23:24Z'; 86 | unsignedCredential.alumniOf = poisonData; 87 | 88 | let error; 89 | try { 90 | await jsigs.sign(unsignedCredential, { 91 | suite, 92 | purpose: new AssertionProofPurpose(), 93 | documentLoader 94 | }); 95 | } catch(e) { 96 | error = e; 97 | } 98 | expect(error).to.exist; 99 | expect(error.message).to.include('Maximum deep iterations'); 100 | }); 101 | 102 | it('should fail to sign with undefined term', async () => { 103 | const unsignedCredential = JSON.parse(JSON.stringify(credential)); 104 | unsignedCredential.undefinedTerm = 'foo'; 105 | const keyPair = await Ed25519VerificationKey2020.from({ 106 | ...mockKeyPair2020 107 | }); 108 | const suite = new Ed25519Signature2020({key: keyPair}); 109 | suite.date = '2010-01-01T19:23:24Z'; 110 | 111 | let error; 112 | try { 113 | await jsigs.sign(unsignedCredential, { 114 | suite, 115 | purpose: new AssertionProofPurpose(), 116 | documentLoader 117 | }); 118 | } catch(e) { 119 | error = e; 120 | } 121 | expect(error).to.exist; 122 | expect(error.name).to.equal('jsonld.ValidationError'); 123 | }); 124 | 125 | it('should fail to sign with relative type URL', async () => { 126 | const unsignedCredential = JSON.parse(JSON.stringify(credential)); 127 | unsignedCredential.type.push('UndefinedType'); 128 | const keyPair = await Ed25519VerificationKey2020.from({ 129 | ...mockKeyPair2020 130 | }); 131 | const suite = new Ed25519Signature2020({key: keyPair}); 132 | suite.date = '2010-01-01T19:23:24Z'; 133 | 134 | let error; 135 | try { 136 | await jsigs.sign(unsignedCredential, { 137 | suite, 138 | purpose: new AssertionProofPurpose(), 139 | documentLoader 140 | }); 141 | } catch(e) { 142 | error = e; 143 | } 144 | expect(error).to.exist; 145 | expect(error.name).to.equal('jsonld.ValidationError'); 146 | }); 147 | 148 | it('signs a document given a signer object', async () => { 149 | const unsignedCredential = {...credential}; 150 | const keyPair = await Ed25519VerificationKey2020.from({ 151 | ...mockKeyPair2020 152 | }); 153 | 154 | // Note: Typically a signer object comes from a KMS; mocking it here 155 | const signer = keyPair.signer(); 156 | 157 | const suite = new Ed25519Signature2020({signer}); 158 | suite.date = '2010-01-01T19:23:24Z'; 159 | 160 | const signedCredential = await jsigs.sign(unsignedCredential, { 161 | suite, 162 | purpose: new AssertionProofPurpose(), 163 | documentLoader 164 | }); 165 | 166 | expect(signedCredential).to.have.property('proof'); 167 | expect(signedCredential.proof.proofValue).to 168 | .equal('z3MvGcVxzRzzpKF1HA11EjvfPZsN8NAb7kXBRfeTm3CBg2gcJLQM5hZNmj6Cc' + 169 | 'd9Lk4C1YueiFZvkSx4FuHVYVouQk'); 170 | }); 171 | 172 | it('should throw error if "signer" is not specified', async () => { 173 | const unsignedCredential = {...credential}; 174 | let signedCredential; 175 | // No key, no signer object given 176 | const suite = new Ed25519Signature2020(); 177 | let err; 178 | try { 179 | signedCredential = await jsigs.sign(unsignedCredential, { 180 | suite, 181 | purpose: new AssertionProofPurpose(), 182 | documentLoader 183 | }); 184 | } catch(e) { 185 | err = e; 186 | } 187 | expect(signedCredential).to.equal(undefined); 188 | expect(err.name).to.equal('Error'); 189 | expect(err.message).to.equal('A signer API has not been specified.'); 190 | }); 191 | 192 | it('should add the suite context by default', async () => { 193 | const unsignedCredential = {...credential}; 194 | unsignedCredential['@context'] = [ 195 | 'https://www.w3.org/2018/credentials/v1', 196 | { 197 | AlumniCredential: 'https://schema.org#AlumniCredential', 198 | alumniOf: 'https://schema.org#alumniOf' 199 | } 200 | // do not include the suite-specific context 201 | ]; 202 | 203 | const keyPair = await Ed25519VerificationKey2020.from({ 204 | ...mockKeyPair2020 205 | }); 206 | const suite = new Ed25519Signature2020({key: keyPair}); 207 | 208 | const signedCredential = await jsigs.sign(unsignedCredential, { 209 | suite, 210 | purpose: new AssertionProofPurpose(), 211 | documentLoader 212 | }); 213 | 214 | expect(signedCredential['@context']).to.eql([ 215 | 'https://www.w3.org/2018/credentials/v1', 216 | { 217 | AlumniCredential: 'https://schema.org#AlumniCredential', 218 | alumniOf: 'https://schema.org#alumniOf' 219 | }, 220 | 'https://w3id.org/security/suites/ed25519-2020/v1' 221 | ]); 222 | }); 223 | 224 | it('should error if no context and addSuiteContext false', async () => { 225 | const unsignedCredential = {...credential}; 226 | unsignedCredential['@context'] = [ 227 | 'https://www.w3.org/2018/credentials/v1', 228 | 'https://www.w3.org/2018/credentials/examples/v1' 229 | // do not include the suite-specific context 230 | ]; 231 | 232 | const keyPair = await Ed25519VerificationKey2020.from({ 233 | ...mockKeyPair2020 234 | }); 235 | const suite = new Ed25519Signature2020({key: keyPair}); 236 | 237 | let err; 238 | try { 239 | await jsigs.sign(unsignedCredential, { 240 | suite, 241 | purpose: new AssertionProofPurpose(), 242 | documentLoader, 243 | addSuiteContext: false 244 | }); 245 | } catch(e) { 246 | err = e; 247 | } 248 | expect(err.name).to.equal('TypeError'); 249 | expect(err.message).to 250 | .match(/The document to be signed must contain this suite's @context/); 251 | }); 252 | }); 253 | 254 | describe('verify() 2020 key type', () => { 255 | let signedCredential; 256 | 257 | before(async () => { 258 | const unsignedCredential = {...credential}; 259 | const keyPair = await Ed25519VerificationKey2020.from({ 260 | ...mockKeyPair2020 261 | }); 262 | const suite = new Ed25519Signature2020({key: keyPair}); 263 | suite.date = '2010-01-01T19:23:24Z'; 264 | 265 | signedCredential = await jsigs.sign(unsignedCredential, { 266 | suite, 267 | purpose: new AssertionProofPurpose(), 268 | documentLoader 269 | }); 270 | }); 271 | 272 | it('should verify a document', async () => { 273 | const suite = new Ed25519Signature2020(); 274 | const result = await jsigs.verify(signedCredential, { 275 | suite, 276 | purpose: new AssertionProofPurpose(), 277 | documentLoader 278 | }); 279 | expect(result.verified).to.be.true; 280 | }); 281 | 282 | it('should fail to verify a document with a poison graph', async () => { 283 | const poisonCredential = {...signedCredential}; 284 | const suite = new Ed25519Signature2020(); 285 | poisonCredential.alumniOf = poisonData; 286 | 287 | const result = await jsigs.verify(poisonCredential, { 288 | suite, 289 | purpose: new AssertionProofPurpose(), 290 | documentLoader 291 | }); 292 | expect(result.verified).to.be.false; 293 | const {error} = result.results[0]; 294 | expect(error.message).to.include('Maximum deep iterations'); 295 | }); 296 | 297 | it('should fail verification if "proofValue" is not string', 298 | async () => { 299 | const suite = new Ed25519Signature2020(); 300 | const signedCredentialCopy = 301 | JSON.parse(JSON.stringify(signedCredential)); 302 | // intentionally modify proofValue type to not be string 303 | signedCredentialCopy.proof.proofValue = {}; 304 | 305 | const result = await jsigs.verify(signedCredentialCopy, { 306 | suite, 307 | purpose: new AssertionProofPurpose(), 308 | documentLoader 309 | }); 310 | 311 | const {error} = result.results[0]; 312 | expect(result.verified).to.be.false; 313 | expect(error.name).to.equal('TypeError'); 314 | expect(error.message).to.equal( 315 | 'The proof does not include a valid "proofValue" property.' 316 | ); 317 | }); 318 | 319 | it('should fail verification if "proofValue" is not given', 320 | async () => { 321 | const suite = new Ed25519Signature2020(); 322 | const signedCredentialCopy = 323 | JSON.parse(JSON.stringify(signedCredential)); 324 | // intentionally modify proofValue to be undefined 325 | signedCredentialCopy.proof.proofValue = undefined; 326 | 327 | const result = await jsigs.verify(signedCredentialCopy, { 328 | suite, 329 | purpose: new AssertionProofPurpose(), 330 | documentLoader 331 | }); 332 | 333 | const {error} = result.results[0]; 334 | 335 | expect(result.verified).to.be.false; 336 | expect(error.name).to.equal('TypeError'); 337 | expect(error.message).to.equal( 338 | 'The proof does not include a valid "proofValue" property.' 339 | ); 340 | }); 341 | 342 | it('should fail verification if proofValue string does not start with "z"', 343 | async () => { 344 | const suite = new Ed25519Signature2020(); 345 | const signedCredentialCopy = 346 | JSON.parse(JSON.stringify(signedCredential)); 347 | // intentionally modify proofValue to not start with 'z' 348 | signedCredentialCopy.proof.proofValue = 'a'; 349 | 350 | const result = await jsigs.verify(signedCredentialCopy, { 351 | suite, 352 | purpose: new AssertionProofPurpose(), 353 | documentLoader 354 | }); 355 | 356 | const {errors} = result.error; 357 | 358 | expect(result.verified).to.be.false; 359 | expect(errors[0].name).to.equal('Error'); 360 | expect(errors[0].message).to.equal( 361 | 'Only base58btc multibase encoding is supported.' 362 | ); 363 | }); 364 | 365 | it('should fail verification if proof type is not Ed25519Signature2020', 366 | async () => { 367 | const suite = new Ed25519Signature2020(); 368 | const signedCredentialCopy = 369 | JSON.parse(JSON.stringify(signedCredential)); 370 | // intentionally modify proof type to be Ed25519Signature2018 371 | signedCredentialCopy.proof.type = 'Ed25519Signature2018'; 372 | 373 | const result = await jsigs.verify(signedCredentialCopy, { 374 | suite, 375 | purpose: new AssertionProofPurpose(), 376 | documentLoader 377 | }); 378 | 379 | const {errors} = result.error; 380 | 381 | expect(result.verified).to.be.false; 382 | expect(errors[0].name).to.equal('NotFoundError'); 383 | }); 384 | }); 385 | describe('verify() 2018 key type', () => { 386 | let signedCredential; 387 | 388 | before(async () => { 389 | const unsignedCredential = {...credential}; 390 | const keyPair = await Ed25519VerificationKey2018.from({ 391 | ...mockKeyPair2018 392 | }); 393 | const suite = new Ed25519Signature2020({key: keyPair}); 394 | suite.date = '2010-01-01T19:23:24Z'; 395 | 396 | signedCredential = await jsigs.sign(unsignedCredential, { 397 | suite, 398 | purpose: new AssertionProofPurpose(), 399 | documentLoader 400 | }); 401 | }); 402 | 403 | it('should verify when verificationMethod contains 2018 key and context', 404 | async () => { 405 | loader.addStatic(mockKeyPair2018.controller, controllerDoc2018); 406 | loader.addStatic(mockPublicKey2018.id, mockPublicKey2018); 407 | const documentLoader = loader.build(); 408 | const suite = new Ed25519Signature2020(); 409 | const result = await jsigs.verify(signedCredential, { 410 | suite, 411 | purpose: new AssertionProofPurpose(), 412 | documentLoader 413 | }); 414 | expect(result.verified).to.be.true; 415 | }); 416 | it('should throw error when verification method contains 2018 key ' + 417 | 'with (not-matching) 2020 context', async () => { 418 | const mockPublicKey2018With2020Context = {...mockPublicKey2018}; 419 | // intentionally modify the context to ed25519 2020 context 420 | mockPublicKey2018With2020Context['@context'] = 421 | 'https://w3id.org/security/suites/ed25519-2020/v1'; 422 | loader.addStatic(mockKeyPair2018.controller, controllerDoc2018); 423 | loader.addStatic(mockPublicKey2018With2020Context.id, 424 | mockPublicKey2018With2020Context); 425 | const documentLoader = loader.build(); 426 | const suite = new Ed25519Signature2020(); 427 | const result = await jsigs.verify(signedCredential, { 428 | suite, 429 | purpose: new AssertionProofPurpose(), 430 | documentLoader 431 | }); 432 | expect(result.verified).to.be.false; 433 | expect(result.results[0].error.message).equal( 434 | 'Context not supported ' + 435 | '"https://w3id.org/security/suites/ed25519-2020/v1".'); 436 | }); 437 | }); 438 | }); 439 | -------------------------------------------------------------------------------- /test/documentLoader.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import { 5 | controllerDoc2020, 6 | mockKeyPair2020, 7 | mockPublicKey2020, 8 | } from './mock-data.js'; 9 | import ed25519Context2018 from 'ed25519-signature-2018-context'; 10 | import {securityLoader} from '@digitalbazaar/security-document-loader'; 11 | 12 | export const loader = securityLoader(); 13 | 14 | loader.addStatic(mockKeyPair2020.controller, controllerDoc2020); 15 | loader.addStatic(mockPublicKey2020.id, mockPublicKey2020); 16 | loader.addStatic(ed25519Context2018.constants.CONTEXT_URL, 17 | ed25519Context2018.contexts.get(ed25519Context2018.constants.CONTEXT_URL)); 18 | 19 | loader.addStatic( 20 | 'https://www.w3.org/ns/credentials/examples/v2', 21 | { 22 | '@context': { 23 | '@vocab': 'https://www.w3.org/ns/credentials/examples#' 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /test/mock-data.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | export const controller = 'https://example.edu/issuers/565049'; 5 | 6 | export const mockPublicKey2020 = { 7 | '@context': 'https://w3id.org/security/suites/ed25519-2020/v1', 8 | type: 'Ed25519VerificationKey2020', 9 | controller, 10 | id: controller + '#z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T', 11 | publicKeyMultibase: 'z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T' 12 | }; 13 | 14 | export const mockPublicKey2018 = { 15 | '@context': 'https://w3id.org/security/suites/ed25519-2018/v1', 16 | type: 'Ed25519VerificationKey2018', 17 | controller, 18 | id: controller + '#z6MkumafR1duPR5FZgbVu8nzX3VyhULoXNpq9rpjhfaiMQmx', 19 | publicKeyBase58: 'DggG1kT5JEFwTC6RJTsT6VQPgCz1qszCkX5Lv4nun98x' 20 | }; 21 | 22 | export const mockKeyPair2020 = { 23 | type: 'Ed25519VerificationKey2020', 24 | controller, 25 | id: controller + '#z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T', 26 | publicKeyMultibase: 'z6MknCCLeeHBUaHu4aHSVLDCYQW9gjVJ7a63FpMvtuVMy53T', 27 | privateKeyMultibase: 'zrv2EET2WWZ8T1Jbg4fEH5cQxhbUS22XxdweypUbjWVzv1YD6VqYu' + 28 | 'W6LH7heQCNYQCuoKaDwvv2qCWz3uBzG2xesqmf' 29 | }; 30 | 31 | export const mockKeyPair2018 = { 32 | type: 'Ed25519VerificationKey2018', 33 | controller, 34 | id: controller + '#z6MkumafR1duPR5FZgbVu8nzX3VyhULoXNpq9rpjhfaiMQmx', 35 | publicKeyBase58: 'DggG1kT5JEFwTC6RJTsT6VQPgCz1qszCkX5Lv4nun98x', 36 | privateKeyBase58: 'sSicNq6YBSzafzYDAcuduRmdHtnrZRJ7CbvjzdQhC45e' + 37 | 'wwvQeuqbM2dNwS9RCf6buUJGu6N3rBy6oLSpMwha8tc' 38 | }; 39 | 40 | export const controllerDoc2020 = { 41 | '@context': [ 42 | 'https://www.w3.org/ns/did/v1', 43 | 'https://w3id.org/security/suites/ed25519-2020/v1' 44 | ], 45 | id: 'https://example.edu/issuers/565049', 46 | assertionMethod: [mockPublicKey2020] 47 | }; 48 | 49 | export const controllerDoc2018 = { 50 | '@context': [ 51 | 'https://www.w3.org/ns/did/v1', 52 | 'https://w3id.org/security/suites/ed25519-2018/v1' 53 | ], 54 | id: 'https://example.edu/issuers/565049', 55 | assertionMethod: [mockPublicKey2018] 56 | }; 57 | 58 | export const credential = { 59 | '@context': [ 60 | 'https://www.w3.org/2018/credentials/v1', 61 | { 62 | AlumniCredential: 'https://schema.org#AlumniCredential', 63 | alumniOf: 'https://schema.org#alumniOf' 64 | }, 65 | 'https://w3id.org/security/suites/ed25519-2020/v1' 66 | ], 67 | id: 'http://example.edu/credentials/1872', 68 | type: ['VerifiableCredential', 'AlumniCredential'], 69 | issuer: 'https://example.edu/issuers/565049', 70 | issuanceDate: '2010-01-01T19:23:24Z', 71 | credentialSubject: { 72 | id: 'https://example.edu/students/alice', 73 | alumniOf: 'Example University' 74 | } 75 | }; 76 | -------------------------------------------------------------------------------- /test/test-mocha.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2021 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import chai from 'chai'; 5 | global.should = chai.should(); 6 | -------------------------------------------------------------------------------- /test/test-vectors.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2023-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | /* Note: This file contains data generated from the vc-di-eddsa specification 5 | test vectors. */ 6 | 7 | /* eslint-disable max-len */ 8 | /* eslint-disable quote-props */ 9 | /* eslint-disable quotes */ 10 | export const keyMaterial = { 11 | "publicKeyMultibase": "z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2", 12 | "secretKeyMultibase": "z3u2en7t5LR2WtQH5PfFqMqwVHBeXouLzo6haApm8XHqvjxq" 13 | }; 14 | 15 | // public key above converted to multikey format + controller doc: 16 | export const publicKey = { 17 | '@context': 'https://w3id.org/security/multikey/v1', 18 | id: 'did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2#z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2', 19 | type: 'Multikey', 20 | controller: 'did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2', 21 | publicKeyMultibase: 'z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2' 22 | }; 23 | export const controllerDoc = { 24 | '@context': [ 25 | 'https://www.w3.org/ns/did/v1', 26 | 'https://w3id.org/security/multikey/v1' 27 | ], 28 | id: publicKey.controller, 29 | assertionMethod: [publicKey] 30 | }; 31 | 32 | export const signedFixture = { 33 | "@context": [ 34 | "https://www.w3.org/ns/credentials/v2", 35 | "https://www.w3.org/ns/credentials/examples/v2", 36 | "https://w3id.org/security/suites/ed25519-2020/v1" 37 | ], 38 | "id": "urn:uuid:58172aac-d8ba-11ed-83dd-0b3aef56cc33", 39 | "type": [ 40 | "VerifiableCredential", 41 | "AlumniCredential" 42 | ], 43 | "name": "Alumni Credential", 44 | "description": "A minimum viable example of an Alumni Credential.", 45 | "issuer": "https://vc.example/issuers/5678", 46 | "validFrom": "2023-01-01T00:00:00Z", 47 | "credentialSubject": { 48 | "id": "did:example:abcdefgh", 49 | "alumniOf": "The School of Examples" 50 | }, 51 | "proof": { 52 | "type": "Ed25519Signature2020", 53 | "created": "2023-02-24T23:36:38Z", 54 | "verificationMethod": "did:key:z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2#z6MkrJVnaZkeFzdQyMZu1cgjg7k1pZZ6pvBQ7XJPt4swbTQ2", 55 | "proofPurpose": "assertionMethod", 56 | "proofValue": "z57Mm1vboMtZiCyJ4aReZsv8co4Re64Y8GEjL1ZARzMbXZgkARFLqFs1P345NpPGG2hgCrS4nNdvJhpwnrNyG3kEF" 57 | } 58 | }; 59 | /* eslint-enable quotes */ 60 | /* eslint-enable quote-props */ 61 | /* eslint-enable max-len */ 62 | -------------------------------------------------------------------------------- /test/test-vectors.spec.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright (c) 2023-2024 Digital Bazaar, Inc. All rights reserved. 3 | */ 4 | import * as Ed25519Multikey from '@digitalbazaar/ed25519-multikey'; 5 | import {Ed25519Signature2020} from '../lib/index.js'; 6 | import {expect} from 'chai'; 7 | import jsigs from 'jsonld-signatures'; 8 | import {loader} from './documentLoader.js'; 9 | 10 | import * as testVectors from './test-vectors.js'; 11 | 12 | const {purposes: {AssertionProofPurpose}} = jsigs; 13 | 14 | const documentLoader = loader.build(); 15 | 16 | describe('test vectors', () => { 17 | let keyPair; 18 | before(async () => { 19 | const {keyMaterial} = testVectors; 20 | keyPair = await Ed25519Multikey.from(keyMaterial); 21 | keyPair.controller = `did:key:${keyPair.publicKeyMultibase}`; 22 | keyPair.id = `${keyPair.controller}#${keyPair.publicKeyMultibase}`; 23 | }); 24 | 25 | it('should create proof', async () => { 26 | const {signedFixture} = testVectors; 27 | const unsigned = {...signedFixture}; 28 | delete unsigned.proof; 29 | 30 | const signer = keyPair.signer(); 31 | const date = new Date(signedFixture.proof.created); 32 | 33 | let error; 34 | let signed; 35 | try { 36 | signed = await jsigs.sign(unsigned, { 37 | suite: new Ed25519Signature2020({signer, date}), 38 | purpose: new AssertionProofPurpose(), 39 | documentLoader 40 | }); 41 | } catch(e) { 42 | error = e; 43 | } 44 | 45 | expect(error).to.not.exist; 46 | expect(signed).to.deep.equal(signedFixture); 47 | }); 48 | 49 | it('should verify signed fixture', async () => { 50 | const {signedFixture} = testVectors; 51 | 52 | const result = await jsigs.verify(signedFixture, { 53 | suite: new Ed25519Signature2020(), 54 | purpose: new AssertionProofPurpose(), 55 | documentLoader 56 | }); 57 | 58 | expect(result.verified).to.be.true; 59 | }); 60 | }); 61 | --------------------------------------------------------------------------------