From 9fe5933d15d38520f1d560548e49ddd9d96a2eba Mon Sep 17 00:00:00 2001 From: Kristine Robison Date: Tue, 9 Oct 2018 17:53:50 -0700 Subject: [PATCH] feat: sign verifiable claims with jsonld-signatures feat: sign verifiable claims with jsonld-signatures use jsonld-signatures to create and verify Linked Data Signature Proofs for Verifiable Claims BREAKING CHANGE: the mechanism to create a verifiable claim and sign it has changed dramatically. Refer to the README for details. Use the method below to create a claim. ``` import { configureCreateVerifiableClaim, createIssuerFromPrivateKey, getVerifiableClaimSigner } from '@po.et/poet-js' const { configureSignVerifiableClaim } = getVerifiableClaimSigner() const issuerPrivateKey = '' const issuer = createIssuerFromPrivateKey(issuerPrivateKey) const createVerifiableWorkClaim = configureCreateVerifiableClaim({ issuer }) const signVerifiableClaim = configureSignVerifiableClaim({ privateKey: issuerPrivateKey }) const workClaim = { name: 'The Raven', author: 'Edgar Allan Poe', tags: 'poem', dateCreated: '', datePublished: '1845-01-29T03:00:00.000Z', archiveUrl: 'https://example.com/raven', hash: '', } const unsignedVerifiableClaim = await createVerifiableWorkClaim(workClaim) const signedWorkClaim = await signVerifiableClaim(unsignedVerifiableClaim) ``` --- README.md | 175 ++++++++-- package-lock.json | 521 +++++++++++++++++------------- package.json | 11 +- src/Claim.test.ts | 390 ---------------------- src/Claim.ts | 76 ----- src/Interfaces.test.ts | 131 +++++--- src/Interfaces.ts | 221 ++++++++++--- src/VerifiableClaim.test.ts | 257 +++++++++++++++ src/VerifiableClaim.ts | 53 +++ src/VerifiableClaimSigner.test.ts | 418 ++++++++++++++++++++++++ src/VerifiableClaimSigner.ts | 82 +++++ src/index.ts | 3 +- src/util/DataDocumentLoader.ts | 34 ++ src/util/KeyHelper.test.ts | 123 +++++++ src/util/KeyHelper.ts | 156 +++++++++ tests/unit/index.ts | 4 +- tests/unit/shared.ts | 218 +++++++++++++ typings/json-signatures.d.ts | 19 ++ typings/jsonld.d.ts | 12 +- typings/node-forge.d.ts | 51 +++ typings/parse-data-url.d.ts | 1 + typings/riteway.d.ts | 1 - 22 files changed, 2143 insertions(+), 814 deletions(-) delete mode 100644 src/Claim.test.ts delete mode 100644 src/Claim.ts create mode 100644 src/VerifiableClaim.test.ts create mode 100644 src/VerifiableClaim.ts create mode 100644 src/VerifiableClaimSigner.test.ts create mode 100644 src/VerifiableClaimSigner.ts create mode 100644 src/util/DataDocumentLoader.ts create mode 100644 src/util/KeyHelper.test.ts create mode 100644 src/util/KeyHelper.ts create mode 100644 tests/unit/shared.ts create mode 100644 typings/json-signatures.d.ts create mode 100644 typings/node-forge.d.ts create mode 100644 typings/parse-data-url.d.ts delete mode 100644 typings/riteway.d.ts diff --git a/README.md b/README.md index 5b3c001..555c231 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,51 @@ [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) [![Join the chat at https://gitter.im/poetapp/Lobby](https://badges.gitter.im/poetapp/Lobby.svg)](https://gitter.im/poetapp/Lobby) -Po.et JS is a small library that provides methods to easily create and sign Po.et Claims. +Po.et JS is a small library that provides methods to easily create and sign Po.et Claims according to the +[Verifiable Credentials Data Model](https://w3c.github.io/vc-data-model). These claims are [JSON-LD](https://w3c.github.io/json-ld-syntax/) +documents. As such, you can define your own JSON-LD `@context` to map your submitted Claims. + +Po.et does provide a few default `@context` objects that you can extend or override in the `createClaim` function. The current defaults are as follows: + +```typescript +export const DefaultClaimContext: ClaimContext = { + cred: 'https://w3id.org/credentials#', + dc: 'http://purl.org/dc/terms/', + schema: 'http://schema.org/', + sec: 'https://w3id.org/security#', + + id: 'sec:digestValue', + issuer: 'cred:issuer', + issuanceDate: 'cred:issued', + type: 'schema:additionalType', + claim: 'schema:Thing', // The most generic definition in schema.org, +} + +export const DefaultWorkClaimContext: ClaimContext = { + archiveUrl: 'schema:url', + author: 'schema:author', + canonicalUrl: 'schema:url', + claim: 'schema:CreativeWork', + contributors: { + '@id': 'schema:ItemList', + '@container': '@list', + '@type': 'schema:contributor', + }, + copyrightHolder: 'schema:copyrightHolder', + dateCreated: 'schema:dateCreated', + datePublished: 'schema:datePublished', + license: 'schema:license', + name: 'schema:name', + tags: 'schema:keywords', + hash: 'sec:digestValue', +} + +export const DefaultIdentityClaimContext: ClaimContext = { + publicKey: 'sec:publicKeyBase58', + profileUrl: 'sec:owner', +} + +``` ## Installation @@ -15,29 +59,54 @@ npm i @po.et/poet-js ## Usage -The main function you'll be using is `createClaim`: +Note that the Po.et network currently uses +[Ed25519Signature2018](https://w3c-dvcg.github.io/lds-ed25519-2018/), which requires a Base58 +form of the Ed25519 Private Key. You can use the KeyHelper utility to generate a base58 public/privateKey pair, if you +do not yet have one. -### Example 1: createClaim for Work Claims +**WARNING** +Do not use the example private key in these documents. No one should have access to your private key, and it certainly should not be in the example documents of a library. If you +use the example private key, others can make additional claims using the same key. -```ts -import { Claim, ClaimType, createClaim } from '@po.et/poet-js' +```typescript +import { createIssuerFromPrivateKey, generateED25519Base58Keys } from '@po.et/poet-js' + +const { privateKey } = generateED25519Base58Keys('entropy_phrase') // e.g 'LWgo1jraJrCB2QT64UVgRemepsNopBF3eJaYMPYVTxpEoFx7sSzCb1QysHeJkH2fnGFgHirgVR35Hz5A1PpXuH6' + +const issuer = createIssuerFromPrivateKey(privateKey) -const workAttributes = { +``` + +---- + +Use `configureCreateVerifiableClaim()` to create a `createVerifiableClaim` function to create an unsigned Verifiable Claim. +Then use `configureSignVerifiableClaim` from `getVerifiableClaimSigner()` to create the proper function to sign and verify your claims. + +### Example 1: Create and Sign a Verifiable Work Claims + +```typescript +import { configureCreateVerifiableClaim, createIssuerFromPrivateKey, getVerifiableClaimSigner } from '@po.et/poet-js' + +const { configureSignVerifiableClaim } = getVerifiableClaimSigner() + +const issuerPrivateKey = 'LWgo1jraJrCB2QT64UVgRemepsNopBF3eJaYMPYVTxpEoFx7sSzCb1QysHeJkH2fnGFgHirgVR35Hz5A1PpXuH6' +const issuer = createIssuerFromPrivateKey(issuerPrivateKey) + +const createVerifiableWorkClaim = configureCreateVerifiableClaim({ issuer }) +const signVerifiableClaim = configureSignVerifiableClaim({ privateKey: issuerPrivateKey }) + +const workClaim = { name: 'The Raven', author: 'Edgar Allan Poe', tags: 'poem', dateCreated: '', datePublished: '1845-01-29T03:00:00.000Z', - content: 'Once upon a midnight dreary...' + archiveUrl: 'https://example.com/raven', + hash: '', } -const Issuer = 'po.et://entities/' - -const claim = createClaim( - Issuer, - ClaimType.Work, - workAttributes -) +const unsignedVerifiableClaim = await createVerifiableWorkClaim(workClaim) +const signedWorkClaim = await signVerifiableClaim(unsignedVerifiableClaim) ``` Once this claim is created, you can publish it to a Po.et Node: @@ -49,28 +118,77 @@ const response = await fetch(poetNodeUrl + '/works/', { 'Accept': 'application/json', 'Content-Type': 'application/json' }, - body: JSON.stringify(claim) + body: JSON.stringify(signedWorkClaim) }) ``` -Note, if you are creating an identity claim, your Identity Provider (IDP) will be the issuer of the claim. If you are self-serving your own IDP, you will have to create an IdentityClaim for the IDP from which you can issue all further identities. +### Example 2: Create and sign a Verifiable Work Claim with overriding context -### Example 2: createClaim for Identity Claims +If you want to extend or override the default context defined by Po.et, you simply need to pass a context object into +the `configureCreateVerifiableClaim` function: -```ts -import { Claim, ClaimType, createClaim } from '@po.et/poet-js' +```typescript +import { ClaimType, configureCreateVerifiableClaim, createIssuerFromPrivateKey, getVerifiableClaimSigner } from '@po.et/poet-js' -const identityAttributes = { - publicKey: '' +const { configureSignVerifiableClaim } = getVerifiableClaimSigner() + +const issuerPrivateKey = 'LWgo1jraJrCB2QT64UVgRemepsNopBF3eJaYMPYVTxpEoFx7sSzCb1QysHeJkH2fnGFgHirgVR35Hz5A1PpXuH6' +const issuer = createIssuerFromPrivateKey(issuerPrivateKey) + +const externalContext: any = { + claim: 'schema:Book', + edition: 'schema:bookEdition', + isbn: 'schema.org/isbn', +} + +const createVerifiableWorkClaim = configureCreateVerifiableClaim({ issuer, type: ClaimType.Work, context: externalContext }) +const signVerifiableClaim = configureSignVerifiableClaim({ privateKey: issuerPrivateKey }) + + +const workClaim = { + name: 'The Raven', + author: 'Edgar Allan Poe', + tags: 'poem', + dateCreated: '', + datePublished: '1845-01-29T03:00:00.000Z', + archiveUrl: 'https://example.com/raven', + hash: '', + isbn: '9781458318404', + edition: '1', } -const Issuer = 'po.et://entities/' +const unsignedVerifiableClaim = await createVerifiableWorkClaim(workClaim) +const signedWorkClaim = await signVerifiableClaim(unsignedVerifiableClaim) +``` + +### Example 3: createClaim for Identity Claims +Note, if you are creating an identity claim, your IDP will be the issuer of the claim. [Frost](https://frost.po.et/) is one such IDP. +If you are self-serving your own identity claim, your identity provider (IDP) will have to create an IdentityClaim for +itself from which you can issue all further identities. Currently the Po.et network uses the [Ed25519Signature2018](https://w3c-dvcg.github.io/lds-ed25519-2018/), +which requires a Base58 form of the Ed25519 Public Key. + + +```typescript +import { ClaimType, configureCreateVerifiableClaim, createIssuerFromPrivateKey, getVerifiableClaimSigner, KeyHelper } from '@po.et/poet-js' + +const { configureSignVerifiableClaim } = getVerifiableClaimSigner() + +const issuerPrivateKey = 'LWgo1jraJrCB2QT64UVgRemepsNopBF3eJaYMPYVTxpEoFx7sSzCb1QysHeJkH2fnGFgHirgVR35Hz5A1PpXuH6' +// Issuer is the IDP +const issuer = createIssuerFromPrivateKey(issuerPrivateKey) + +const createVerifiableIdentityClaim = configureCreateVerifiableClaim({ issuer, type: ClaimType.Identity }) +const signVerifiableClaim = configureSignVerifiableClaim({ privateKey: issuerPrivateKey }) + +// Store the privateKey for this profile for signing future claims +const { publicKey, privateKey } = KeyHelper.generateED25519Base58Keys('entropy_phrase') + +const identityClaim = { + publicKey, +} -const claim = createClaim( - Issuer, - ClaimType.Identity, - identityAttributes -) +const unsignedVerifiableClaim = await createVerifiableIdentityClaim(identityClaim) +const signedWorkClaim = await signVerifiableClaim(unsignedVerifiableClaim) ``` Once this claim is created, you can publish it to a Po.et Node: @@ -86,7 +204,8 @@ const response = await fetch(poetNodeUrl + '/identities/', { }) ``` -Notice you don't need to wait for the server's response to know the claim's ID. You don't even need to publish it! `claim.id` is readily available right after calling `createClaim`. +Notice you don't need to wait for the server's response to know the claim's ID. You don't even need to publish it! +`claim.id` is readily available right after creating the unsigned verifiable claim. ## Contributing diff --git a/package-lock.json b/package-lock.json index 1aa41a4..61cc02b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3149,60 +3149,6 @@ "tslint-plugin-prettier": "2.0.0" } }, - "@protobufjs/aspromise": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", - "integrity": "sha1-m4sMxmPWaafY9vXQiToU00jzD78=" - }, - "@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" - }, - "@protobufjs/codegen": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", - "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" - }, - "@protobufjs/eventemitter": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", - "integrity": "sha1-NVy8mLr61ZePntCV85diHx0Ga3A=" - }, - "@protobufjs/fetch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", - "integrity": "sha1-upn7WYYUr2VwDBYZ/wbUVLDYTEU=", - "requires": { - "@protobufjs/aspromise": "^1.1.1", - "@protobufjs/inquire": "^1.1.0" - } - }, - "@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha1-Xp4avctz/Ap8uLKR33jIy9l7h9E=" - }, - "@protobufjs/inquire": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", - "integrity": "sha1-/yAOPnzyQp4tyvwRQIKOjMY48Ik=" - }, - "@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha1-bMKyDFya1q0NzP0hynZz2Nf79o0=" - }, - "@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha1-Cf0V8tbTq/qbZbw2ZQbWrXhG/1Q=" - }, - "@protobufjs/utf8": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", - "integrity": "sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA=" - }, "@samverschueren/stream-to-observable": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@samverschueren/stream-to-observable/-/stream-to-observable-0.3.0.tgz", @@ -3466,18 +3412,28 @@ "defer-to-connect": "^1.0.1" } }, - "@types/bitcore-lib": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/@types/bitcore-lib/-/bitcore-lib-0.15.1.tgz", - "integrity": "sha512-rxBI1QO9AjbZsj8nXZvQGzRm33XBrLCdAf9ok35a4ihH5wUNnM5hmY1eaortWVhouetk1aGGnSW7Uos3VBXlTw==", + "@types/base-x": { + "version": "1.0.29", + "resolved": "https://registry.npmjs.org/@types/base-x/-/base-x-1.0.29.tgz", + "integrity": "sha1-ii1z5KXDEhdXpfiHDPpFCWVRhrY=" + }, + "@types/bs58": { + "version": "3.0.30", + "resolved": "https://registry.npmjs.org/@types/bs58/-/bs58-3.0.30.tgz", + "integrity": "sha1-kWuhCZJGXlb4GvxzXnk1/XSYk+E=", "requires": { - "@types/node": "*" + "@types/base-x": "*" } }, - "@types/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.0.tgz", - "integrity": "sha512-1w52Nyx4Gq47uuu0EVcsHBxZFJgurQ+rTKS3qMHxR1GY2T8c2AJYd6vZoZ9q1rupaDjU0yT+Jc2XTyXkjeMA+Q==" + "@types/cuid": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@types/cuid/-/cuid-1.3.0.tgz", + "integrity": "sha1-ILDgDKVV31ZIZrxS0uJRv5DIjbc=" + }, + "@types/joi": { + "version": "13.4.4", + "resolved": "https://registry.npmjs.org/@types/joi/-/joi-13.4.4.tgz", + "integrity": "sha512-DLiyc2EyiMzSyw71krLn28Iy8WFmOvGHiDTwAQT8cbPv8upLAiz4GlKt5jMP5vQTkTVQMHNszYKL16se8jpWIw==" }, "@types/node": { "version": "8.5.7", @@ -3537,7 +3493,6 @@ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", "dev": true, - "optional": true, "requires": { "kind-of": "^3.0.2", "longest": "^1.0.1", @@ -4036,24 +3991,81 @@ "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.3.0.tgz", "integrity": "sha512-DpLh5EzMR2kzvX1KIlVC0VkC3iZtHKTgdtZ0a3pglBZdaQFjt5S9g9xd1lE+YvXyfd6mtCeRnrUfOLYiTMlNSw==" }, - "bitcore-lib": { - "version": "0.15.0", - "resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-0.15.0.tgz", - "integrity": "sha512-AeXLWhiivF6CDFzrABZHT4jJrflyylDWTi32o30rF92HW9msfuKpjzrHtFKYGa9w0kNVv5HABQjCB3OEav4PhQ==", + "bitcore-message": { + "version": "github:CoMakery/bitcore-message#8799cc327029c3d34fc725f05b2cf981363f6ebf", + "from": "github:CoMakery/bitcore-message#dist", "requires": { - "bn.js": "=4.11.8", - "bs58": "=4.0.1", - "buffer-compare": "=1.1.1", - "elliptic": "=6.4.0", - "inherits": "=2.0.1", - "lodash": "=4.17.4" + "bitcore-lib": "^0.13.7" + }, + "dependencies": { + "bitcore-lib": { + "version": "0.13.19", + "resolved": "https://registry.npmjs.org/bitcore-lib/-/bitcore-lib-0.13.19.tgz", + "integrity": "sha1-SK8em9oQBnwasWJjRyta3SAA89w=", + "requires": { + "bn.js": "=2.0.4", + "bs58": "=2.0.0", + "buffer-compare": "=1.0.0", + "elliptic": "=3.0.3", + "inherits": "=2.0.1", + "lodash": "=3.10.1" + }, + "dependencies": { + "bn.js": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-2.0.4.tgz", + "integrity": "sha1-Igp81nf38b+pNif/QZN3b+eBlIA=" + }, + "bs58": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-2.0.0.tgz", + "integrity": "sha1-crcTvtIjoKxRi72g484/SBfznrU=" + }, + "buffer-compare": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-compare/-/buffer-compare-1.0.0.tgz", + "integrity": "sha1-rKp6lm6Y7un64Usxw5pfFY+zxKI=" + }, + "elliptic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-3.0.3.tgz", + "integrity": "sha1-hlybQgv75VAGuflp+XoNLESWZZU=", + "requires": { + "bn.js": "^2.0.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "inherits": "^2.0.1" + }, + "dependencies": { + "brorand": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.0.5.tgz", + "integrity": "sha1-B7VMowKGq9Fxig4qgwgD79yb+gQ=" + }, + "hash.js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.0.3.tgz", + "integrity": "sha1-EzL/ABVsCg/92CNgE9B7d6BFFXM=", + "requires": { + "inherits": "^2.0.1" + } + } + } + }, + "inherits": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", + "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=" + }, + "lodash": { + "version": "3.10.1", + "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz", + "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y=" + } + } + } } }, - "bn.js": { - "version": "4.11.8", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz", - "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA==" - }, "bottleneck": { "version": "2.11.2", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.11.2.tgz", @@ -4126,11 +4138,6 @@ "repeat-element": "^1.1.2" } }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=" - }, "bs58": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/bs58/-/bs58-4.0.1.tgz", @@ -4145,11 +4152,6 @@ "integrity": "sha1-M3dm2hWAEhD92VbCLpxokaudAzc=", "dev": true }, - "buffer-compare": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-compare/-/buffer-compare-1.1.1.tgz", - "integrity": "sha1-W+e+hTr4kZjR9N3AkNHWakiu9ZY=" - }, "buffer-from": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.0.tgz", @@ -4308,6 +4310,33 @@ "supports-color": "^2.0.0" } }, + "chloride": { + "version": "2.2.10", + "resolved": "https://registry.npmjs.org/chloride/-/chloride-2.2.10.tgz", + "integrity": "sha512-CbU1ISGiB2JBV6PDXx7hkl8D94d2TPD1BANUMFbr8rZYKJi8De2d3Hu2XDIOLAhXf+8yhoFOdjtLG6fxz3QByQ==", + "requires": { + "is-electron": "^2.0.0", + "sodium-browserify": "^1.2.4", + "sodium-browserify-tweetnacl": "^0.2.2", + "sodium-chloride": "^1.1.0", + "sodium-native": "^2.1.6" + } + }, + "chloride-test": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/chloride-test/-/chloride-test-1.2.2.tgz", + "integrity": "sha1-F4aGqF6SeARREulujHkXk/mhCuo=", + "requires": { + "json-buffer": "^2.0.11" + }, + "dependencies": { + "json-buffer": { + "version": "2.0.11", + "resolved": "http://registry.npmjs.org/json-buffer/-/json-buffer-2.0.11.tgz", + "integrity": "sha1-PkQf2jCYvo0eMXGtWRvGKjPi1V8=" + } + } + }, "chokidar": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-1.7.0.tgz", @@ -4677,6 +4706,11 @@ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4=", "dev": true }, + "cuid": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cuid/-/cuid-2.1.4.tgz", + "integrity": "sha512-l1vrCiWOwBpu85bfm939Hg3MQh2alxAMoGZnjcJOYkgN7wzy9yi3vQhRzwcTr9mdIlfTp8DP9G5ZgIIPyaQEvg==" + }, "currently-unhandled": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", @@ -4953,6 +4987,14 @@ "safer-buffer": "^2.1.0" } }, + "ed2curve": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/ed2curve/-/ed2curve-0.1.4.tgz", + "integrity": "sha1-lKRCSLuH2jXbDv968KpXYWgRf1k=", + "requires": { + "tweetnacl": "0.x.x" + } + }, "electron-to-chromium": { "version": "1.3.70", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.70.tgz", @@ -4965,20 +5007,6 @@ "integrity": "sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4=", "dev": true }, - "elliptic": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.0.tgz", - "integrity": "sha1-ysmvh2LIWDYYcAPI3+GT5eLq5d8=", - "requires": { - "bn.js": "^4.4.0", - "brorand": "^1.0.1", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.0" - } - }, "encoding": { "version": "0.1.12", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz", @@ -5738,8 +5766,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "optional": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" }, "aproba": { "version": "1.2.0", @@ -5760,14 +5787,12 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "optional": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5782,20 +5807,17 @@ "code-point-at": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "optional": true + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=" }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "optional": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", - "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "optional": true + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=" }, "core-util-is": { "version": "1.0.2", @@ -5912,8 +5934,7 @@ "inherits": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "optional": true + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { "version": "1.3.5", @@ -5925,7 +5946,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5940,7 +5960,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5948,14 +5967,12 @@ "minimist": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "optional": true + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" }, "minipass": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -5974,7 +5991,6 @@ "version": "0.5.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6054,8 +6070,7 @@ "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "optional": true + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=" }, "object-assign": { "version": "4.1.1", @@ -6067,7 +6082,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "optional": true, "requires": { "wrappy": "1" } @@ -6153,8 +6167,7 @@ "safe-buffer": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", - "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "optional": true + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==" }, "safer-buffer": { "version": "2.1.2", @@ -6190,7 +6203,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6210,7 +6222,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6254,14 +6265,12 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "optional": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "yallist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", - "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "optional": true + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=" } } }, @@ -6380,7 +6389,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-2.0.0.tgz", "integrity": "sha1-gTg9ctsFT8zPUzbaqQLxgvbtuyg=", - "optional": true, "requires": { "is-glob": "^2.0.0" } @@ -6570,31 +6578,10 @@ } } }, - "hash.js": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.3.tgz", - "integrity": "sha512-/UETyP0W22QILqS+6HowevwhEFJ3MBJnwTf75Qob9Wz9t0DPuisL8kW8YZMK62dHAKE1c1p+gY1TtOLY+USEHA==", - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.0" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } + "hoek": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-5.0.4.tgz", + "integrity": "sha512-Alr4ZQgoMlnere5FZJsIyfIjORBqZll5POhDsF4q64dPuJR6rNxXdDxtHSQq8OXRurhmx+PWYEE8bXRROY8h0w==" }, "home-or-tmp": { "version": "2.0.0", @@ -6868,8 +6855,7 @@ "ini": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", - "dev": true + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==" }, "into-stream": { "version": "3.1.0", @@ -6994,6 +6980,11 @@ "integrity": "sha1-pqLzL/0t+wT1yiXs0Pa4PPeYoeE=", "optional": true }, + "is-electron": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.1.0.tgz", + "integrity": "sha512-dkg5xT383+M6zIbbXW/z7n2nz4SFUi2OSyhntnFYkRdtV+HVEfdjEK+5AWisfYgkpe3WYjTIuh7toaKmSfFVWw==" + }, "is-equal-shallow": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz", @@ -7011,8 +7002,7 @@ "is-extglob": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-1.0.0.tgz", - "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=", - "optional": true + "integrity": "sha1-rEaBd8SUNAWgkvyPKXYMb/xiBsA=" }, "is-finite": { "version": "1.0.2", @@ -7032,7 +7022,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-2.0.1.tgz", "integrity": "sha1-0Jb5JqPe1WAPP9/ZEZjLCIjC2GM=", - "optional": true, "requires": { "is-extglob": "^1.0.0" } @@ -7225,6 +7214,21 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, + "isemail": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-3.1.3.tgz", + "integrity": "sha512-5xbsG5wYADIcB+mfLsd+nst1V/D+I7EU7LEZPo2GOIMu4JzfcRs5yQoypP4avA7QtUqgxYLKBYNv4IdzBmbhdw==", + "requires": { + "punycode": "2.x.x" + }, + "dependencies": { + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" + } + } + }, "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -7409,6 +7413,16 @@ } } }, + "joi": { + "version": "13.6.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-13.6.0.tgz", + "integrity": "sha512-E4QB0yRgEa6ZZKcSHJuBC+QeAwy+akCG0Bsa9edLqljyhlr+GuGDSmXYW1q7sj/FuAPy+ECUI3evVtK52tVfwg==", + "requires": { + "hoek": "5.x.x", + "isemail": "3.x.x", + "topo": "3.x.x" + } + }, "js-levenshtein": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.4.tgz", @@ -7493,6 +7507,19 @@ "xmldom": "0.1.19" } }, + "jsonld-signatures": { + "version": "2.3.0", + "resolved": "http://registry.npmjs.org/jsonld-signatures/-/jsonld-signatures-2.3.0.tgz", + "integrity": "sha512-0fMBhKMWKiJxCLfq0hO1RS8EeitYVxQICOdCRYTC3+qcWVWmhwVNcjPP1u54XafNGaiq+9agoFeQzjuDLgzOxg==", + "requires": { + "bitcore-message": "github:CoMakery/bitcore-message#8799cc327029c3d34fc725f05b2cf981363f6ebf", + "bs58": "^4.0.1", + "chloride": "^2.2.8", + "jsonld": "^1.0.1", + "node-forge": "^0.7.4", + "semver": "^5.5.0" + } + }, "jsonparse": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", @@ -7564,6 +7591,19 @@ "integrity": "sha1-wuep93IJTe6dNCAq6KzORoeHVYA=", "dev": true }, + "libsodium": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.7.3.tgz", + "integrity": "sha512-ld+deUNqSsZYbAobUs63UyduPq8ICp/Ul/5lbvBIYpuSNWpPRU0PIxbW+xXipVZtuopR6fIz9e0tTnNuPMNeqw==" + }, + "libsodium-wrappers": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.7.3.tgz", + "integrity": "sha512-dw5Jh6TZ5qc5rQVZe3JrSO/J05CE+DmAPnqD7Q2glBUE969xZ6o3fchnUxyPlp6ss3x0MFxmdJntveFN+XTg1g==", + "requires": { + "libsodium": "0.7.3" + } + }, "lint-staged": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-7.3.0.tgz", @@ -8169,17 +8209,11 @@ "integrity": "sha512-l9x0+1offnKKIzYVjyXU2SiwhXDLekRzKyhnbyldPHvC7BvLPVpdNUNR2KeMAiCN2D/kLNttZgQD5WjSxuBx3Q==", "dev": true }, - "long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, "longest": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", - "dev": true, - "optional": true + "dev": true }, "loose-envify": { "version": "1.3.1", @@ -8359,7 +8393,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -8450,16 +8484,6 @@ "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", "dev": true }, - "minimalistic-assert": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz", - "integrity": "sha1-cCvi3aazf0g2vLP121ZkG2Sh09M=" - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=" - }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -8616,6 +8640,12 @@ "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.6.tgz", "integrity": "sha512-sol30LUpz1jQFBjOKwbjxijiE3b6pjd74YwfD0fJOKPjF+fONKb2Yg8rYgS6+bK6VDl+/wfr4IYpC7jDzLUIfw==" }, + "node-gyp-build": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.4.0.tgz", + "integrity": "sha512-YoviGBJYGrPdLOKDIQB0sKxuKy/EEsxzooNkOZak4vSTKT/qH0Pa6dj3t1MJjEQGsefih61IyHDmO1WW7xOFfw==", + "optional": true + }, "node-modules-regexp": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", @@ -8647,7 +8677,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "optional": true, "requires": { "remove-trailing-separator": "^1.0.1" } @@ -12112,6 +12141,14 @@ "semver": "^5.1.0" } }, + "parse-data-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-data-url/-/parse-data-url-1.0.0.tgz", + "integrity": "sha512-ODy+F4itgWm2BEwgHSoOScBkCYrN8adHy0dUCM8IAP6qp+bHVKYfdH38tKDgaQPb1qFq8qVEH5l8EEiQbetnkQ==", + "requires": { + "valid-data-url": "^1.0.0" + } + }, "parse-github-url": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/parse-github-url/-/parse-github-url-1.0.2.tgz", @@ -12377,33 +12414,6 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=" }, - "protobufjs": { - "version": "6.8.8", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", - "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", - "requires": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.0", - "@types/node": "^10.1.0", - "long": "^4.0.0" - }, - "dependencies": { - "@types/node": { - "version": "10.9.4", - "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", - "integrity": "sha512-fCHV45gS+m3hH17zgkgADUSi2RR1Vht6wOZ0jyHP8rjiQra9f+mIcgwPQHllmDocYOstIEbKlxbFDYlgrTPYqw==" - } - } - }, "protocols": { "version": "1.4.6", "resolved": "https://registry.npmjs.org/protocols/-/protocols-1.4.6.tgz", @@ -12713,8 +12723,7 @@ "remove-trailing-separator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "optional": true + "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=" }, "repeat-element": { "version": "1.1.2", @@ -13201,6 +13210,14 @@ } } }, + "sha.js": { + "version": "2.4.5", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.5.tgz", + "integrity": "sha1-J9Fx78yCoRi5ljn/WBZgJCtQbnw=", + "requires": { + "inherits": "^2.0.1" + } + }, "shebang-command": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", @@ -13420,6 +13437,56 @@ "kind-of": "^3.2.0" } }, + "sodium-browserify": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sodium-browserify/-/sodium-browserify-1.2.4.tgz", + "integrity": "sha512-IYcxKje/uf/c3a7VhZYJLlUxWMcktfbD4AjqHjUD1/VWKjj0Oq5wNbX8wjJOWVO9UhUMqJQiOn2xFbzKWBmy5w==", + "requires": { + "libsodium-wrappers": "^0.7.3", + "sha.js": "2.4.5", + "sodium-browserify-tweetnacl": "^0.2.3", + "tweetnacl": "^0.14.1" + } + }, + "sodium-browserify-tweetnacl": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/sodium-browserify-tweetnacl/-/sodium-browserify-tweetnacl-0.2.3.tgz", + "integrity": "sha1-tVN//LufdOvEQ7i2ohGykej8vI4=", + "requires": { + "chloride-test": "^1.1.0", + "ed2curve": "^0.1.4", + "sha.js": "^2.4.8", + "tweetnacl": "^0.14.1", + "tweetnacl-auth": "^0.3.0" + }, + "dependencies": { + "sha.js": { + "version": "2.4.11", + "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "requires": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + } + } + }, + "sodium-chloride": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sodium-chloride/-/sodium-chloride-1.1.2.tgz", + "integrity": "sha512-8AVzr9VHueXqfzfkzUA0aXe/Q4XG3UTmhlP6Pt+HQc5bbAPIJFo7ZIMh9tvn+99QuiMcyDJdYumegGAczl0N+g==" + }, + "sodium-native": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/sodium-native/-/sodium-native-2.2.2.tgz", + "integrity": "sha512-TdAa+PlmWki2Spx3TJyGFVkj6gLsxs5HV/i/j2COZ5INEaCmLz15NIHgUAT/GQ0KrG8Ddy6tkvuP7B5YJpmW6g==", + "optional": true, + "requires": { + "ini": "^1.3.5", + "nan": "^2.4.0", + "node-gyp-build": "^3.0.0" + } + }, "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -13725,7 +13792,7 @@ }, "minimist": { "version": "1.2.0", - "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", "dev": true }, @@ -13763,7 +13830,7 @@ }, "text-encoding": { "version": "0.6.4", - "resolved": "http://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=", "dev": true }, @@ -13848,6 +13915,14 @@ } } }, + "topo": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-3.0.0.tgz", + "integrity": "sha512-Tlu1fGlR90iCdIPURqPiufqAlCZYzLjHYVVbcFWDMcX7+tK8hdZWAfsMrD/pBul9jqHHwFjNdf1WaxA9vTRRhw==", + "requires": { + "hoek": "5.x.x" + } + }, "tough-cookie": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz", @@ -14313,8 +14388,15 @@ "tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", - "optional": true + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=" + }, + "tweetnacl-auth": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/tweetnacl-auth/-/tweetnacl-auth-0.3.1.tgz", + "integrity": "sha1-t1vC3xVkm7hOi5qjwGacbEvODSU=", + "requires": { + "tweetnacl": "0.x.x" + } }, "type-detect": { "version": "4.0.8", @@ -14643,6 +14725,11 @@ "user-home": "^1.1.1" } }, + "valid-data-url": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/valid-data-url/-/valid-data-url-1.0.0.tgz", + "integrity": "sha512-Iby9QiZ+96MxRYGbgKQsK5GCJR8eySIqZv6h+kj/NJPJ9XFmtzeFiq/f8yHwQBlqOS6wqfaAKmKwjZrzyp+MPg==" + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -14794,7 +14881,7 @@ }, "yargs": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", + "resolved": "http://registry.npmjs.org/yargs/-/yargs-11.1.0.tgz", "integrity": "sha512-NwW69J42EsCSanF8kyn5upxvjp5ds+t3+udGBeTbFnERA+lF541DDpMawzo4z6W/QrzNM18D+BPMiOBibnFV5A==", "dev": true, "requires": { diff --git a/package.json b/package.json index 8589baf..66652bc 100644 --- a/package.json +++ b/package.json @@ -36,17 +36,22 @@ "url": "https://github.com/poetapp/poet-js/issues" }, "dependencies": { - "@types/bitcore-lib": "0.15.1", + "@types/bs58": "3.0.30", + "@types/cuid": "1.3.0", + "@types/joi": "13.4.4", "@types/node-fetch": "1.6.9", "babel-cli": "6.26.0", "babel-plugin-module-resolver": "3.1.1", - "bitcore-lib": "0.15.0", + "cuid": "2.1.4", + "joi": "13.6.0", "jsonld": "1.1.0", + "jsonld-signatures": "2.3.0", "node-fetch": "1.7.3", - "protobufjs": "6.8.8" + "parse-data-url": "1.0.0" }, "devDependencies": { "@po.et/tslint-rules": "1.4.6", + "@types/node-fetch": "1.6.9", "husky": "1.1.1", "lint-staged": "7.3.0", "nyc": "github:poetapp/nyc#fbc2ed1", diff --git a/src/Claim.test.ts b/src/Claim.test.ts deleted file mode 100644 index 53b26bc..0000000 --- a/src/Claim.test.ts +++ /dev/null @@ -1,390 +0,0 @@ -/* tslint:disable:no-relative-imports */ -import { describe } from 'riteway' -import { createClaim, isValidSignature, getClaimId, getClaimSignature, isValidClaim, canonizeClaim } from './Claim' -import { Claim, ClaimType, ClaimAttributes, isClaim, Work } from './Interfaces' - -const makeClaim = (attributes: ClaimAttributes) => { - const created = '2017-12-11T22:54:40.261Z' - const publicKey = '02db393ae2d566ceddd95a97fd88bc2897a0818528158261cec45087a58786f09d' - const type = ClaimType.Work - return { - publicKey, - created, - type, - attributes, - } -} - -const TheRaven: Work = { - id: '3181a2f4de0c25a4c6e6acdab993d08dd80ff83cdefe1ab39846c9c984e4fff1', - publicKey: '02badf4650ba545608242c2d303d587cf4f778ae3cf2b3ef99fbda37555a400fd2', - signature: - '3044022054a342c1629d3046992ba53077064201808c695399741ed002377526080f12990220242a22cb34da1362efd770eb6fb03baf29d1fe5d006c31b3e9a108f9f301c17e', - type: ClaimType.Work, - created: '2017-11-13T15:00:00.000Z', - attributes: { - name: 'The Raven', - author: 'Edgar Allan Poe', - tags: 'poem', - dateCreated: '', - datePublished: '1845-01-29T03:00:00.000Z', - text: 'Once upon a midnight dreary...', - }, -} - -const canonicalRaven = - '_:c14n0 "2017-11-13T15:00:00.000Z" .\n' + - '_:c14n0 _:c14n1 .\n' + - '_:c14n0 "02badf4650ba545608242c2d303d587cf4f778ae3cf2b3ef99fbda37555a400fd2" .\n' + - '_:c14n0 "Work" .\n' + - '_:c14n1 "Edgar Allan Poe" .\n' + - '_:c14n1 "" .\n' + - '_:c14n1 "1845-01-29T03:00:00.000Z" .\n' + - '_:c14n1 "poem" .\n' + - '_:c14n1 "The Raven" .\n' + - '_:c14n1 "Once upon a midnight dreary..." .\n' - -const expectedCanonicalDoc = - '_:c14n0 "2017-12-11T22:54:40.261Z" .\n' + - '_:c14n0 _:c14n1 .\n' + - '_:c14n0 "02db393ae2d566ceddd95a97fd88bc2897a0818528158261cec45087a58786f09d" .\n' + - '_:c14n0 "Work" .\n' + - '_:c14n1 "Edgar Allan Poe" .\n' + - '_:c14n1 "The Raven" .\n' - -const Key = { - privateKey: 'L1mptZyB6aWkiJU7dvAK4UUjLSaqzcRNYJn3KuAA7oEVyiNn3ZPF', - publicKey: '02cab54b227f16dd4866310799842cdd239f2adb56d0a3789519c6f43a892a61f6', -} - -const PrivateKeyEAP = 'KxuZJmgVAipi9hfYXHTyGYmmhkbG7fBzmkyVnj6t9j9rDR1nN1vN' - -describe('Claim', async (assert: any) => { - const returnError = (err: Error): Error => err - - { - assert({ - given: 'a complete claim', - should: 'include all items in canonical doc', - actual: await canonizeClaim(TheRaven), - expected: canonicalRaven, - }) - } - - { - const claim = makeClaim({ - name: TheRaven.attributes.name, - author: TheRaven.attributes.author, - }) - - assert({ - given: 'a claim', - should: 'create a canonical string document', - actual: await canonizeClaim(claim).catch(returnError), - expected: expectedCanonicalDoc, - }) - } - - { - const work1: Claim = makeClaim({ - name: TheRaven.attributes.name, - author: TheRaven.attributes.author, - }) - - const work2: Claim = makeClaim({ - author: TheRaven.attributes.author, - name: TheRaven.attributes.name, - }) - - const canonicalDocument1 = await canonizeClaim(work1).catch(returnError) - const canonicalDocument2 = await canonizeClaim(work2).catch(returnError) - - assert({ - given: 'two claims with disordered keys', - should: 'have the same canonical document', - actual: canonicalDocument1 === canonicalDocument2, - expected: true, - }) - } - - { - const work1: Claim = makeClaim({ - name: TheRaven.attributes.name, - author: TheRaven.attributes.author, - }) - - const work2: Claim = makeClaim({ - Author: TheRaven.attributes.author, - NAME: TheRaven.attributes.name, - }) - - const canonicalDocument1 = await canonizeClaim(work1).catch(returnError) - const canonicalDocument2 = await canonizeClaim(work2).catch(returnError) - - assert({ - given: 'two claims with keys casing', - should: 'NOT have the same canonical document', - actual: canonicalDocument1 !== canonicalDocument2, - expected: true, - }) - } - - { - const claim = makeClaim({ - name: TheRaven.attributes.name, - author: TheRaven.attributes.author, - }) - - assert({ - given: 'A claim', - should: 'generate an id for the claim', - actual: await getClaimId(claim), - expected: '817436801385c4227718448cc909a591c7193428a8dccfea5eee22534029ec34', - }) - } - - { - const claim = await createClaim(Key.privateKey, ClaimType.Work, TheRaven.attributes) - - assert({ - given: 'the public key of a Claim', - should: 'be equal to public key of key', - actual: claim.publicKey, - expected: Key.publicKey, - }) - } - - { - const claim = await createClaim(Key.privateKey, ClaimType.Work, TheRaven.attributes) - - assert({ - given: 'the result of isValidSignature of Claim with a valid signature', - should: 'should return true', - actual: isValidSignature(claim), - expected: true, - }) - } - - { - assert({ - given: 'a claim id', - should: 'be equal to the work id', - actual: await getClaimId(TheRaven).catch(returnError), - expected: TheRaven.id, - }) - } - - { - assert({ - given: 'a claim with extra id, the new id', - should: 'be ignored in the calculation of the id and should be equal to the work id', - actual: await getClaimId({ ...TheRaven, id: '123' }).catch(returnError), - expected: TheRaven.id, - }) - } - - { - assert({ - given: 'a claim with extra signature, the new signature', - should: 'be ignored in the calculation of the id and should be equal to the work id', - actual: await getClaimId({ ...TheRaven, signature: '123' }).catch(returnError), - expected: TheRaven.id, - }) - } - - { - const claimId = await getClaimId({ ...TheRaven, publicKey: '123' }).catch(returnError) - - assert({ - given: 'a claim with extra publicKey, the new publicKey', - should: 'be included in the calculation of the id and should be not equal to the work id', - actual: claimId !== TheRaven.id, - expected: true, - }) - } - - { - const claimId = await getClaimId({ ...TheRaven, type: 'Asd' as ClaimType }).catch(returnError) - - assert({ - given: 'a claim with extra type, the new type', - should: 'be included in the calculation of the id and should be not equal to the work id', - actual: claimId !== TheRaven.id, - expected: true, - }) - } - - { - const claimId = await getClaimId({ ...TheRaven, created: '2017-09-13T15:00:00.000Z' }).catch(returnError) - - assert({ - given: 'a claim with extra dateCreated, the new dateCreated', - should: 'be included in the calculation of the id and should be not equal to the work id', - actual: claimId !== TheRaven.id, - expected: true, - }) - } - - { - const work1: Claim = makeClaim({ - name: TheRaven.attributes.name, - author: TheRaven.attributes.author, - }) - - const work2: Claim = makeClaim({ - author: TheRaven.attributes.author, - name: TheRaven.attributes.name, - }) - - const claimId1 = await getClaimId(work1).catch(returnError) - const claimId2 = await getClaimId(work2).catch(returnError) - - assert({ - given: 'two claims with disordered keys', - should: 'have the same claims id', - actual: claimId1 === claimId2, - expected: true, - }) - } - - { - const work1: Claim = makeClaim({ - name: TheRaven.attributes.name, - author: TheRaven.attributes.author, - }) - - const work2: Claim = makeClaim({ - Author: TheRaven.attributes.author, - NAME: TheRaven.attributes.name, - }) - - const claimId1 = await getClaimId(work1).catch(returnError) - const claimId2 = await getClaimId(work2).catch(returnError) - - assert({ - given: 'two claims with keys casing', - should: 'NOT have the same claims id', - actual: claimId1 !== claimId2, - expected: true, - }) - } - - { - const signature = await getClaimSignature(TheRaven, PrivateKeyEAP) - - assert({ - given: 'a signature of a Claim', - should: 'be the signature equal to of work signature', - actual: signature, - expected: TheRaven.signature, - }) - } - - { - const expectedMessage = 'Cannot sign a claim that has an empty .id field.' - - assert({ - given: 'a claim without id', - should: `throw an error with the message ${expectedMessage}`, - actual: await getClaimSignature({ ...TheRaven, id: '' }, PrivateKeyEAP).catch(returnError), - expected: new Error(expectedMessage), - }) - } - - { - const expectedMessage = 'Cannot sign a claim whose id has been altered or generated incorrectly.' - - assert({ - given: 'a claim without id', - should: `throw an error with the message ${expectedMessage}`, - actual: await getClaimSignature( - { ...TheRaven, id: 'be81cc75bcf6ca0f1fdd356f460e6ec920ba36ec78bd9e70c4d04a19f8943102' }, - PrivateKeyEAP - ).catch(returnError), - expected: new Error(expectedMessage), - }) - } - - { - const expectedMessage = 'Cannot sign a claim that has an empty .publicKey field.' - - assert({ - given: 'a claim with publicKey undefined', - should: `throw an error with the message ${expectedMessage}`, - actual: await getClaimSignature({ ...TheRaven, publicKey: undefined }, PrivateKeyEAP).catch(returnError), - expected: new Error(expectedMessage), - }) - } - - { - const expectedMessage = `Cannot sign this claim with the provided privateKey. It doesn\t match the claim's public key.` - - assert({ - given: 'a claim with a different publicKey', - should: `throw an error with the message ${expectedMessage}`, - actual: await getClaimSignature( - { ...TheRaven, publicKey: '03f0dc475e93105bdc7701b40003200039202ffd4a0789696c78f9b34d5518aef9' }, - PrivateKeyEAP - ).catch(returnError), - expected: new Error(expectedMessage), - }) - } - - { - assert({ - given: 'a valid claim, isClaim', - should: `return true`, - actual: isClaim(TheRaven), - expected: true, - }) - } - - { - assert({ - given: 'an object that is not a claim', - should: `return false`, - actual: await isValidClaim({ foo: 'bar' }), - expected: false, - }) - } - - { - assert({ - given: 'a claim with an invalid id', - should: `return false`, - actual: await isValidClaim({ ...TheRaven, id: '111' }), - expected: false, - }) - } - - { - assert({ - given: 'a claim with an invalid publicKey', - should: `return false`, - actual: await isValidClaim({ ...TheRaven, publicKey: '111' }), - expected: false, - }) - } - - { - assert({ - given: 'a claim with an invalid signature', - should: `return false`, - actual: await isValidClaim({ ...TheRaven, signature: '111' }), - expected: false, - }) - } - - { - const result = await Promise.all( - ['', false, null, undefined].map(value => isValidClaim({ ...TheRaven, created: value })) - ) - - assert({ - given: 'a claim with an invalid input', - should: `return false`, - actual: result, - expected: [false, false, false, false], - }) - } -}) diff --git a/src/Claim.ts b/src/Claim.ts deleted file mode 100644 index 88178aa..0000000 --- a/src/Claim.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* tslint:disable:no-relative-imports */ -import * as bitcore from 'bitcore-lib' -import * as crypto from 'crypto' -import { canonize } from 'jsonld' - -import { IllegalArgumentException } from './Exceptions' -import { Claim, ClaimAttributes, ClaimType, ClaimContext, isClaim } from './Interfaces' - -export const canonizeClaim = async (claim: Claim): Promise => { - const contextualClaim = { ...ClaimContext, ...claim } - return canonize(contextualClaim) -} - -export const getClaimId = async (claim: Claim): Promise => { - const canonizedClaim = await canonizeClaim(claim) - const buffer = Buffer.from(canonizedClaim) - return crypto - .createHash('sha256') - .update(buffer) - .digest() - .toString('hex') -} - -export const getClaimSignature = async (claim: Claim, privateKey: string): Promise => { - if (!claim.publicKey) throw new IllegalArgumentException('Cannot sign a claim that has an empty .publicKey field.') - if (new bitcore.PrivateKey(privateKey).publicKey.toString() !== claim.publicKey) - throw new IllegalArgumentException( - "Cannot sign this claim with the provided privateKey. It doesn\t match the claim's public key." - ) - const generatedClaimId = await getClaimId(claim) - if (!claim.id) throw new IllegalArgumentException('Cannot sign a claim that has an empty .id field.') - if (claim.id !== generatedClaimId) - throw new IllegalArgumentException('Cannot sign a claim whose id has been altered or generated incorrectly.') - - const signature = bitcore.crypto.ECDSA.sign(Buffer.from(claim.id, 'hex'), new bitcore.PrivateKey(privateKey)) - return signature.toString() -} - -export const isValidSignature = (claim: Claim): boolean => { - try { - return bitcore.crypto.ECDSA.verify( - Buffer.from(claim.id, 'hex'), - bitcore.crypto.Signature.fromString(claim.signature), - new bitcore.PublicKey(claim.publicKey) - ) - } catch (exception) { - return false - } -} - -export const createClaim = async (privateKey: string, type: ClaimType, attributes: ClaimAttributes): Promise => { - const claim: Claim = { - id: '', - publicKey: new bitcore.PrivateKey(privateKey).publicKey.toString(), - signature: '', - type, - created: new Date().toISOString(), - attributes, - } - const id = await getClaimId(claim) - const signature = await getClaimSignature( - { - ...claim, - id, - }, - privateKey - ) - return { - ...claim, - id, - signature, - } -} - -export const isValidClaim = async (claim: {}): Promise => - !!isClaim(claim) && isValidSignature(claim) && (await getClaimId(claim)) === claim.id diff --git a/src/Interfaces.test.ts b/src/Interfaces.test.ts index 5ab7977..de869df 100644 --- a/src/Interfaces.test.ts +++ b/src/Interfaces.test.ts @@ -1,35 +1,7 @@ /* tslint:disable:no-relative-imports */ import { describe } from 'riteway' -import { ClaimType, Identity, isClaim, isIdentity, Work } from './Interfaces' - -const TheRaven: Work = { - id: '1bb5e7959c7cb28936ec93eb6893094241a5bc396f08845b4f52c86034f0ddf8', - publicKey: '02badf4650ba545608242c2d303d587cf4f778ae3cf2b3ef99fbda37555a400fd2', - signature: - '3045022100e020a7ffeffa5d40ffde618c6c861678e38de69fd377028ec57ad93893883b3702201f085284a9064bab7e1cd39349e65d136d8f67e4b6b897c3e7db6b400ed91034', - type: ClaimType.Work, - created: '2017-11-13T15:00:00.000Z', - attributes: { - name: 'The Raven', - author: 'Edgar Allan Poe', - tags: 'poem', - dateCreated: '', - datePublished: '1845-01-29T03:00:00.000Z', - text: 'Once upon a midnight dreary...', - }, -} - -const Me: Identity = { - id: '1bb5e7959c7cb28936ec93eb6893094241a5bc396f08845b4f52c86034f0ddf8', - publicKey: '02badf4650ba545608242c2d303d587cf4f778ae3cf2b3ef99fbda37555a400fd2', - signature: - '3045022100e020a7ffeffa5d40ffde618c6c861678e38de69fd377028ec57ad93893883b3702201f085284a9064bab7e1cd39349e65d136d8f67e4b6b897c3e7db6b400ed91034', - type: ClaimType.Identity, - created: '2017-11-13T15:00:00.000Z', - attributes: { - publicKey: '02badf4650ba545608242c2d303d587cf4f778ae3cf2b3ef99fbda37555a400fd2', - }, -} +import { MyIdentity, Ed25519TheRaven, Ed25519SignedRaven, RsaTheRaven, RsaSignedRaven } from '../tests/unit/shared' +import { ClaimType, isIdentity, isSignedVerifiableClaim, isVerifiableClaim, isWork } from './Interfaces' const InvalidClaim = { id: '1bb5e7959c7cb28936ec93eb6893094241a5bc396f08845b4f52c86034f0ddf8', @@ -42,55 +14,87 @@ const InvalidClaim = { }, } -describe('Interfaces', async (assert: any) => { +describe('Interfaces.isIdentity', async (assert: any) => { { assert({ - given: 'a valid claim', + given: 'a valid Identity claim', should: 'return true', - actual: isClaim(TheRaven), + actual: isIdentity(MyIdentity), expected: true, }) assert({ - given: 'an invalid claim, isClaim', + given: 'a valid Work claim', should: 'return false', - actual: isClaim(InvalidClaim), + actual: isIdentity(Ed25519TheRaven), + expected: false, + }) + + assert({ + given: 'a valid Work claim', + should: 'return false', + actual: isIdentity(RsaTheRaven), expected: false, }) } +}) +describe('Interfaces.isWork', async (assert: any) => { { assert({ - given: 'a valid Identity claim, isClaim', + given: 'a valid Work claim', should: 'return true', - actual: isClaim(Me), + actual: isWork(Ed25519TheRaven), expected: true, }) - } - { assert({ - given: 'a valid Identity claim, isIdentity', + given: 'a valid Work Claim', should: 'return true', - actual: isIdentity(Me), + actual: isWork(RsaTheRaven), expected: true, }) + } + { assert({ - given: 'a valid Work claim, isIdentity', + given: 'a valid Identity Claim', should: 'return false', - actual: isIdentity(TheRaven), + actual: isWork(MyIdentity), expected: false, }) } +}) +describe('Interfaces.isVerifiableClaim', async (assert: any) => { { assert({ - given: 'a valid claim with a context', + given: 'a valid Verifiable Work Claim', + should: `return true`, + actual: isVerifiableClaim(Ed25519TheRaven), + expected: true, + }) + + assert({ + given: 'a valid Verifiable RSA Work Claim', + should: 'return true', + actual: isVerifiableClaim(RsaTheRaven), + expected: true, + }) + + assert({ + given: 'a valid Verifiable Identity Claim', should: 'return true', - actual: isClaim(TheRaven), + actual: isVerifiableClaim(MyIdentity), expected: true, }) + + assert({ + given: 'an invalid Verifiable Claim', + should: 'return false', + actual: isVerifiableClaim(InvalidClaim), + expected: false, + }) } ;['', false, null, undefined].forEach(value => { @@ -98,9 +102,44 @@ describe('Interfaces', async (assert: any) => { assert({ given: 'a claim with an invalid date', should: `return false`, - actual: isClaim({ ...TheRaven, created: value }), + actual: isVerifiableClaim({ ...Ed25519TheRaven, issuanceDate: value }), expected: false, }) } }) }) + +describe('Interfaces.isSignedVerifiableClaim', async (assert: any) => { + assert({ + given: 'a Singed Verifiable Ed25519 Claim', + should: 'return true', + actual: isSignedVerifiableClaim(Ed25519SignedRaven), + expected: true, + }) + + assert({ + given: 'a Signed Verifiable Rsa Claim', + should: 'return true', + actual: isSignedVerifiableClaim(RsaSignedRaven), + expected: true, + }) + + assert({ + given: 'an unsigned VerifiableClaim', + should: 'return false', + actual: isSignedVerifiableClaim(Ed25519TheRaven), + expected: false, + }) + + const badObject = { + ...Ed25519SignedRaven, + id: 'something too short', + } + + assert({ + given: 'a signed Verifiable Claim in an invalid id', + should: 'return false', + actual: isSignedVerifiableClaim(badObject), + expected: false, + }) +}) diff --git a/src/Interfaces.ts b/src/Interfaces.ts index c7a4d1d..1d4be7f 100644 --- a/src/Interfaces.ts +++ b/src/Interfaces.ts @@ -1,78 +1,175 @@ -export interface Claim { - readonly '@context'?: any - readonly id?: string - - readonly issuer?: string - readonly issuanceDate?: Date // should replace dateCreated... or created... once we remove protobufs - readonly publicKey?: string - readonly signature?: string - readonly created?: string +/* tslint:disable:no-relative-imports */ +import * as Joi from 'joi' +import * as JSONLD_SIGS from 'jsonld-signatures' + +import { IllegalArgumentException } from './Exceptions' + +export enum ClaimType { + Identity = 'Identity', + Work = 'Work', +} + +export interface BaseVerifiableClaim { + readonly '@context': any + readonly issuer: string + readonly issuanceDate: string readonly type: ClaimType - readonly attributes: T // TODO: replace attributes with claim, once we remove protobufs + readonly claim: object +} +export interface VerifiableClaim extends BaseVerifiableClaim { + readonly id: string } -interface ClaimContext { - readonly '@context': { - readonly [key: string]: string - } +export interface SignedVerifiableClaim extends VerifiableClaim { + readonly 'sec:proof': any } -export function isClaim(object: any): object is Claim { - // TODO: use joi or protobuf - return !!(object.id && object.publicKey && object.signature && object.type && object.attributes && object.created) +export interface ClaimContext { + readonly [key: string]: unknown +} + +export interface ClaimTypeContexts { + readonly [key: string]: ClaimContext +} + +export interface SigningOptions { + readonly algorithm: SigningAlgorithm + readonly nonce: string +} + +export interface Ed25519SigningOptions extends SigningOptions { + readonly privateKeyBase58: string +} + +export interface RsaSigningOptions extends SigningOptions { + readonly privateKeyPem: string } // WARNING: This MUST account for ALL of the attributes in a Claim, except for id, @context, and signature. // Otherwise those attributes without context will be left out of the canonized/signed claim. // Refer to https://www.w3.org/2018/jsonld-cg-reports/json-ld/#the-context -export const ClaimContext: ClaimContext = { - '@context': { - attributes: 'http://schema.org/CreativeWork', - author: 'http://schema.org/author', - text: 'http://schema.org/text', - created: 'http://purl.org/dc/terms/created', - dateCreated: 'http://schema.org/dateCreated', - datePublished: 'http://schema.org/datePublished', - name: 'http://schema.org/name', - tags: 'http://schema.org/keywords', - type: 'http://schema.org/additionalType', - publicKey: 'http://schema.org/Text', +export const DefaultClaimContext: ClaimContext = { + cred: 'https://w3id.org/credentials#', + dc: 'http://purl.org/dc/terms/', + schema: 'http://schema.org/', + sec: 'https://w3id.org/security#', + + id: 'sec:digestValue', + issuer: 'cred:issuer', + issuanceDate: 'cred:issued', + type: 'schema:additionalType', + claim: 'schema:Thing', // The most generic definition in schema.org, +} + +export const DefaultWorkClaimContext: ClaimContext = { + archiveUrl: 'schema:url', + author: 'schema:author', + canonicalUrl: 'schema:url', + claim: 'schema:CreativeWork', + contributors: { + '@id': 'schema:ItemList', + '@container': '@list', + '@type': 'schema:contributor', }, + copyrightHolder: 'schema:copyrightHolder', + dateCreated: 'schema:dateCreated', + datePublished: 'schema:datePublished', + license: 'schema:license', + name: 'schema:name', + tags: 'schema:keywords', + hash: 'sec:digestValue', } -export interface ClaimAttributes { - readonly [key: string]: string +const verifiableClaimSchema = { + '@context': Joi.object(), + id: Joi.string() + .required() + .alphanum() + .min(64), + issuer: Joi.string() + .required() + .uri({ + scheme: ['po.et', 'http', 'https', 'did', 'data'], + }), + issuanceDate: Joi.string() + .required() + .isoDate(), + type: Joi.string() + .required() + .only([ClaimType.Identity, ClaimType.Work]), + claim: Joi.object().required(), } -export enum ClaimType { - Identity = 'Identity', - Work = 'Work', +const signatureSchema = { + '@graph': Joi.object().keys({ + '@type': Joi.string().only(Object.keys(JSONLD_SIGS.suites).map(suite => `sec:${suite}`)), + 'http://purl.org/dc/terms/created': Joi.object().keys({ + '@type': Joi.string().uri(), + '@value': Joi.string() + .required() + .isoDate(), + }), + 'http://purl.org/dc/terms/creator': Joi.object({ + '@id': Joi.string() + .required() + .uri({ scheme: ['po.et', 'http', 'https', 'did', 'data'] }), + }), + 'dc:creator': Joi.object({ + '@id': Joi.string() + .required() + .uri({ scheme: ['po.et', 'http', 'https', 'did', 'data'] }), + }), + 'https://w3id.org/security#jws': Joi.string(), + 'dc:created': Joi.object().keys({ + '@type': Joi.string().uri(), + '@value': Joi.string() + .required() + .isoDate(), + }), + 'sec:jws': Joi.string(), + 'sec:nonce': Joi.string(), + }), } -export interface WorkAttributes extends ClaimAttributes { - readonly name: string - readonly datePublished: string - readonly dateCreated: string - readonly author: string - readonly tags?: string - readonly text: string +const signedVerifiableClaimSchema = { + ...verifiableClaimSchema, + 'sec:proof': Joi.object(signatureSchema).required(), } -export interface Work extends Claim {} +export interface CreateVerifiableClaimConfig { + readonly issuer: string + readonly type?: ClaimType + readonly context?: ClaimContext +} -export interface Identity extends Claim {} +export const isVerifiableClaim = (object: any): object is VerifiableClaim => { + return Joi.validate(object, verifiableClaimSchema).error === null +} + +export const isSignedVerifiableClaim = (object: any): object is SignedVerifiableClaim => { + return Joi.validate(object, signedVerifiableClaimSchema).error === null +} + +export const DefaultIdentityClaimContext: ClaimContext = { + publicKey: 'sec:publicKeyBase58', + profileUrl: 'sec:owner', +} -export interface IdentityAttributes extends ClaimAttributes { - readonly profileUrl?: string - readonly publicKey: string +export const claimTypeDefaults: ClaimTypeContexts = { + Work: DefaultWorkClaimContext, + Identity: DefaultIdentityClaimContext, } -export function isWork(claim: Claim): claim is Work { - return claim.type === ClaimType.Work +export interface Work extends VerifiableClaim {} + +export interface Identity extends VerifiableClaim {} + +export function isWork(verifiableClaim: VerifiableClaim): verifiableClaim is Work { + return verifiableClaim.type === ClaimType.Work } -export function isIdentity(claim: Claim): claim is Identity { - return claim.type === ClaimType.Identity +export function isIdentity(verifiableClaim: VerifiableClaim): verifiableClaim is Identity { + return verifiableClaim.type === ClaimType.Identity } export interface PoetAnchor { @@ -94,3 +191,27 @@ export interface PoetBlockAnchor extends PoetTransactionAnchor { export enum StorageProtocol { IPFS = 0, } + +export enum SigningAlgorithm { + Ed25519Signature2018 = 'Ed25519Signature2018', + RsaSignature2018 = 'RsaSignature2018', +} + +export const getSigningAlgorithm = (algorithmName: string): SigningAlgorithm => { + switch (algorithmName) { + case SigningAlgorithm.Ed25519Signature2018: { + return SigningAlgorithm.Ed25519Signature2018 + } + case SigningAlgorithm.RsaSignature2018: { + return SigningAlgorithm.RsaSignature2018 + } + default: { + throw new IllegalArgumentException(`Unsupported Signing Algorithm ${algorithmName}`) + } + } +} + +export enum AlgorithmPublicKeyType { + Ed25519VerificationKey2018 = 'Ed25519VerificationKey2018', + RsaVerificationKey2018 = 'RsaVerificationKey2018', +} diff --git a/src/VerifiableClaim.test.ts b/src/VerifiableClaim.test.ts new file mode 100644 index 0000000..108816c --- /dev/null +++ b/src/VerifiableClaim.test.ts @@ -0,0 +1,257 @@ +/* tslint:disable:no-relative-imports */ +import { describe } from 'riteway' + +import { + BaseEd25519RavenClaim, + ed25519Base58PrivateKey, + Ed25519TheRaven, + externalContext, + getErrorMessage, + makeClaim, + MyIdentity, + TheRavenBook, + TheRavenBookClaim, + TheRavenClaim, +} from '../tests/unit/shared' + +import { + BaseVerifiableClaim, + ClaimType, + DefaultClaimContext, + DefaultIdentityClaimContext, + DefaultWorkClaimContext, + isVerifiableClaim, + SigningAlgorithm, +} from './Interfaces' + +import { configureCreateVerifiableClaim, generateClaimId } from './VerifiableClaim' +import { createIssuerFromPrivateKey, generateRsaKeyPems } from './util/KeyHelper' + +const ravenClaim: any = { ...Ed25519TheRaven.claim } + +const ravenBookClaim: any = { ...TheRavenBook.claim } + +describe('VerifiableClaim.generateClaimId', async (assert: any) => { + { + assert({ + given: 'a claim id', + should: 'be equal to the work id', + actual: await generateClaimId(Ed25519TheRaven).catch(getErrorMessage), + expected: Ed25519TheRaven.id, + }) + } + + { + const id = await generateClaimId({ ...BaseEd25519RavenClaim, issuanceDate: '2017-09-13T15:00:00.000Z' }).catch( + getErrorMessage + ) + + assert({ + given: 'a claim with extra dateCreated, the new dateCreated', + should: 'be included in the calculation of the id and should be not equal to the work id', + actual: id !== Ed25519TheRaven.id, + expected: true, + }) + } + + { + const work1: BaseVerifiableClaim = makeClaim({ + name: ravenClaim.name, + author: ravenClaim.author, + }) + + const work2: BaseVerifiableClaim = makeClaim({ + author: ravenClaim.author, + name: ravenClaim.name, + }) + + const id1 = await generateClaimId(work1).catch(getErrorMessage) + const id2 = await generateClaimId(work2).catch(getErrorMessage) + + assert({ + given: 'two claims with disordered keys', + should: 'have the same claims id', + actual: id1 === id2, + expected: true, + }) + } + + { + const work1: BaseVerifiableClaim = makeClaim({ + name: ravenClaim.name, + author: ravenClaim.author, + }) + + const work2: BaseVerifiableClaim = makeClaim({ + author: ravenClaim.author, + nAME: ravenClaim.name, + }) + + const id1 = await generateClaimId(work1) + const id2 = await generateClaimId(work2) + + assert({ + given: 'two claims with different keys casing', + should: 'NOT have the same claims id', + actual: id1 !== id2, + expected: true, + }) + } + + { + const workClaim = makeClaim({ + name: ravenClaim.name, + author: ravenClaim.author, + }) + + const TheRavenId = '9bd16701a14884f129625f399ddad0b956b7883a5ef30142d0cda28f38988eca' + + assert({ + given: 'A work claim', + should: 'generate an id for the claim', + actual: await generateClaimId(workClaim), + expected: TheRavenId, + }) + + const identityClaim = makeClaim({ ...MyIdentity.claim }) + + assert({ + given: 'an verifiable identity claim', + should: 'generate an id for the claim', + actual: await generateClaimId(identityClaim), + expected: 'b609122e7988bf3e9a4fd7b4336748bce1ae7134a7317e7009661c6936d55722', + }) + + const workClaim2 = makeClaim({ + name: ravenBookClaim.name, + author: ravenBookClaim.author, + isbn: ravenBookClaim.isbn, + edition: ravenBookClaim.edition, + }) + + assert({ + given: 'an extended work claim WITHOUT proper context', + should: 'generate the id without the extra attributes', + actual: await generateClaimId(workClaim2), + expected: TheRavenId, + }) + + assert({ + given: 'an extended work claim WITH proper context', + should: 'generate a different id', + actual: (await generateClaimId({ ...workClaim2, '@context': externalContext })) !== TheRavenId, + expected: true, + }) + } +}) + +describe('Claim.configureCreateVerifiableClaim with Ed25519 Issuer', async (assert: any) => { + const issuer = createIssuerFromPrivateKey(ed25519Base58PrivateKey) + + { + const createWorkClaim = configureCreateVerifiableClaim({ issuer }) + const verifiableClaim = await createWorkClaim(TheRavenClaim) + + assert({ + given: 'a claim', + should: 'create a Verifiable Claim', + actual: isVerifiableClaim(verifiableClaim), + expected: true, + }) + + assert({ + given: 'a work claim, default work type and context', + should: 'return a verifiable work claim with the correct context', + actual: verifiableClaim['@context'], + expected: { ...DefaultClaimContext, ...DefaultWorkClaimContext }, + }) + + assert({ + given: 'a work claim, default work type and context', + should: 'return a verifiable claim of type work', + actual: verifiableClaim.type, + expected: ClaimType.Work, + }) + } + + { + const createWorkClaim = configureCreateVerifiableClaim({ issuer, context: externalContext }) + const verifiableWorkClaim = await createWorkClaim(TheRavenBookClaim) + + assert({ + given: 'a work claim, default claim type of work, and an extended context', + should: 'return a verifiable work claim with the correct context', + actual: verifiableWorkClaim['@context'], + expected: { ...DefaultClaimContext, ...DefaultWorkClaimContext, ...externalContext }, + }) + + assert({ + given: 'a work claim, default claim type of work, and an extended context', + should: 'return a verifiable claim of type work', + actual: verifiableWorkClaim.type, + expected: ClaimType.Work, + }) + } + + { + const createIdentityClaim = configureCreateVerifiableClaim({ issuer, type: ClaimType.Identity }) + const verifiableIdentityClaim = await createIdentityClaim(MyIdentity.claim) + + assert({ + given: 'an identity claim, identity claim type and default context', + should: 'return a verifiable identity claim with the correct context', + actual: verifiableIdentityClaim['@context'], + expected: { ...DefaultClaimContext, ...DefaultIdentityClaimContext }, + }) + + assert({ + given: 'an identity claim, identity claim type and default context', + should: 'return a verifiable claim of type Identity', + actual: verifiableIdentityClaim.type, + expected: ClaimType.Identity, + }) + } + + { + const externalIdentityContext = { githubUrl: 'schema:url' } + const createIdentityClaim = configureCreateVerifiableClaim({ + issuer, + type: ClaimType.Identity, + context: externalIdentityContext, + }) + const verifiableIdentityClaim = await createIdentityClaim({ + ...MyIdentity.claim, + githubUrl: 'https://github.com/poetapp', + }) + + assert({ + given: 'an identity claim, identity claim type and an extended context', + should: 'return a verifiable claim with the correct context', + actual: verifiableIdentityClaim['@context'], + expected: { ...DefaultClaimContext, ...DefaultIdentityClaimContext, ...externalIdentityContext }, + }) + + assert({ + given: 'an identity claim, identity claim type and an extended context', + should: 'return a verifiable claim of type Identity', + actual: verifiableIdentityClaim.type, + expected: ClaimType.Identity, + }) + } +}) + +describe('Claim.configureCreateVerifiableClaim with RSA Issuer', async (assert: any) => { + { + const rsaPrivateKeyPem = generateRsaKeyPems().privateKey + const issuer = createIssuerFromPrivateKey(rsaPrivateKeyPem, SigningAlgorithm.RsaSignature2018) + const createWorkClaim = configureCreateVerifiableClaim({ issuer }) + const verifiableClaim = await createWorkClaim(TheRavenClaim) + + assert({ + given: 'a claim', + should: 'create a Verifiable Claim', + actual: isVerifiableClaim(verifiableClaim), + expected: true, + }) + } +}) diff --git a/src/VerifiableClaim.ts b/src/VerifiableClaim.ts new file mode 100644 index 0000000..c7c7792 --- /dev/null +++ b/src/VerifiableClaim.ts @@ -0,0 +1,53 @@ +/* tslint:disable:no-relative-imports */ +import * as crypto from 'crypto' +import * as JSONLD from 'jsonld' + +import { + BaseVerifiableClaim, + ClaimType, + claimTypeDefaults, + CreateVerifiableClaimConfig, + DefaultClaimContext, + VerifiableClaim, +} from './Interfaces' + +const canonizeClaim = async (document: BaseVerifiableClaim): Promise => { + const contextualClaim = { + type: document.type, + '@context': { + ...DefaultClaimContext, + ...claimTypeDefaults[document.type], + ...document['@context'], + }, + issuer: document.issuer, + issuanceDate: document.issuanceDate, + claim: document.claim, + } + return JSONLD.canonize(contextualClaim) +} + +export const generateClaimId = async (claim: BaseVerifiableClaim): Promise => { + const canonizedClaim = await canonizeClaim(claim) + const buffer = Buffer.from(canonizedClaim) + return crypto + .createHash('sha256') + .update(buffer) + .digest() + .toString('hex') +} + +export const configureCreateVerifiableClaim = ({ + issuer, + type = ClaimType.Work, + context = {}, +}: CreateVerifiableClaimConfig) => async (claim: object): Promise => { + const verifiableClaim = { + '@context': { ...DefaultClaimContext, ...claimTypeDefaults[type], ...context }, + type, + issuer, + issuanceDate: new Date().toISOString(), + claim, + } + const id = await generateClaimId(verifiableClaim) + return { ...verifiableClaim, id } +} diff --git a/src/VerifiableClaimSigner.test.ts b/src/VerifiableClaimSigner.test.ts new file mode 100644 index 0000000..b1b51e6 --- /dev/null +++ b/src/VerifiableClaimSigner.test.ts @@ -0,0 +1,418 @@ +/* tslint:disable:no-relative-imports */ +import { describe } from 'riteway' + +import { + claimSigner, + ed25519Base58PrivateKey, + Ed25519TheRaven, + externalContext, + getErrorMessage, + MyIdentity, + rsaPemPrivateKey, + testBadPublicKey, + TheRavenBook, + TheRavenBookClaim, + TheRavenClaim, +} from '../tests/unit/shared' + +import { + ClaimType, + DefaultClaimContext, + DefaultWorkClaimContext, + isSignedVerifiableClaim, + SigningAlgorithm, +} from './Interfaces' +import { configureCreateVerifiableClaim } from './VerifiableClaim' +import { createIssuerFromPrivateKey } from './util/KeyHelper' + +const { configureSignVerifiableClaim, isValidSignedVerifiableClaim, isValidSignature } = claimSigner +const rsaIssuer = createIssuerFromPrivateKey(rsaPemPrivateKey, SigningAlgorithm.RsaSignature2018) + +describe('VerifiableClaimSigner.configureSignVerifiableClaim - Ed25519', async (assert: any) => { + { + const issuer = createIssuerFromPrivateKey(ed25519Base58PrivateKey) + const createVerifiableWorkClaim = configureCreateVerifiableClaim({ issuer }) + const verifiableWorkClaim = await createVerifiableWorkClaim(TheRavenClaim) + const signWorkClaim = configureSignVerifiableClaim({ privateKey: ed25519Base58PrivateKey }) + const signedWorkClaim = await signWorkClaim(verifiableWorkClaim) + + assert({ + given: 'a signed work c laim, the creator of the signature proof', + should: 'be equal to the issuer of the Verifiable Claim', + actual: signedWorkClaim['sec:proof']['@graph']['dc:creator']['@id'], + expected: issuer, + }) + + assert({ + given: 'a Signed Verifiable Work Claim', + should: 'have a valid Signature', + actual: await isValidSignature(signedWorkClaim), + expected: true, + }) + + assert({ + given: 'a Signed Verifiable Work Claim', + should: 'be a valid Claim', + actual: await isValidSignedVerifiableClaim(signedWorkClaim), + expected: true, + }) + } + + { + const issuer = createIssuerFromPrivateKey(ed25519Base58PrivateKey) + const createVerifiableWorkClaim = configureCreateVerifiableClaim({ issuer, context: externalContext }) + const verifiableWorkClaim = await createVerifiableWorkClaim(TheRavenBookClaim) + const signWorkClaim = configureSignVerifiableClaim({ privateKey: ed25519Base58PrivateKey }) + const signedWorkClaim = await signWorkClaim(verifiableWorkClaim) + + assert({ + given: 'a Signed Verifiable Work Claim with an external context', + should: 'include all fields in the Verfiable Work Claim', + actual: JSON.stringify(Object.keys(signedWorkClaim.claim).sort()), + expected: JSON.stringify(Object.keys(TheRavenBook.claim).sort()), + }) + } + + { + const issuer = createIssuerFromPrivateKey(ed25519Base58PrivateKey) + const createVerifiableWorkClaim = configureCreateVerifiableClaim({ issuer }) + const verifiableWorkClaim = await createVerifiableWorkClaim(TheRavenBookClaim) + const signWorkClaim = configureSignVerifiableClaim({ privateKey: ed25519Base58PrivateKey }) + const signedWorkClaim = await signWorkClaim(verifiableWorkClaim).catch(getErrorMessage) + + assert({ + given: 'an extended verififable claim without an external context, signVerifiableClaim', + should: 'will return an Error()', + actual: signedWorkClaim, + expected: 'The property "edition" in the input was not defined in the context.', + }) + } +}) + +describe('VerifiableClaimSigner.configureSignVerifiableClaim - RSA', async (assert: any) => { + { + const createVerifiableWorkClaim = configureCreateVerifiableClaim({ issuer: rsaIssuer }) + const verifiableWorkClaim = await createVerifiableWorkClaim(TheRavenClaim) + const signWorkClaim = configureSignVerifiableClaim({ + privateKey: rsaPemPrivateKey, + algorithm: SigningAlgorithm.RsaSignature2018, + }) + const signedWorkClaim = await signWorkClaim(verifiableWorkClaim) + + assert({ + given: 'a signed work claim, the creator of the signature proof', + should: 'be equal to the issuer of the Verifiable Claim', + actual: signedWorkClaim['sec:proof']['@graph']['dc:creator']['@id'], + expected: rsaIssuer, + }) + + assert({ + given: 'a Signed Verifiable Work Claim', + should: 'have a valid Signature', + actual: await isValidSignature(signedWorkClaim), + expected: true, + }) + + assert({ + given: 'a Signed Verifiable Work Claim', + should: 'be a valid Claim', + actual: await isValidSignedVerifiableClaim(signedWorkClaim), + expected: true, + }) + } + + { + const createVerifiableIdentityClaim = configureCreateVerifiableClaim({ + issuer: rsaIssuer, + type: ClaimType.Identity, + }) + const verifiableIdentityClaim = await createVerifiableIdentityClaim(MyIdentity.claim) + + const signIdentityClaim = configureSignVerifiableClaim({ + privateKey: rsaPemPrivateKey, + algorithm: SigningAlgorithm.RsaSignature2018, + }) + const signedIdentityClaim = await signIdentityClaim(verifiableIdentityClaim) + + assert({ + given: 'a signed identity claim, the creator of the signature proof', + should: 'be equal to the issuer of the claim ', + actual: signedIdentityClaim['sec:proof']['@graph']['dc:creator']['@id'], + expected: rsaIssuer, + }) + + assert({ + given: 'a Signed Verifiable Identity Claim', + should: 'have a valid signature', + actual: await isValidSignature(signedIdentityClaim), + expected: true, + }) + + assert({ + given: 'a Signed Verifiable Identity Claim', + should: 'be a valid claim', + actual: await isValidSignedVerifiableClaim(signedIdentityClaim), + expected: true, + }) + } + + { + const createVerifiableWorkClaim = configureCreateVerifiableClaim({ issuer: rsaIssuer, context: externalContext }) + const verifiableWorkClaim = await createVerifiableWorkClaim(TheRavenBookClaim) + const signWorkClaim = configureSignVerifiableClaim({ + privateKey: rsaPemPrivateKey, + algorithm: SigningAlgorithm.RsaSignature2018, + }) + const signedWorkClaim = await signWorkClaim(verifiableWorkClaim) + + assert({ + given: 'a Signed Verifiable Claim with an external context', + should: 'include all fields in the Verfiable Claim', + actual: + JSON.stringify(Object.keys(signedWorkClaim.claim).sort()) === + JSON.stringify(Object.keys(TheRavenBook.claim).sort()), + expected: true, + }) + } + + { + const createVerifiableWorkClaim = configureCreateVerifiableClaim({ issuer: rsaIssuer }) + const verifiableWorkClaim = await createVerifiableWorkClaim(TheRavenBookClaim) + const signWorkClaim = configureSignVerifiableClaim({ + privateKey: rsaPemPrivateKey, + algorithm: SigningAlgorithm.RsaSignature2018, + }) + const signedWorkClaim = await signWorkClaim(verifiableWorkClaim).catch(getErrorMessage) + + assert({ + given: 'an extended verififable claim without an external context', + should: 'will return an Error()', + actual: signedWorkClaim, + expected: 'The property "edition" in the input was not defined in the context.', + }) + } +}) + +describe('VerifiableClaimSigner.isValidSignature - Ed25519', async (assert: any) => { + { + const signWorkClaim = configureSignVerifiableClaim({ privateKey: ed25519Base58PrivateKey }) + const signedWorkClaim = await signWorkClaim(Ed25519TheRaven) + + assert({ + given: 'a Signed Verifiable Claim with a valid signature', + should: 'return true', + actual: await isValidSignature(signedWorkClaim), + expected: true, + }) + } + + { + const issuer = createIssuerFromPrivateKey(ed25519Base58PrivateKey) + const createVerifiableClaim = configureCreateVerifiableClaim({ + issuer, + type: ClaimType.Work, + context: externalContext, + }) + const signWorkClaim = configureSignVerifiableClaim({ privateKey: ed25519Base58PrivateKey }) + + const verifiableWorkClaim = await createVerifiableClaim(TheRavenBookClaim) + const signedWorkClaim = await signWorkClaim(verifiableWorkClaim) + + assert({ + given: 'a Signed Verifiable Claim with an external context', + should: 'return true', + actual: await isValidSignature(signedWorkClaim), + expected: true, + }) + } + + { + const badClaim = { + '@context': { ...DefaultClaimContext, ...DefaultWorkClaimContext }, + ...Ed25519TheRaven, + 'sec:proof': { + '@graph': { + '@type': 'sec:Ed25519Signature2018', + 'dc:created': { + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + '@value': '2018-09-05T20:19:20Z', + }, + 'dc:creator': { + '@id': `data:,${testBadPublicKey}`, + }, + 'sec:jws': + 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..TSHkMOwbWZvIp8Hd-MyebaMgItf4Iyl3dgUSlHBBlnidw' + + 'gzo084pGpKmbOewYFrXfmAVhXnC4UPzaPUjaU9BDw', + }, + }, + } + + assert({ + given: 'a Signed Verifiable claim with an altered public key', + should: 'return false', + actual: await isValidSignature(badClaim), + expected: false, + }) + } +}) + +describe('VerifiableClaimSigner.isValidSignature - RSA', async (assert: any) => { + { + const createVerifiableClaim = configureCreateVerifiableClaim({ + issuer: rsaIssuer, + type: ClaimType.Work, + context: externalContext, + }) + const signWorkClaim = configureSignVerifiableClaim({ + privateKey: rsaPemPrivateKey, + algorithm: SigningAlgorithm.RsaSignature2018, + }) + + const verifiableWorkClaim = await createVerifiableClaim(TheRavenBookClaim) + const signedWorkClaim = await signWorkClaim(verifiableWorkClaim) + + assert({ + given: 'a Signed Verifiable Claim with a valid signature', + should: 'return true', + actual: await isValidSignature(signedWorkClaim), + expected: true, + }) + } + + { + const createVerifiableClaim = configureCreateVerifiableClaim({ + issuer: rsaIssuer, + type: ClaimType.Work, + context: externalContext, + }) + const signWorkClaim = configureSignVerifiableClaim({ + privateKey: rsaPemPrivateKey, + algorithm: SigningAlgorithm.RsaSignature2018, + }) + + const verifiableWorkClaim = await createVerifiableClaim(TheRavenBookClaim) + const signedWorkClaim = await signWorkClaim(verifiableWorkClaim) + + assert({ + given: 'a Signed Verifiable Claim with an external context', + should: 'return true', + actual: await isValidSignature(signedWorkClaim), + expected: true, + }) + } + + { + const createRsaVerifiableClaimWithExternalContext = configureCreateVerifiableClaim({ + issuer: rsaIssuer, + type: ClaimType.Work, + context: externalContext, + }) + const rsaTheRavenBookWithFullContext = await createRsaVerifiableClaimWithExternalContext(TheRavenBook.claim) + + const badClaim = { + '@context': { ...DefaultClaimContext, ...DefaultWorkClaimContext }, + ...rsaTheRavenBookWithFullContext, + 'sec:proof': { + '@graph': { + '@type': 'sec:RsaSignature2018', + 'dc:created': { + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + '@value': '2018-09-05T20:19:20Z', + }, + 'dc:creator': { + '@id': `data:,${testBadPublicKey}`, + }, + 'sec:jws': + 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..TSHkMOwbWZvIp8Hd-MyebaMgItf4Iyl3dgUSlHBBlnidw' + + 'gzo084pGpKmbOewYFrXfmAVhXnC4UPzaPUjaU9BDw', + }, + }, + } + + assert({ + given: 'a Signed Verifiable claim with an altered public key', + should: 'return false', + actual: await isValidSignature(badClaim), + expected: false, + }) + } +}) + +describe('VerifiableClaimSigner.isValidSignedVerifiableClaim', async (assert: any) => { + { + assert({ + given: 'an object that is not a claim', + should: `return false`, + actual: await isSignedVerifiableClaim({ foo: 'bar' }), + expected: false, + }) + } + + { + const signWorkClaim = configureSignVerifiableClaim({ privateKey: ed25519Base58PrivateKey }) + const signedWorkClaim = await signWorkClaim(Ed25519TheRaven) + + assert({ + given: 'a claim with an invalid id', + should: `return false`, + actual: await isValidSignedVerifiableClaim({ ...signedWorkClaim, id: '111' }), + expected: false, + }) + } + + { + const badClaim = { + ...Ed25519TheRaven, + 'sec:proof': { + '@graph': { + '@type': 'https://w3id.org/security#Ed25519Signature2018', + 'dc:created': { + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + '@value': '2018-09-05T20:19:20Z', + }, + 'dc:creator': { + '@id': `data:,${testBadPublicKey}`, + }, + 'sec:jws': + 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..TSHkMOwbWZvIp8Hd-MyebaMgItf4Iyl3dgUSlHBBlnidw' + + 'gzo084pGpKmbOewYFrXfmAVhXnC4UPzaPUjaU9BDw', + }, + }, + } + + assert({ + given: 'a claim with an invalid publicKey', + should: `return false`, + actual: await isValidSignedVerifiableClaim(badClaim), + expected: false, + }) + } + + { + const signWorkClaim = configureSignVerifiableClaim({ privateKey: ed25519Base58PrivateKey }) + const signedWorkClaim = await signWorkClaim(Ed25519TheRaven) + + assert({ + given: 'a claim with an invalid signature', + should: `return false`, + actual: await isValidSignedVerifiableClaim({ ...signedWorkClaim, 'sec:proof': '111' }), + expected: false, + }) + } + + { + const signWorkClaim = configureSignVerifiableClaim({ privateKey: ed25519Base58PrivateKey }) + const signedWorkClaim = await signWorkClaim(Ed25519TheRaven) + + const result = await Promise.all( + ['', null, undefined].map(value => isValidSignedVerifiableClaim({ ...signedWorkClaim, issuanceDate: value })) + ) + + assert({ + given: 'a claim with an invalid issuanceDate', + should: 'return false', + actual: result, + expected: [false, false, false], + }) + } +}) diff --git a/src/VerifiableClaimSigner.ts b/src/VerifiableClaimSigner.ts new file mode 100644 index 0000000..9e15fbd --- /dev/null +++ b/src/VerifiableClaimSigner.ts @@ -0,0 +1,82 @@ +/* tslint:disable:no-relative-imports */ +import * as cuid from 'cuid' + +import { IllegalArgumentException } from './Exceptions' +import { isSignedVerifiableClaim, SignedVerifiableClaim, SigningAlgorithm, VerifiableClaim } from './Interfaces' +import { generateClaimId } from './VerifiableClaim' + +import JSONLD = require('jsonld') +import JSONLD_SIGS = require('jsonld-signatures') +import { dataDocumentLoader } from './util/DataDocumentLoader' +import { SupportedAlgorithms } from './util/KeyHelper' + +export interface VerifiableClaimSigner { + readonly configureSignVerifiableClaim: ( + config: SignVerifiableClaimConfig + ) => (verifiableClaim: VerifiableClaim) => Promise + readonly isValidSignedVerifiableClaim: (signedVerifiableClaim: SignedVerifiableClaim) => Promise + readonly isValidSignature: (signedVerifiableClaim: SignedVerifiableClaim) => Promise +} + +interface SignVerifiableClaimConfig { + readonly privateKey: string + readonly algorithm?: SigningAlgorithm +} + +export const getVerifiableClaimSigner = (): VerifiableClaimSigner => { + const jsonld = JSONLD() + + jsonld.documentLoader = dataDocumentLoader + const jsig = JSONLD_SIGS() + jsig.use('jsonld', jsonld) + + const isValidSignature = async (claim: SignedVerifiableClaim): Promise => { + const results: any = await jsig.verify(claim, { checkNonce: cuid.isCuid }) + + return results.verified + } + + const signClaimValidateSigningOptions = (signingOptions: any) => { + if (signingOptions.creator === undefined || signingOptions.creator === null || signingOptions.creator === '') + throw new IllegalArgumentException('Cannot sign a claim with an invalid creator in the signing options.') + } + + const createSigningOptions = (algorithm: SigningAlgorithm = SigningAlgorithm.Ed25519Signature2018) => ( + privateKey: string + ) => SupportedAlgorithms[algorithm].getSigningOptions(privateKey) + + const configureSignVerifiableClaim = ({ + privateKey, + algorithm = SigningAlgorithm.Ed25519Signature2018, + }: SignVerifiableClaimConfig) => async (verifiableClaim: VerifiableClaim): Promise => { + const id = await generateClaimId(verifiableClaim) + const signingOptions = { ...createSigningOptions(algorithm)(privateKey), creator: verifiableClaim.issuer } + + signClaimValidateSigningOptions(signingOptions) + + const signedClaim = await jsig.sign( + { + '@context': verifiableClaim['@context'], + id, + type: verifiableClaim.type, + issuer: verifiableClaim.issuer, + issuanceDate: verifiableClaim.issuanceDate, + claim: verifiableClaim.claim, + }, + signingOptions + ) + if (isValidSignature(signedClaim)) return signedClaim + throw new IllegalArgumentException('Claim signature is invalid') + } + + const isValidSignedVerifiableClaim = async (signedVerifiableClaim: SignedVerifiableClaim): Promise => + isSignedVerifiableClaim(signedVerifiableClaim) && + (await isValidSignature(signedVerifiableClaim)) && + (await generateClaimId(signedVerifiableClaim)) === signedVerifiableClaim.id + + return { + configureSignVerifiableClaim, + isValidSignedVerifiableClaim, + isValidSignature, + } +} diff --git a/src/index.ts b/src/index.ts index 1d01099..f2d9487 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,5 @@ -export * from './Claim' export * from './Exceptions' export * from './Interfaces' export * from './Interval' +export * from './VerifiableClaim' +export * from './VerifiableClaimSigner' diff --git a/src/util/DataDocumentLoader.ts b/src/util/DataDocumentLoader.ts new file mode 100644 index 0000000..ab8143a --- /dev/null +++ b/src/util/DataDocumentLoader.ts @@ -0,0 +1,34 @@ +/* tslint:disable:no-relative-imports */ +import * as JSONLD from 'jsonld' +import * as ParseDataUrl from 'parse-data-url' + +import { getSigningAlgorithm } from '../Interfaces' +import { SupportedAlgorithms } from './KeyHelper' + +const nodeDocumentLoader = JSONLD.documentLoaders.node({ usePromise: true }) + +const fromBase64 = (base64String: string): { algorithm: string; publicKey: string } => + JSON.parse(Buffer.from(base64String, 'base64').toString()) + +export const dataDocumentLoader = async (url: string, callback: (error: any, data: any) => any) => { + const parsedData = ParseDataUrl(url) + + if (parsedData) { + const extractedParsedData = parsedData.base64 ? fromBase64(parsedData.data) : parsedData.data + const publicKey = SupportedAlgorithms[getSigningAlgorithm(extractedParsedData.algorithm)].publicKey( + url, + extractedParsedData.publicKey + ) + return callback(null, { + contextUrl: ['https://w3id.org/security/v2'], + document: { + owner: { + id: url, + publicKey: [publicKey], + }, + publicKey, + }, + }) + } + nodeDocumentLoader(url, callback) +} diff --git a/src/util/KeyHelper.test.ts b/src/util/KeyHelper.test.ts new file mode 100644 index 0000000..5b11898 --- /dev/null +++ b/src/util/KeyHelper.test.ts @@ -0,0 +1,123 @@ +/* tslint:disable:no-relative-imports */ +import * as bs58 from 'bs58' +import { pki } from 'node-forge' +import { describe } from 'riteway' + +import { + generateED25519Base58Keys, + getED25519Base58PublicKeyFromBase58PrivateKey, + generateRsaKeyPems, + getRsaPublicPemFromPrivatePem, +} from './KeyHelper' + +type NativeBuffer = Buffer | Uint8Array | number[] + +const ed25519Sign = (privateKey: NativeBuffer): NativeBuffer => + pki.ed25519.sign({ + message: 'test', + encoding: 'utf8', + privateKey, + }) + +const verify = (publicKey: NativeBuffer, signature: NativeBuffer): boolean => + pki.ed25519.verify({ + message: 'test', + encoding: 'utf8', + publicKey, + signature, + }) + +describe('KeyHelper.generateED25519Base58Keys', async (assert: any) => { + { + const keyPair = generateED25519Base58Keys() + const signature = ed25519Sign(bs58.decode(keyPair.privateKey)) + const verified = verify(bs58.decode(keyPair.publicKey), signature) + + assert({ + given: 'no password', + should: 'generate the base58 public key', + actual: keyPair.publicKey.length >= 43 && keyPair.publicKey.length <= 44, + expected: true, + }) + + assert({ + given: 'no password', + should: 'generate the base58 private key', + actual: keyPair.privateKey.length >= 87 && keyPair.privateKey.length <= 88, + expected: true, + }) + + assert({ + given: 'no password', + should: 'generate a working set of keys', + actual: verified, + expected: true, + }) + } + + { + const keyPair = generateED25519Base58Keys('thisismypassword') + const signature = ed25519Sign(bs58.decode(keyPair.privateKey)) + const verified = verify(bs58.decode(keyPair.publicKey), signature) + + assert({ + given: 'a password', + should: 'generate the base58 public key', + actual: keyPair.publicKey.length >= 43 && keyPair.publicKey.length <= 44, + expected: true, + }) + + assert({ + given: 'a password', + should: 'generate the base58 private key', + actual: keyPair.privateKey.length >= 87 && keyPair.privateKey.length <= 88, + expected: true, + }) + + assert({ + given: 'a password', + should: 'generate a working set of keys', + actual: verified, + expected: true, + }) + } +}) + +describe('KeyHelper.getED25519Base58PublicKeyFromBase58PrivateKey', async (assert: any) => { + const keyPair = generateED25519Base58Keys() + const publicKey = getED25519Base58PublicKeyFromBase58PrivateKey(keyPair.privateKey) + const signature = ed25519Sign(bs58.decode(keyPair.privateKey)) + const verified = verify(bs58.decode(publicKey), signature) + + assert({ + given: 'a valid base58 ED25519 private key', + should: 'return a valid base 58 ED25519 public key', + actual: verified, + expected: true, + }) +}) + +describe('KeyHelper.generateRsaKeyPems', async (assert: any) => { + const keyPair = generateRsaKeyPems() + + assert({ + given: 'generateRsaKeyPems', + should: 'return a pair of public/private key PEMS', + actual: + keyPair.publicKey.includes('-----BEGIN PUBLIC KEY-----', 0) && + keyPair.privateKey.includes('-----BEGIN RSA PRIVATE KEY-----', 0), + expected: true, + }) +}) + +describe('KeyHelper.getRsaPublicPemFromPrivatePem', async (assert: any) => { + const keyPair = generateRsaKeyPems() + const derivedPublicKeyPEM = getRsaPublicPemFromPrivatePem(keyPair.privateKey) + + assert({ + given: 'a valid private key PEM', + should: 'return a valid public key PEM', + actual: derivedPublicKeyPEM.includes('-----BEGIN PUBLIC KEY-----', 0), + expected: true, + }) +}) diff --git a/src/util/KeyHelper.ts b/src/util/KeyHelper.ts new file mode 100644 index 0000000..33f15c3 --- /dev/null +++ b/src/util/KeyHelper.ts @@ -0,0 +1,156 @@ +/* tslint:disable:no-relative-imports */ +import { EncodeBuffer } from 'base-x' +import * as bs58 from 'bs58' +import * as cuid from 'cuid' +import { pki, random } from 'node-forge' + +import { + AlgorithmPublicKeyType, + Ed25519SigningOptions, + RsaSigningOptions, + SigningAlgorithm, + SigningOptions, +} from '../Interfaces' + +interface KeyPair { + readonly publicKey: EncodeBuffer + readonly privateKey: EncodeBuffer +} + +interface StringKeyPair { + readonly publicKey: string + readonly privateKey: string +} + +interface PrivateKey { + readonly n: number + readonly e: number +} + +const generateRsaPublicKeyFromPrivateKey = ({ privateKey }: { privateKey: PrivateKey }) => + pki.rsa.setPublicKey(privateKey.n, privateKey.e) + +interface GenerateKeyPairOptions { + bits?: number + readonly seed?: string | ArrayBuffer | Buffer + workers?: number +} + +interface PublicKey { + readonly id: string + readonly type: AlgorithmPublicKeyType + readonly owner: string +} + +interface Ed25519PublicKey extends PublicKey { + publicKeyBase58: string +} + +interface RsaPublicKey extends PublicKey { + publicKeyPem: string +} + +type Algorithms = { [P in SigningAlgorithm]: Algorithm } + +interface Algorithm { + readonly engine: { + readonly generateKeyPair: (options: GenerateKeyPairOptions) => KeyPair + } + readonly getPublicKeyFromPrivateKey: (privateKey: any) => any + readonly getPublicKeyStringFromPrivateKeyString: (privateKey: string) => string + readonly getSigningOptions?: (privateKey: string) => SigningOptions + readonly publicKey: (id: string, publicKey: string) => PublicKey +} + +export const getED25519Base58PublicKeyFromBase58PrivateKey = (privateKey: string): string => + bs58.encode(getED25519PublicKeyFromPrivateKey(bs58.decode(privateKey))) + +export const getRsaPublicPemFromPrivatePem = (privateKey: string): string => + pki.publicKeyToPem(getRsaPublicKeyFromPrivateKey(pki.privateKeyFromPem(privateKey))) + +export const SupportedAlgorithms: Algorithms = { + [SigningAlgorithm.Ed25519Signature2018]: { + engine: pki.ed25519, + getPublicKeyFromPrivateKey: pki.ed25519.publicKeyFromPrivateKey, + getPublicKeyStringFromPrivateKeyString: getED25519Base58PublicKeyFromBase58PrivateKey, + getSigningOptions: (privateKeyBase58: string): Ed25519SigningOptions => ({ + privateKeyBase58, + algorithm: SigningAlgorithm.Ed25519Signature2018, + nonce: cuid(), + }), + publicKey: (id: string, publicKey: string): Ed25519PublicKey => ({ + id, + type: AlgorithmPublicKeyType.Ed25519VerificationKey2018, + owner: id, + publicKeyBase58: publicKey, + }), + }, + [SigningAlgorithm.RsaSignature2018]: { + engine: pki.rsa, + getPublicKeyFromPrivateKey: generateRsaPublicKeyFromPrivateKey, + getPublicKeyStringFromPrivateKeyString: getRsaPublicPemFromPrivatePem, + getSigningOptions: (privateKeyPem: string): RsaSigningOptions => ({ + privateKeyPem, + algorithm: SigningAlgorithm.RsaSignature2018, + nonce: cuid(), + }), + publicKey: (id: string, publicKey): RsaPublicKey => ({ + id, + type: AlgorithmPublicKeyType.RsaVerificationKey2018, + owner: id, + publicKeyPem: publicKey, + }), + }, +} + +export const createIssuerFromPrivateKey = ( + privateKey: string, + algorithm: SigningAlgorithm = SigningAlgorithm.Ed25519Signature2018 +): string => { + const signingInfo = { + algorithm, + publicKey: SupportedAlgorithms[algorithm].getPublicKeyStringFromPrivateKeyString(privateKey), + } + const base64SigningingInfo = Buffer.from(JSON.stringify(signingInfo)).toString('base64') + return `data:;base64,${base64SigningingInfo}` +} + +const generateKeyPair = (algorithm: SigningAlgorithm = SigningAlgorithm.Ed25519Signature2018) => ( + options: GenerateKeyPairOptions = {} +): KeyPair => { + const keyPair = SupportedAlgorithms[algorithm].engine.generateKeyPair(options) + return { + publicKey: keyPair.publicKey, + privateKey: keyPair.privateKey, + } +} + +const generateED25519KeyPair = generateKeyPair(SigningAlgorithm.Ed25519Signature2018) + +const generateRsaKeyPair = generateKeyPair(SigningAlgorithm.RsaSignature2018) + +const getPublicKeyFromPrivateKey = (algorithm: SigningAlgorithm = SigningAlgorithm.Ed25519Signature2018) => ( + privateKey: number[] +): EncodeBuffer => { + return SupportedAlgorithms[algorithm].getPublicKeyFromPrivateKey({ privateKey }) +} + +const getED25519PublicKeyFromPrivateKey = getPublicKeyFromPrivateKey(SigningAlgorithm.Ed25519Signature2018) +const getRsaPublicKeyFromPrivateKey = getPublicKeyFromPrivateKey(SigningAlgorithm.RsaSignature2018) + +export const generateED25519Base58Keys = (entropy: string = ''): StringKeyPair => { + const seed = entropy.length === 0 ? random.getBytesSync(32) : Buffer.from(entropy) + const keyPair = generateED25519KeyPair({ seed }) + return { + publicKey: bs58.encode(keyPair.publicKey).toString(), + privateKey: bs58.encode(keyPair.privateKey).toString(), + } +} + +export const generateRsaKeyPems = () => { + const keyPair = generateRsaKeyPair() + return { + publicKey: pki.publicKeyToPem(keyPair.publicKey), + privateKey: pki.privateKeyToPem(keyPair.privateKey), + } +} diff --git a/tests/unit/index.ts b/tests/unit/index.ts index 2a28a58..a09b33e 100644 --- a/tests/unit/index.ts +++ b/tests/unit/index.ts @@ -1,3 +1,5 @@ /* tslint:disable:no-relative-imports */ -import '../../src/Claim.test' import '../../src/Interfaces.test' +import '../../src/VerifiableClaim.test' +import '../../src/VerifiableClaimSigner.test' +import '../../src/util/KeyHelper.test' diff --git a/tests/unit/shared.ts b/tests/unit/shared.ts new file mode 100644 index 0000000..03e32ac --- /dev/null +++ b/tests/unit/shared.ts @@ -0,0 +1,218 @@ +/* tslint:disable:no-relative-imports */ +import { + BaseVerifiableClaim, + ClaimContext, + ClaimType, + DefaultClaimContext, + DefaultIdentityClaimContext, + DefaultWorkClaimContext, + Identity, + SignedVerifiableClaim, + SigningAlgorithm, + Work, +} from '../../src/Interfaces' +import { getVerifiableClaimSigner } from '../../src/VerifiableClaimSigner' +import { createIssuerFromPrivateKey } from '../../src/util/KeyHelper' + +export const claimSigner = getVerifiableClaimSigner() + +export const getErrorMessage = (err: Error): string => err.message + +export const makeClaim = ( + claim: object, + type: ClaimType = ClaimType.Work, + context: ClaimContext = DefaultClaimContext +): BaseVerifiableClaim => { + const issuer = Ed25519TheRaven.issuer + const issuanceDate = '2017-12-11T22:54:40.261Z' + return { + '@context': context, + issuer, + issuanceDate, + type, + claim, + } +} + +const workContext = { + ...DefaultClaimContext, + ...DefaultWorkClaimContext, +} + +const ed25519Base58PublicKey = 'JAi9YoyDdgBQLenyVzoXWH4C26wKMzHrjertxVrjLWTe' +export const testBadPublicKey: string = 'JAi9YoyDdgBQLenyVzoXWH4C26wKMzHrjertxVrjLWT5' +export const ed25519Base58PrivateKey: string = + 'LWgo1jraJrCB2QT64UVgRemepsNopBF3eJaYMPYVTxpEoFx7sSzCb1QysHeJkH2fnGFgHirgVR35Hz5A1PpXuH6' + +export const badRsaPublicPemKey = + '-----BEGIN PUBLIC KEY-----\r\n' + + 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjKaGI9riNPd87dly3nvh\r\n' + + '2ISSB2i9MVO02nVsfzD+D1kahe/mQMgwwwobs9ArkurFqs4j3fwbSPlRU7F/dPon\r\n' + + 'yrQTMgKGN+jyNV4fGRI16lFbPTKkasRpB9fQ2InIHZkIpUyZHwJYMS1xf8zdtM1i\r\n' + + 'p1Brevhw8w6QtPIhFMgebWJ2LhanjwZhgyhAQtO0FkUgdSegolRcrFf1cEaDVEUc\r\n' + + '2kcJVnLpExN1VLpE4Vnby4ZrNA0r5NgmCPMx15Fp/Vw6v8H1w+kSmgHAWzRlmhUH\r\n' + + 'lKCGtvEnKkpFfoWJcn59HotofR6k4LD/pkrNWtfRQIaup8N15lNyY5Pjxni7RE/q\r\n' + + 'mQIDAQAZ\r\n' + + '-----END PUBLIC KEY-----\r\n' + +export const rsaPemPrivateKey = + '-----BEGIN RSA PRIVATE KEY-----\r\n' + + 'MIIEpAIBAAKCAQEAjKaGI9riNPd87dly3nvh2ISSB2i9MVO02nVsfzD+D1kahe/m\r\n' + + 'QMgwwwobs9ArkurFqs4j3fwbSPlRU7F/dPonyrQTMgKGN+jyNV4fGRI16lFbPTKk\r\n' + + 'asRpB9fQ2InIHZkIpUyZHwJYMS1xf8zdtM1ip1Brevhw8w6QtPIhFMgebWJ2Lhan\r\n' + + 'jwZhgyhAQtO0FkUgdSegolRcrFf1cEaDVEUc2kcJVnLpExN1VLpE4Vnby4ZrNA0r\r\n' + + '5NgmCPMx15Fp/Vw6v8H1w+kSmgHAWzRlmhUHlKCGtvEnKkpFfoWJcn59HotofR6k\r\n' + + '4LD/pkrNWtfRQIaup8N15lNyY5Pjxni7RE/qmQIDAQABAoIBAAmQtaSwmRuzDRr7\r\n' + + '49T/pc9czLWWSO+W2sDUpYlM4qpWi/g55XXYZ4CMKnAjIyN9te200TmikJR46DAB\r\n' + + '7UIeVSBy+K84/rnErNw2R3UkCOijmcnirM3aB66R3dEsJaDlrHHZcLxsI2VMyuA/\r\n' + + 'JYDLSv9H99dIGB5fijXUFa3dIjycEE/IMFycy/Z9DwwPwzNAvuFzYNuSZ7Ba3QMw\r\n' + + 'FjVyI1jzOKny7LSQDM9CQhzv1loXpQxdRkDBDS94boSAZRbwK9636dN4xYkv6Yx9\r\n' + + 'eNGD9d9cWObGqiDntj8EIf2uKd+gW+gUjdEmLBgRM1dUO4AJ49y4zRBWQUjOcJJ0\r\n' + + 'YPoi8dUCgYEA5wgOWxgeu4eU4Q9QM6X6TE9IhIayvldHwuJWSHUjP0F0ZJJOyn/s\r\n' + + 'ntD8mrp70y3VxXjeoC64GQZI7TPPN1dSghcK3R6483l1pOxHHsUFcR/cVI321aYT\r\n' + + 'q7N6j3WInB90Hsw0cCP4uoAy5qFFHLSZIGPQwf++m2kl3ST3LA7xIfcCgYEAm9nm\r\n' + + 'Drr1oVBaP30aNAuqHDiU0zjo04iNntJXTtRQZuzvurKWukKyC68o7966do5MAiXh\r\n' + + '8h8L5MYl0UBIDR9jkKGa+lp8DciG5T8UfStJoo6ZyrUmm8Av/JoG1zEMMLtqfAnq\r\n' + + 'upWMQkNepB/HgBcidinytw1GsFGxjQqQAki7M+8CgYEAvjS/vPfKtZIWXISC/0Kz\r\n' + + 'I4hSp+lN1698AVLevqDR+A4niXV7MPTJFqfwkGLf9ylRSlcM0swj/VZTTBbPjzxx\r\n' + + 'TXEzHIFiu/FPjgyJMSf8JvqYJ3UJtzQYFdCaIuodIowyyfhNY9X5vXI2dfJoOA3n\r\n' + + '0+bZxB6OCt0yszLv3HIgzFkCgYEAgP2t/Y8b8bGxoE6Iu37UApuKAfBeM4YXwNXS\r\n' + + '0TnEegusttdNDUhaWHVW6oFrzugjXLvB8EVl8KlXb4NGnyXVoEVBIeh2OGo5y8+T\r\n' + + 'w61qOpLQEwgvtkUw8l8BPmYn8sWLcrI6hsdz2PwtfqWW1xtOuIIrkvn4AcL7swKF\r\n' + + 'An70Ah0CgYATjmy0e7UxZ0kquqdAFqPDtURcC7jGrjqE9TMfyfctKmcc+rhvfd1T\r\n' + + 'RklPrURV0qb1hEh0X9iEbzQhfHawfyVqHR+ihJM4h4IF8qhEAQXhrpSBaJi7NQxq\r\n' + + '3ZMJNx34XdSGdpvCJtu3XzbqMbjDnGw7Qa65xeK7ygHFzObG0lS4+w==\r\n' + + '-----END RSA PRIVATE KEY-----\r\n' + +const ed25519Issuer = createIssuerFromPrivateKey(ed25519Base58PrivateKey) +const rsaIssuer = createIssuerFromPrivateKey(rsaPemPrivateKey, SigningAlgorithm.RsaSignature2018) + +const rsaSigProof = { + 'sec:proof': { + '@graph': { + '@type': 'sec:RsaSignature2018', + 'dc:created': { + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + '@value': '2018-10-08T20:03:24Z', + }, + 'dc:creator': { + '@id': + 'data:;base64,eyJhbGdvcml0aG0iOiJSc2FTaWduYXR1cmUyMDE4IiwicHVibGljS2V5IjoiLS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS1cclxuTUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFqS2FHSTlyaU5QZDg3ZGx5M252aFxyXG4ySVNTQjJpOU1WTzAyblZzZnpEK0Qxa2FoZS9tUU1nd3d3b2JzOUFya3VyRnFzNGozZndiU1BsUlU3Ri9kUG9uXHJcbnlyUVRNZ0tHTitqeU5WNGZHUkkxNmxGYlBUS2thc1JwQjlmUTJJbklIWmtJcFV5Wkh3SllNUzF4Zjh6ZHRNMWlcclxucDFCcmV2aHc4dzZRdFBJaEZNZ2ViV0oyTGhhbmp3WmhneWhBUXRPMEZrVWdkU2Vnb2xSY3JGZjFjRWFEVkVVY1xyXG4ya2NKVm5McEV4TjFWTHBFNFZuYnk0WnJOQTByNU5nbUNQTXgxNUZwL1Z3NnY4SDF3K2tTbWdIQVd6UmxtaFVIXHJcbmxLQ0d0dkVuS2twRmZvV0pjbjU5SG90b2ZSNms0TEQvcGtyTld0ZlJRSWF1cDhOMTVsTnlZNVBqeG5pN1JFL3FcclxubVFJREFRQUJcclxuLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tXHJcbiJ9', + }, + 'sec:jws': + 'eyJhbGciOiJQUzI1NiIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..OoOrMRs-s_rhrtEbcyHFmGlerQqfq1fzXJC-lXTtYoKhGcc-V5xeHbOFnotdj9UZTjtj66yObmM4MvAv5y3mBOXcCQ5akBigxGk6ECzNV23h8vj9r221PCwEP4dPushwW0RYS2d_80ymsztenyU9Z9iCOXc4GMNBzVbeALhbYLt0BssqR0SHJFtNbh4NE81VDZs97jdR-DzFyZHLF9N_b0ANbzJv95_w9cOax08YMlY_vF-XdGtFfSNgxC_-f_z2lag3oen4VhRbtk-FrSCFehWRX5oRlwUy5rcXWvGLZdVX74SuXuP1oLk5xc1mqCrSe3jEMK_a-__wfelzSV4ZFQ', + 'sec:nonce': 'cjn0q4sbj0003rhc91v7649xh', + }, + }, +} + +export const MyIdentity: Identity = { + '@context': { ...DefaultClaimContext, ...DefaultIdentityClaimContext }, + id: '2b28274e3e88304f7baacec37c9959f8b237955c4e242f882150090b033966f4', + type: ClaimType.Identity, + issuer: ed25519Issuer, + issuanceDate: '2017-11-13T15:00:00.000Z', + claim: { + publicKey: ed25519Base58PublicKey, + }, +} + +export const TheRavenClaim: object = { + name: 'The Raven', + author: 'Edgar Allan Poe', + tags: 'poem', + dateCreated: '', + copyrightHolder: ed25519Issuer, + license: 'https://creativecommons.org/licenses/by/2.0/', + datePublished: '1845-01-29T03:00:00.000Z', + hash: 'de1c818f9be211a78dff90a03b9e297bbb61b3c292f1c1bbc3a5283e9b203cb1', + archiveUrl: 'ipfs:/theRaven', + canonicalUrl: 'https://amazon.com/Ed25519TheRaven', +} + +export const BaseEd25519RavenClaim: BaseVerifiableClaim = { + '@context': workContext, + type: ClaimType.Work, + issuer: ed25519Issuer, + issuanceDate: '2017-11-13T15:00:00.000Z', + claim: TheRavenClaim, +} + +export const BaseRsaRavenClaim: BaseVerifiableClaim = { + '@context': workContext, + type: ClaimType.Work, + issuer: rsaIssuer, + issuanceDate: '2017-11-13T15:00:00.000Z', + claim: TheRavenClaim, +} + +export const Ed25519TheRaven: Work = { + ...BaseEd25519RavenClaim, + id: '04369f7eca0c8b8bdd4f13392e64caf66724bc8b3f6e4a043c34be466619f976', +} + +export const RsaTheRaven: Work = { + ...BaseRsaRavenClaim, + id: 'baa06c1f301d3425bf46de18e044dce609620e7c34d7a68e9fb06093c038c1e9', +} + +export const Ed25519SignedRaven: SignedVerifiableClaim = { + '@context': workContext, + id: 'a2f3975a47ed18cd2cdaa2e31c04ca4a3b1256ad0711d98d5fdae1ae31440cf5', + type: ClaimType.Work, + issuer: + 'data:;base64,eyJhbGdvcml0aG0iOiJFZDI1NTE5U2lnbmF0dXJlMjAxOCIsInB1YmxpY0tleSI6IkpBaTlZb3lEZGdCUUxlbnlWem9YV0g0QzI2d0tNekhyamVydHhWcmpMV1RlIn0=', + issuanceDate: '2018-10-09T19:06:06.508Z', + claim: { + name: 'The Raven', + author: 'Edgar Allan Poe', + tags: 'poem', + dateCreated: '', + copyrightHolder: + 'data:;base64,eyJhbGdvcml0aG0iOiJFZDI1NTE5U2lnbmF0dXJlMjAxOCIsInB1YmxpY0tleSI6IkpBaTlZb3lEZGdCUUxlbnlWem9YV0g0QzI2d0tNekhyamVydHhWcmpMV1RlIn0=', + license: 'https://creativecommons.org/licenses/by/2.0/', + datePublished: '1845-01-29T03:00:00.000Z', + hash: 'de1c818f9be211a78dff90a03b9e297bbb61b3c292f1c1bbc3a5283e9b203cb1', + archiveUrl: 'ipfs:/theRaven', + canonicalUrl: 'https://amazon.com/Ed25519TheRaven', + }, + 'sec:proof': { + '@graph': { + '@type': 'sec:Ed25519Signature2018', + 'dc:created': { + '@type': 'http://www.w3.org/2001/XMLSchema#dateTime', + '@value': '2018-10-09T19:06:06Z', + }, + 'dc:creator': { + '@id': + 'data:;base64,eyJhbGdvcml0aG0iOiJFZDI1NTE5U2lnbmF0dXJlMjAxOCIsInB1YmxpY0tleSI6IkpBaTlZb3lEZGdCUUxlbnlWem9YV0g0QzI2d0tNekhyamVydHhWcmpMV1RlIn0=', + }, + 'sec:jws': + 'eyJhbGciOiJFZERTQSIsImI2NCI6ZmFsc2UsImNyaXQiOlsiYjY0Il19..SjLCvnnaKXATF1LUyAtJDFvu8SmqrVSqpW2j4kUEw1fgsegDy2APfeeNxSa4bR4egeSs_1UXI380f7BW5JURAQ', + 'sec:nonce': 'cjn23ixwu0000stc9dufk76i1', + }, + }, +} + +export const RsaSignedRaven: SignedVerifiableClaim = { + ...RsaTheRaven, + ...rsaSigProof, +} + +export const externalContext: any = { + claim: 'http://schema.org/Book', + edition: 'http://schema.org/bookEdition', + isbn: 'http://schema.org/isbn', +} + +export const TheRavenBookClaim = { + ...TheRavenClaim, + edition: '1', + isbn: '9781458318404', + contributors: [ed25519Issuer], +} + +export const TheRavenBook: Work = { + ...Ed25519TheRaven, + claim: TheRavenBookClaim, +} diff --git a/typings/json-signatures.d.ts b/typings/json-signatures.d.ts new file mode 100644 index 0000000..fba4bdd --- /dev/null +++ b/typings/json-signatures.d.ts @@ -0,0 +1,19 @@ +declare module 'jsonld-signatures' { + interface VerifiedResults { + readonly keyResults: ReadonlyArray + readonly verified: boolean + } + + interface JsonldSignatureFactory { + (): JsonldSignatureFactory + suites: object + SECURITY_CONTEXT_URL: string + sign: (doc: any, signingOptions: any) => Promise + use: (libraryName: string, library: any) => void + verify: (doc: any, verificationOptions: any) => Promise + } + + const jsonLdSignatureFactory: JsonldSignatureFactory + + export = jsonLdSignatureFactory +} diff --git a/typings/jsonld.d.ts b/typings/jsonld.d.ts index bf0a3ff..8787319 100644 --- a/typings/jsonld.d.ts +++ b/typings/jsonld.d.ts @@ -1,3 +1,13 @@ declare module 'jsonld' { - export function canonize(doc: any): Promise + interface Jsonld { + (): Jsonld + canonize: (doc: any) => Promise + documentLoader: (url: string, callback: (error: any, data: any) => any) => any + documentLoaders: { + node: (options: object) => (url: string, callback: (err: Error, payload: object) => void) => any + } + } + const jsonld: Jsonld + + export = jsonld } diff --git a/typings/node-forge.d.ts b/typings/node-forge.d.ts new file mode 100644 index 0000000..7b8b209 --- /dev/null +++ b/typings/node-forge.d.ts @@ -0,0 +1,51 @@ +declare module 'node-forge' { + type NativeBuffer = Buffer | Uint8Array | number[] + + interface KeyPair { + readonly publicKey: any + readonly privateKey: any + } + + namespace pki { + type PEM = string + type Key = any + + function privateKeyFromPem(pem: PEM): Key + function privateKeyToPem(key: Key, maxline?: number): PEM + function publicKeyToPem(key: Key, maxline?: number): PEM + + namespace ed25519 { + function sign(options: { message: any; encoding: string; privateKey: NativeBuffer }): NativeBuffer + function verify(options: { + message: any + encoding: string + publicKey: NativeBuffer + signature: NativeBuffer + }): boolean + function publicKeyFromPrivateKey(options: { privateKey: NativeBuffer }): NativeBuffer + function generateKeyPair(options: { seed: string | ArrayBuffer }): KeyPair + } + + namespace rsa { + interface GenerateKeyPairOptions { + bits?: number + e?: number + workerScript?: string + workers?: number + workLoad?: number + prng?: any + algorithm?: string + } + + function generateKeyPair(bits?: number, e?: number, callback?: (err: Error, keypair: KeyPair) => void): KeyPair + function generateKeyPair( + options?: GenerateKeyPairOptions, + callback?: (err: Error, keypair: KeyPair) => void + ): KeyPair + function setPublicKey(n: number, e: number): Key + } + } + namespace random { + function getBytesSync(count: number): string + } +} diff --git a/typings/parse-data-url.d.ts b/typings/parse-data-url.d.ts new file mode 100644 index 0000000..834b7b5 --- /dev/null +++ b/typings/parse-data-url.d.ts @@ -0,0 +1 @@ +declare module 'parse-data-url' diff --git a/typings/riteway.d.ts b/typings/riteway.d.ts deleted file mode 100644 index ec694be..0000000 --- a/typings/riteway.d.ts +++ /dev/null @@ -1 +0,0 @@ -declare module 'riteway'