diff --git a/package-lock.json b/package-lock.json index 7f7829d..cca5fca 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2460,24 +2460,6 @@ "@babel/types": "^7.3.0" } }, - "@types/bn.js": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@types/bn.js/-/bn.js-5.1.0.tgz", - "integrity": "sha512-QSSVYj7pYFN49kW77o2s9xTCwZ8F2xLbjLLSEVh8D2F4JUhZtPAGOFLTD+ffqksBx/u4cE/KImFjyhqCjn/LIA==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/elliptic": { - "version": "6.4.12", - "resolved": "https://registry.npmjs.org/@types/elliptic/-/elliptic-6.4.12.tgz", - "integrity": "sha512-gP1KsqoouLJGH6IJa28x7PXb3cRqh83X8HCLezd2dF+XcAIMKYv53KV+9Zn6QA561E120uOqZBQ+Jy/cl+fviw==", - "dev": true, - "requires": { - "@types/bn.js": "*" - } - }, "@types/estree": { "version": "0.0.39", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz", @@ -3195,11 +3177,6 @@ } } }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -3313,15 +3290,6 @@ "node-int64": "^0.4.0" } }, - "buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", @@ -6819,11 +6787,6 @@ "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", "dev": true }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" - }, "ignore": { "version": "5.1.8", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", @@ -16012,9 +15975,9 @@ "optional": true }, "uint8arrays": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-2.1.4.tgz", - "integrity": "sha512-Q/Ys2JhFWpZkw8Hi2Zz7NFpVDH8avK9k2NjYKdOHoOAn5dTtOSNT9xMtaQz5D4kWVPOGKte8CAroEIdTzvF9AA==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uint8arrays/-/uint8arrays-2.1.5.tgz", + "integrity": "sha512-CSR7AO+4AHUeSOnZ/NBNCElDeWfRh9bXtOck27083kc7SznmmHIhNEkEOCQOn0wvrIMjS3IH0TNLR16vuc46mA==", "requires": { "multibase": "^4.0.1" } diff --git a/package.json b/package.json index a875b62..7b4df65 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "format": "prettier --write \"src/**/*.[jt]s\"", "lint": "eslint \"src/**/*.[jt]s\"", "build:js": "microbundle", - "build": "npm run build:js && npm run test", + "build": "npm run lint && npm run build:js && npm run test", "prepare": "npm run build", "release": "semantic-release --debug" }, @@ -55,13 +55,13 @@ "@ethersproject/contracts": "5.1.0", "@semantic-release/changelog": "5.0.1", "@semantic-release/git": "9.0.0", - "@types/elliptic": "6.4.12", "@types/jest": "26.0.22", "@typescript-eslint/eslint-plugin": "4.22.0", "@typescript-eslint/parser": "4.22.0", "codecov": "3.8.1", "eslint": "7.24.0", "eslint-config-prettier": "8.2.0", + "ethr-did-registry": "0.0.3", "ganache-cli": "6.12.2", "jest": "26.6.3", "microbundle": "0.13.0", @@ -73,12 +73,16 @@ "dependencies": { "@ethersproject/abstract-signer": "^5.1.0", "@ethersproject/address": "^5.1.0", + "@ethersproject/base64": "^5.1.0", + "@ethersproject/basex": "^5.1.0", + "@ethersproject/bytes": "^5.1.0", + "@ethersproject/providers": "^5.1.0", + "@ethersproject/signing-key": "^5.1.0", + "@ethersproject/strings": "^5.1.0", "@ethersproject/transactions": "^5.1.0", "@ethersproject/wallet": "^5.1.0", - "buffer": "^6.0.3", "did-jwt": "^5.1.2", "did-resolver": "^3.1.0", - "ethr-did-registry": "0.0.3", "ethr-did-resolver": "^4.1.0" } } diff --git a/src/__tests__/index-test.ts b/src/__tests__/index-test.ts index 15a69b2..1814e2f 100644 --- a/src/__tests__/index-test.ts +++ b/src/__tests__/index-test.ts @@ -97,7 +97,7 @@ describe('EthrDID', () => { describe('add signing delegate', () => { beforeAll(async () => { const txHash = await ethrDid.addDelegate(delegate1, { - expiresIn: 100, + expiresIn: 86400, }) await provider.waitForTransaction(txHash) }) @@ -280,7 +280,7 @@ describe('EthrDID', () => { await ethrDid.setAttribute( 'did/pub/Secp256k1/veriKey', '0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71', - 10 + 86400 ) }) @@ -321,7 +321,7 @@ describe('EthrDID', () => { await ethrDid.setAttribute( 'did/pub/Ed25519/veriKey/base64', 'Arl8MN52fwhM4wgBaO4pMFO6M7I11xFqMmPSnxRQk2tx', - 10 + 86400 ) }) @@ -368,7 +368,7 @@ describe('EthrDID', () => { await ethrDid.setAttribute( 'did/pub/Ed25519/veriKey/base64', Buffer.from('f2b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b72', 'hex'), - 10 + 86400 ) }) @@ -420,7 +420,7 @@ describe('EthrDID', () => { describe('service endpoints', () => { describe('HubService', () => { beforeAll(async () => { - await ethrDid.setAttribute('did/svc/HubService', 'https://hubs.uport.me', 100) + await ethrDid.setAttribute('did/svc/HubService', 'https://hubs.uport.me', 86400) }) it('resolves document', async () => { return expect((await resolver.resolve(did)).didDocument).toEqual({ @@ -604,13 +604,11 @@ describe('EthrDID', () => { describe('plain vanilla keypair account', () => { it('should sign valid jwt', async () => { - const kp: KeyPair = EthrDID.createKeyPair() + const kp: KeyPair = EthrDID.createKeyPair('dev') plainDid = new EthrDID({ - identifier: kp.publicKey, - privateKey: kp.privateKey, + ...kp, provider, registry: registry, - chainNameOrId: 'dev', }) const jwt = await plainDid.signJWT({ hello: 'world' }) const { payload } = await verifyJWT(jwt, { resolver }) @@ -620,16 +618,11 @@ describe('EthrDID', () => { }) describe('verifyJWT', () => { - const kp: KeyPair = EthrDID.createKeyPair() - const ethrDid = new EthrDID({ - identifier: kp.publicKey, - privateKey: kp.privateKey, - chainNameOrId: 'dev', - }) - const did = ethrDid.did + const ethrDidAsIssuer = new EthrDID(EthrDID.createKeyPair('dev')) + const did = ethrDidAsIssuer.did it('verifies the signature of the JWT', async () => { - return ethrDid + return ethrDidAsIssuer .signJWT({ hello: 'friend' }) .then((jwt) => plainDid.verifyJWT(jwt, resolver)) .then(({ issuer }) => expect(issuer).toEqual(did)) @@ -637,19 +630,19 @@ describe('EthrDID', () => { describe('uses did for verifying aud claim', () => { it('verifies the signature of the JWT', () => { - return ethrDid + return ethrDidAsIssuer .signJWT({ hello: 'friend', aud: plainDid.did }) .then((jwt) => plainDid.verifyJWT(jwt, resolver)) .then(({ issuer }) => expect(issuer).toEqual(did)) }) it('fails if wrong did', () => { - return ethrDid + return ethrDidAsIssuer .signJWT({ hello: 'friend', aud: plainDid.did }) .then((jwt) => plainDid.verifyJWT(jwt, resolver)) .catch((error) => expect(error.message).toEqual( - `JWT audience does not match your DID: aud: ${ethrDid.did} !== yours: ${plainDid.did}` + `JWT audience does not match your DID: aud: ${ethrDidAsIssuer.did} !== yours: ${plainDid.did}` ) ) }) @@ -679,9 +672,67 @@ describe('EthrDID', () => { it('should create add the large RSA key in the hex format', async () => { const didDocument = (await resolver.resolve(did)).didDocument const pk = didDocument?.verificationMethod?.find((pk) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any return typeof (pk).publicKeyPem !== 'undefined' }) + // eslint-disable-next-line @typescript-eslint/no-explicit-any expect((pk).publicKeyPem).toEqual(rsa4096PublicKey) }) }) + + describe('base58 key', () => { + const publicKeyBase58 = 'SYnSwQmBmVwrHoGo6mnqFCX28sr3UzAZw9yyiBTLaf2foDfxDTgNdpn3MPD4gUGi4cgunK8cnGbPS5yjVh5uAXGr' + + it('supports base58 keys as hexstring', async () => { + const publicKeyHex = + '04fdd57adec3d438ea237fe46b33ee1e016eda6b585c3e27ea66686c2ea535847946393f8145252eea68afe67e287b3ed9b31685ba6c3b00060a73b9b1242d68f7' + const did = `did:ethr:dev:${delegate1}` + const didController = new EthrDID({ + identifier: did, + provider, + registry, + }) + await didController.setAttribute('did/pub/Secp256k1/veriKey/base58', `0x${publicKeyHex}`, 86400) + const doc = (await resolver.resolve(did)).didDocument + expect(doc?.verificationMethod).toEqual([ + { + blockchainAccountId: `${delegate1}@eip155:1337`, + controller: did, + id: `${did}#controller`, + type: 'EcdsaSecp256k1RecoveryMethod2020', + }, + { + controller: did, + id: `${did}#delegate-1`, + publicKeyBase58, + type: 'EcdsaSecp256k1VerificationKey2019', + }, + ]) + }) + + it('supports base58 keys as string', async () => { + const did = `did:ethr:dev:${delegate2}` + const didController = new EthrDID({ + identifier: did, + provider, + registry, + }) + await didController.setAttribute('did/pub/Secp256k1/veriKey/base58', publicKeyBase58, 86400) + const doc = (await resolver.resolve(did)).didDocument + expect(doc?.verificationMethod).toEqual([ + { + blockchainAccountId: `${delegate2}@eip155:1337`, + controller: did, + id: `${did}#controller`, + type: 'EcdsaSecp256k1RecoveryMethod2020', + }, + { + controller: did, + id: `${did}#delegate-1`, + publicKeyBase58, + type: 'EcdsaSecp256k1VerificationKey2019', + }, + ]) + }) + }) }) diff --git a/src/__tests__/multinet-test.ts b/src/__tests__/multinet-test.ts index ca89ae3..39dbe0e 100644 --- a/src/__tests__/multinet-test.ts +++ b/src/__tests__/multinet-test.ts @@ -1,7 +1,7 @@ import { EthrDID } from '..' describe('other networks', () => { - it('rsk - github #50', () => { + it('supports rsk - github #50', () => { const ethrDid = new EthrDID({ identifier: '0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71', chainNameOrId: 'rsk', @@ -9,4 +9,62 @@ describe('other networks', () => { expect(ethrDid.did).toEqual('did:ethr:rsk:0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71') expect(ethrDid.address).toEqual('0xC662e6c5F91B9FcD22D7FcafC80Cf8b640aed247') }) + + it('supports rsk:testnet - github #50', () => { + const ethrDid = new EthrDID({ + identifier: '0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71', + chainNameOrId: 'rsk:testnet', + }) + expect(ethrDid.did).toEqual( + 'did:ethr:rsk:testnet:0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71' + ) + expect(ethrDid.address).toEqual('0xC662e6c5F91B9FcD22D7FcafC80Cf8b640aed247') + }) + + it('supports rsk as did string', () => { + const ethrDid = new EthrDID({ + identifier: 'did:ethr:rsk:0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71', + }) + expect(ethrDid.did).toEqual('did:ethr:rsk:0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71') + expect(ethrDid.address).toEqual('0xC662e6c5F91B9FcD22D7FcafC80Cf8b640aed247') + }) + + it('supports rsk:testnet as did string', () => { + const ethrDid = new EthrDID({ + identifier: 'did:ethr:rsk:testnet:0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71', + }) + expect(ethrDid.did).toEqual( + 'did:ethr:rsk:testnet:0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71' + ) + expect(ethrDid.address).toEqual('0xC662e6c5F91B9FcD22D7FcafC80Cf8b640aed247') + }) + + it('supports rsk:testnet:custom:params as did string', () => { + const ethrDid = new EthrDID({ + identifier: + 'did:ethr:rsk:testnet:custom:params:0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71', + }) + expect(ethrDid.did).toEqual( + 'did:ethr:rsk:testnet:custom:params:0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71' + ) + expect(ethrDid.address).toEqual('0xC662e6c5F91B9FcD22D7FcafC80Cf8b640aed247') + }) + + it('supports hexstring chainId', () => { + const ethrDid = new EthrDID({ + identifier: '0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71', + chainNameOrId: '0x3', + }) + expect(ethrDid.did).toEqual('did:ethr:0x3:0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71') + expect(ethrDid.address).toEqual('0xC662e6c5F91B9FcD22D7FcafC80Cf8b640aed247') + }) + + it('supports numbered chainId', () => { + const ethrDid = new EthrDID({ + identifier: '0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71', + chainNameOrId: 42, + }) + expect(ethrDid.did).toEqual('did:ethr:0x2a:0x02b97c30de767f084ce3080168ee293053ba33b235d7116a3263d29f1450936b71') + expect(ethrDid.address).toEqual('0xC662e6c5F91B9FcD22D7FcafC80Cf8b640aed247') + }) }) diff --git a/src/index.ts b/src/index.ts index 6b04020..fa6130c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,13 +1,17 @@ -import { createJWT, verifyJWT, Signer as JWTSigner, ES256KSigner } from 'did-jwt' +import { createJWT, verifyJWT, Signer as JWTSigner, ES256KSigner, JWTVerified } from 'did-jwt' import { Signer as TxSigner } from '@ethersproject/abstract-signer' import { CallOverrides } from '@ethersproject/contracts' import { computeAddress } from '@ethersproject/transactions' +import { computePublicKey } from '@ethersproject/signing-key' import { getAddress } from '@ethersproject/address' +import { Provider } from '@ethersproject/providers' import { Wallet } from '@ethersproject/wallet' +import * as base64 from '@ethersproject/base64' +import { hexlify, hexValue, isBytes } from '@ethersproject/bytes' +import { Base58 } from '@ethersproject/basex' +import { toUtf8Bytes } from '@ethersproject/strings' import { REGISTRY, EthrDidController } from 'ethr-did-resolver' import { Resolvable } from 'did-resolver' -import { ec as EC } from 'elliptic' -const secp256k1: any = new EC('secp256k1') export enum DelegateTypes { veriKey = 'veriKey', @@ -17,7 +21,7 @@ export enum DelegateTypes { interface IConfig { identifier: string - chainNameOrId?: string + chainNameOrId?: string | number registry?: string @@ -26,7 +30,8 @@ interface IConfig { privateKey?: string rpcUrl?: string - provider?: any + provider?: Provider + // eslint-disable-next-line @typescript-eslint/no-explicit-any web3?: any } @@ -34,6 +39,12 @@ export type KeyPair = { address: string privateKey: string publicKey: string + identifier: string +} + +type DelegateOptions = { + delegateType?: DelegateTypes + expiresIn?: number } export class EthrDID { @@ -45,25 +56,24 @@ export class EthrDID { constructor(conf: IConfig) { const { address, publicKey, network } = interpretIdentifier(conf.identifier) - + const chainNameOrId = typeof conf.chainNameOrId === 'number' ? hexValue(conf.chainNameOrId) : conf.chainNameOrId if (conf.provider || conf.rpcUrl || conf.web3) { let txSigner = conf.txSigner if (conf.privateKey && typeof txSigner === 'undefined') { txSigner = new Wallet(conf.privateKey) } - this.controller = new EthrDidController( conf.identifier, undefined, txSigner, - conf.chainNameOrId, + chainNameOrId, conf.provider || conf.web3?.currentProvider, conf.rpcUrl, conf.registry || REGISTRY ) this.did = this.controller.did } else { - const net = network || conf.chainNameOrId + const net = network || chainNameOrId let networkString = net ? `${net}:` : '' if (networkString in ['mainnet:', '0x1:']) { networkString = '' @@ -71,9 +81,7 @@ export class EthrDID { this.did = typeof publicKey === 'string' ? `did:ethr:${networkString}${publicKey}` : `did:ethr:${networkString}${address}` } - this.address = address - if (conf.signer) { this.signer = conf.signer } else if (conf.privateKey) { @@ -81,12 +89,14 @@ export class EthrDID { } } - static createKeyPair(): KeyPair { - const kp = secp256k1.genKeyPair() - const privateKey = kp.getPrivate('hex') - const publicKey = '0x' + kp.getPublic(true, 'hex') - const address = computeAddress(publicKey) - return { address, privateKey, publicKey } + static createKeyPair(chainNameOrId?: string | number): KeyPair { + const wallet = Wallet.createRandom() + const privateKey = wallet.privateKey + const address = computeAddress(privateKey) + const publicKey = computePublicKey(privateKey, true) + const net = typeof chainNameOrId === 'number' ? hexValue(chainNameOrId) : chainNameOrId + const identifier = net ? `did:ethr:${net}:${publicKey}` : publicKey + return { address, privateKey, publicKey, identifier } } async lookupOwner(cache = true): Promise { @@ -113,14 +123,19 @@ export class EthrDID { async addDelegate( delegate: string, - { delegateType = DelegateTypes.veriKey, expiresIn = 86400 }, + delegateOptions?: DelegateOptions, txOptions: CallOverrides = {} ): Promise { if (typeof this.controller === 'undefined') { throw new Error('a web3 provider configuration is needed for network operations') } const owner = await this.lookupOwner() - const receipt = await this.controller.addDelegate(delegateType, delegate, expiresIn, { ...txOptions, from: owner }) + const receipt = await this.controller.addDelegate( + delegateOptions?.delegateType || DelegateTypes.veriKey, + delegate, + delegateOptions?.expiresIn || 86400, + { ...txOptions, from: owner } + ) return receipt.transactionHash } @@ -180,10 +195,7 @@ export class EthrDID { async createSigningDelegate( delegateType = DelegateTypes.veriKey, expiresIn = 86400 - ): Promise<{ - kp: KeyPair - txHash: string - }> { + ): Promise<{ kp: KeyPair; txHash: string }> { const kp = EthrDID.createKeyPair() this.signer = ES256KSigner(kp.privateKey, true) const txHash = await this.addDelegate(kp.address, { @@ -193,6 +205,7 @@ export class EthrDID { return { kp, txHash } } + // eslint-disable-next-line async signJWT(payload: any, expiresIn?: number): Promise { if (typeof this.signer !== 'function') { throw new Error('No signer configured') @@ -202,11 +215,12 @@ export class EthrDID { alg: 'ES256K-R', issuer: this.did, } + // eslint-disable-next-line @typescript-eslint/no-explicit-any if (expiresIn) (options)['expiresIn'] = expiresIn return createJWT(payload, options) } - async verifyJWT(jwt: string, resolver: Resolvable, audience = this.did): Promise { + async verifyJWT(jwt: string, resolver: Resolvable, audience = this.did): Promise { return verifyJWT(jwt, { resolver, audience }) } } @@ -217,8 +231,8 @@ function interpretIdentifier(identifier: string): { address: string; publicKey?: if (input.startsWith('did:ethr')) { const components = input.split(':') input = components[components.length - 1] - if (components.length === 4) { - network = components[2] + if (components.length >= 4) { + network = components.splice(2, components.length - 3).join(':') } } if (input.length > 42) { @@ -229,19 +243,22 @@ function interpretIdentifier(identifier: string): { address: string; publicKey?: } function attributeToHex(key: string, value: string | Uint8Array): string { - if (Buffer.isBuffer(value)) { - return `0x${value.toString('hex')}` + if (value instanceof Uint8Array || isBytes(value)) { + return hexlify(value) } - const match = key.match(/^did\/(pub|auth|svc)\/(\w+)(\/(\w+))?(\/(\w+))?$/) - if (match) { - const encoding = match[6] - // TODO add support for base58 + const matchKeyWithEncoding = key.match(/^did\/(pub|auth|svc)\/(\w+)(\/(\w+))?(\/(\w+))?$/) + const encoding = matchKeyWithEncoding?.[6] + const matchHexString = (value).match(/^0x[0-9a-fA-F]*$/) + if (encoding && !matchHexString) { if (encoding === 'base64') { - return `0x${Buffer.from(value, 'base64').toString('hex')}` + return hexlify(base64.decode(value)) } - } - if ((value).match(/^0x[0-9a-fA-F]*$/)) { + if (encoding === 'base58') { + return hexlify(Base58.decode(value)) + } + } else if (matchHexString) { return value } - return `0x${Buffer.from(value).toString('hex')}` + + return hexlify(toUtf8Bytes(value)) }