diff --git a/src/Digest.ts b/src/Digest.ts index 08518e39..7bf05b34 100644 --- a/src/Digest.ts +++ b/src/Digest.ts @@ -3,7 +3,7 @@ import * as u8a from 'uint8arrays' import { keccak_256 } from 'js-sha3' // eslint-disable-line export function sha256(payload: string | Uint8Array): Uint8Array { - const data = (typeof payload === 'string') ? u8a.fromString(payload) : payload + const data = typeof payload === 'string' ? u8a.fromString(payload) : payload return hash(data) } @@ -37,5 +37,5 @@ export function concatKDF(secret: Uint8Array, keyLen: number, alg: string): Uint ]) // since our key lenght is 256 we only have to do one round const roundNumber = 1 - return hash(u8a.concat([ writeUint32BE(roundNumber), secret, value ])) + return hash(u8a.concat([writeUint32BE(roundNumber), secret, value])) } diff --git a/src/JWE.ts b/src/JWE.ts index 0b8f9dac..41d0fc3a 100644 --- a/src/JWE.ts +++ b/src/JWE.ts @@ -41,7 +41,12 @@ export interface Encrypter { export interface Decrypter { alg: string enc: string - decrypt: (sealed: Uint8Array, iv: Uint8Array, aad?: Uint8Array, recipient?: Record) => Promise + decrypt: ( + sealed: Uint8Array, + iv: Uint8Array, + aad?: Uint8Array, + recipient?: Record + ) => Promise } function validateJWE(jwe: JWE) { @@ -49,7 +54,7 @@ function validateJWE(jwe: JWE) { throw new Error('Invalid JWE') } if (jwe.recipients) { - jwe.recipients.map(rec => { + jwe.recipients.map((rec) => { if (!(rec.header && rec.encrypted_key)) { throw new Error('Invalid JWE') } @@ -57,16 +62,7 @@ function validateJWE(jwe: JWE) { } } -function encodeJWE( - { - ciphertext, - tag, - iv, - protectedHeader, - recipient - }: EncryptionResult, - aad?: Uint8Array -): JWE { +function encodeJWE({ ciphertext, tag, iv, protectedHeader, recipient }: EncryptionResult, aad?: Uint8Array): JWE { const jwe: JWE = { protected: protectedHeader, iv: bytesToBase64url(iv), @@ -78,7 +74,12 @@ function encodeJWE( return jwe } -export async function createJWE(cleartext: Uint8Array, encrypters: Encrypter[], protectedHeader = {}, aad?: Uint8Array): Promise { +export async function createJWE( + cleartext: Uint8Array, + encrypters: Encrypter[], + protectedHeader = {}, + aad?: Uint8Array +): Promise { if (encrypters[0].alg === 'dir') { if (encrypters.length > 1) throw new Error('Can only do "dir" encryption to one key.') const encryptionResult = await encrypters[0].encrypt(cleartext, protectedHeader, aad) diff --git a/src/JWT.ts b/src/JWT.ts index a83487ba..f54ae57c 100644 --- a/src/JWT.ts +++ b/src/JWT.ts @@ -1,7 +1,7 @@ import VerifierAlgorithm from './VerifierAlgorithm' import SignerAlgorithm from './SignerAlgorithm' import { encodeBase64url, decodeBase64url, EcdsaSignature } from './util' -import type { Resolver, DIDDocument, VerificationMethod, DIDResolutionResult } from 'did-resolver' +import type { Resolver, VerificationMethod, DIDResolutionResult } from 'did-resolver' export type Signer = (data: string | Uint8Array) => Promise export type SignerAlgorithm = (payload: string, signer: Signer) => Promise @@ -75,16 +75,42 @@ export interface PublicKeyTypes { } export const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = { ES256K: [ + 'EcdsaSecp256k1VerificationKey2019', + /** + * Equivalent to EcdsaSecp256k1VerificationKey2019 when key is an ethereumAddress + */ + 'EcdsaSecp256k1RecoveryMethod2020', + /** + * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress + */ 'Secp256k1VerificationKey2018', + /** + * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress + */ 'Secp256k1SignatureVerificationKey2018', - 'EcdsaPublicKeySecp256k1', - 'EcdsaSecp256k1VerificationKey2019' + /** + * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress + */ + 'EcdsaPublicKeySecp256k1' ], 'ES256K-R': [ + 'EcdsaSecp256k1VerificationKey2019', + /** + * Equivalent to EcdsaSecp256k1VerificationKey2019 when key is an ethereumAddress + */ + 'EcdsaSecp256k1RecoveryMethod2020', + /** + * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress + */ 'Secp256k1VerificationKey2018', + /** + * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress + */ 'Secp256k1SignatureVerificationKey2018', - 'EcdsaPublicKeySecp256k1', - 'EcdsaSecp256k1VerificationKey2019' + /** + * @deprecated, supported for backward compatibility. Equivalent to EcdsaSecp256k1VerificationKey2019 when key is not an ethereumAddress + */ + 'EcdsaPublicKeySecp256k1' ], Ed25519: ['ED25519SignatureVerification', 'Ed25519VerificationKey2018'], EdDSA: ['ED25519SignatureVerification', 'Ed25519VerificationKey2018'] @@ -202,7 +228,10 @@ export async function createJWT( return createJWS(fullPayload, signer, header) } -function verifyJWSDecoded({ header, data, signature }: JWSDecoded, pubkeys: VerificationMethod | VerificationMethod[]): VerificationMethod { +function verifyJWSDecoded( + { header, data, signature }: JWSDecoded, + pubkeys: VerificationMethod | VerificationMethod[] +): VerificationMethod { if (!Array.isArray(pubkeys)) pubkeys = [pubkeys] const signer: VerificationMethod = VerifierAlgorithm(header.alg)(data, signature, pubkeys) return signer diff --git a/src/SignerAlgorithm.ts b/src/SignerAlgorithm.ts index d2a42936..156311bc 100644 --- a/src/SignerAlgorithm.ts +++ b/src/SignerAlgorithm.ts @@ -11,7 +11,9 @@ export function ES256KSignerAlg(recoverable?: boolean): SignerAlgorithm { if (instanceOfEcdsaSignature(signature)) { return toJose(signature, recoverable) } else { - if (recoverable && typeof fromJose(signature).recoveryParam === 'undefined') throw new Error(`ES256K-R not supported when signer doesn't provide a recovery param`) + if (recoverable && typeof fromJose(signature).recoveryParam === 'undefined') { + throw new Error(`ES256K-R not supported when signer doesn't provide a recovery param`) + } return signature } } diff --git a/src/VerifierAlgorithm.ts b/src/VerifierAlgorithm.ts index f21158f6..81940307 100644 --- a/src/VerifierAlgorithm.ts +++ b/src/VerifierAlgorithm.ts @@ -36,7 +36,11 @@ function extractPublicKeyBytes(pk: VerificationMethod): Uint8Array { return new Uint8Array() } -export function verifyES256K(data: string, signature: string, authenticators: VerificationMethod[]): VerificationMethod { +export function verifyES256K( + data: string, + signature: string, + authenticators: VerificationMethod[] +): VerificationMethod { const hash: Uint8Array = sha256(data) const sigObj: EcdsaSignature = toSignatureObject(signature) const fullPublicKeys = authenticators.filter(({ ethereumAddress }) => { @@ -63,7 +67,11 @@ export function verifyES256K(data: string, signature: string, authenticators: Ve return signer } -export function verifyRecoverableES256K(data: string, signature: string, authenticators: VerificationMethod[]): VerificationMethod { +export function verifyRecoverableES256K( + data: string, + signature: string, + authenticators: VerificationMethod[] +): VerificationMethod { let signatures: EcdsaSignature[] if (signature.length > 86) { signatures = [toSignatureObject(signature, true)] @@ -82,29 +90,34 @@ export function verifyRecoverableES256K(data: string, signature: string, authent const recoveredCompressedPublicKeyHex: string = recoveredKey.encode('hex', true) const recoveredAddress: string = toEthereumAddress(recoveredPublicKeyHex) - const signer: VerificationMethod = authenticators.find( - ({ publicKeyHex, ethereumAddress }) => - publicKeyHex === recoveredPublicKeyHex || - publicKeyHex === recoveredCompressedPublicKeyHex || - ethereumAddress === recoveredAddress - ) + const signer: VerificationMethod = authenticators.find((pk: VerificationMethod) => { + const keyHex = bytesToHex(extractPublicKeyBytes(pk)) + return ( + keyHex === recoveredPublicKeyHex || + keyHex === recoveredCompressedPublicKeyHex || + pk.ethereumAddress === recoveredAddress + ) + }) return signer } - const signer: VerificationMethod[] = signatures.map(checkSignatureAgainstSigner).filter(key => key != null) + const signer: VerificationMethod[] = signatures.map(checkSignatureAgainstSigner).filter((key) => key != null) if (signer.length === 0) throw new Error('Signature invalid for JWT') return signer[0] } -export function verifyEd25519(data: string, signature: string, authenticators: VerificationMethod[]): VerificationMethod { +export function verifyEd25519( + data: string, + signature: string, + authenticators: VerificationMethod[] +): VerificationMethod { const clear: Uint8Array = stringToBytes(data) const sig: Uint8Array = base64ToBytes(signature) const signer: VerificationMethod = authenticators.find((pk: VerificationMethod) => { return verify(extractPublicKeyBytes(pk), clear, sig) - } - ) + }) if (!signer) throw new Error('Signature invalid for JWT') return signer } diff --git a/src/__tests__/JWE-test.ts b/src/__tests__/JWE-test.ts index 8e4ce85e..6c53f42e 100644 --- a/src/__tests__/JWE-test.ts +++ b/src/__tests__/JWE-test.ts @@ -1,11 +1,6 @@ import { decryptJWE, createJWE, Encrypter } from '../JWE' import vectors from './jwe-vectors.js' -import { - xc20pDirEncrypter, - xc20pDirDecrypter, - x25519Encrypter, - x25519Decrypter -} from '../xc20pEncryption' +import { xc20pDirEncrypter, xc20pDirDecrypter, x25519Encrypter, x25519Decrypter } from '../xc20pEncryption' import { decodeBase64url } from '../util' import * as u8a from 'uint8arrays' import { randomBytes } from '@stablelib/random' @@ -64,14 +59,14 @@ describe('JWE', () => { it('Creates with only ciphertext', async () => { const jwe = await createJWE(cleartext, [encrypter]) expect(jwe.aad).toBeUndefined() - expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ alg:'dir', enc:'XC20P' }) + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ alg: 'dir', enc: 'XC20P' }) expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext) }) it('Creates with data in protected header', async () => { const jwe = await createJWE(cleartext, [encrypter], { more: 'protected' }) expect(jwe.aad).toBeUndefined() - expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ alg:'dir', enc:'XC20P', more: 'protected' }) + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ alg: 'dir', enc: 'XC20P', more: 'protected' }) expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext) }) @@ -79,7 +74,7 @@ describe('JWE', () => { const aad = u8a.fromString('this data is authenticated') const jwe = await createJWE(cleartext, [encrypter], { more: 'protected' }, aad) expect(u8a.fromString(jwe.aad, 'base64url')).toEqual(aad) - expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ alg:'dir', enc:'XC20P', more: 'protected' }) + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ alg: 'dir', enc: 'XC20P', more: 'protected' }) expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext) delete jwe.aad await expect(decryptJWE(jwe, decrypter)).rejects.toThrow('Failed to decrypt') @@ -101,14 +96,14 @@ describe('JWE', () => { it('Creates with only ciphertext', async () => { const jwe = await createJWE(cleartext, [encrypter]) expect(jwe.aad).toBeUndefined() - expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc:'XC20P' }) + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P' }) expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext) }) it('Creates with data in protected header', async () => { const jwe = await createJWE(cleartext, [encrypter], { more: 'protected' }) expect(jwe.aad).toBeUndefined() - expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc:'XC20P', more: 'protected' }) + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', more: 'protected' }) expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext) }) @@ -116,7 +111,7 @@ describe('JWE', () => { const aad = u8a.fromString('this data is authenticated') const jwe = await createJWE(cleartext, [encrypter], { more: 'protected' }, aad) expect(u8a.fromString(jwe.aad, 'base64url')).toEqual(aad) - expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc:'XC20P', more: 'protected' }) + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', more: 'protected' }) expect(await decryptJWE(jwe, decrypter)).toEqual(cleartext) delete jwe.aad await expect(decryptJWE(jwe, decrypter)).rejects.toThrow('Failed to decrypt') @@ -142,7 +137,7 @@ describe('JWE', () => { it('Creates with only ciphertext', async () => { const jwe = await createJWE(cleartext, [encrypter1, encrypter2]) expect(jwe.aad).toBeUndefined() - expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc:'XC20P' }) + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P' }) expect(await decryptJWE(jwe, decrypter1)).toEqual(cleartext) expect(await decryptJWE(jwe, decrypter2)).toEqual(cleartext) }) @@ -150,7 +145,7 @@ describe('JWE', () => { it('Creates with data in protected header', async () => { const jwe = await createJWE(cleartext, [encrypter1, encrypter2], { more: 'protected' }) expect(jwe.aad).toBeUndefined() - expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc:'XC20P', more: 'protected' }) + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', more: 'protected' }) expect(await decryptJWE(jwe, decrypter1)).toEqual(cleartext) expect(await decryptJWE(jwe, decrypter2)).toEqual(cleartext) }) @@ -159,7 +154,7 @@ describe('JWE', () => { const aad = u8a.fromString('this data is authenticated') const jwe = await createJWE(cleartext, [encrypter1, encrypter2], { more: 'protected' }, aad) expect(u8a.fromString(jwe.aad, 'base64url')).toEqual(aad) - expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc:'XC20P', more: 'protected' }) + expect(JSON.parse(decodeBase64url(jwe.protected))).toEqual({ enc: 'XC20P', more: 'protected' }) expect(await decryptJWE(jwe, decrypter1)).toEqual(cleartext) expect(await decryptJWE(jwe, decrypter2)).toEqual(cleartext) delete jwe.aad diff --git a/src/__tests__/JWT-test.ts b/src/__tests__/JWT-test.ts index 6ff41b11..4fd3da1b 100644 --- a/src/__tests__/JWT-test.ts +++ b/src/__tests__/JWT-test.ts @@ -163,7 +163,7 @@ describe('createJWT()', () => { }) describe('verifyJWT()', () => { - const resolver = { resolve: jest.fn().mockReturnValue(didDoc) } as unknown as Resolver + const resolver = ({ resolve: jest.fn().mockReturnValue(didDoc) } as unknown) as Resolver describe('pregenerated JWT', () => { // tslint:disable-next-line: max-line-length @@ -174,7 +174,9 @@ describe('verifyJWT()', () => { return expect(payload).toMatchSnapshot() }) it('verifies the JWT and return correct profile', async () => { - const { didResolutionResult: { didDocument } } = await verifyJWT(incomingJwt, { resolver }) + const { + didResolutionResult: { didDocument } + } = await verifyJWT(incomingJwt, { resolver }) return expect(didDocument).toEqual(didDoc.didDocument) }) it('verifies the JWT and return correct did for the iss', async () => { @@ -265,7 +267,7 @@ describe('verifyJWT()', () => { }) it('handles ES256K algorithm with ethereum address - github #14', async () => { - const ethResolver = { resolve: jest.fn().mockReturnValue(didDocDefault) } as unknown as Resolver + const ethResolver = ({ resolve: jest.fn().mockReturnValue(didDocDefault) } as unknown) as Resolver const jwt = await createJWT({ hello: 'world' }, { issuer: aud, signer, alg: 'ES256K' }) const { payload } = await verifyJWT(jwt, { resolver: ethResolver }) return expect(payload).toMatchSnapshot() @@ -446,7 +448,7 @@ describe('resolveAuthenticator()', () => { owner: did, publicKeyBase58: 'dummyvalue' } - + const ecKey7 = { id: `${did}#keys-auth7`, type: 'EcdsaSecp256k1VerificationKey2019', @@ -466,7 +468,7 @@ describe('resolveAuthenticator()', () => { '@context': 'https://w3id.org/did/v1', id: did, publicKey: [ecKey1] - }, + } } const multipleKeys = { @@ -475,7 +477,7 @@ describe('resolveAuthenticator()', () => { id: did, publicKey: [ecKey1, ecKey2, ecKey3, encKey1, edKey, edKey2], authentication: [authKey1, authKey2, edAuthKey] - }, + } } const multipleAuthTypes = { @@ -484,7 +486,7 @@ describe('resolveAuthenticator()', () => { id: did, publicKey: [ecKey1, ecKey2, ecKey3, encKey1, edKey, edKey2, edKey6, ecKey7], authentication: [authKey1, authKey2, edAuthKey, `${did}#keys-auth6`, `${did}#keys-auth7`, edKey8] - }, + } } const unsupportedFormat = { @@ -492,19 +494,23 @@ describe('resolveAuthenticator()', () => { '@context': 'https://w3id.org/did/v1', id: did, publicKey: [encKey1] - }, + } } const noPublicKey = { didDocument: { '@context': 'https://w3id.org/did/v1', id: did - }, + } } describe('DID', () => { describe('ES256K', () => { it('finds public key', async () => { - const authenticators = await resolveAuthenticator({ resolve: jest.fn().mockReturnValue(singleKey) } as unknown as Resolver, alg, did) + const authenticators = await resolveAuthenticator( + ({ resolve: jest.fn().mockReturnValue(singleKey) } as unknown) as Resolver, + alg, + did + ) return expect(authenticators).toEqual({ authenticators: [ecKey1], issuer: did, @@ -514,7 +520,7 @@ describe('resolveAuthenticator()', () => { it('filters out irrelevant public keys', async () => { const authenticators = await resolveAuthenticator( - { resolve: jest.fn().mockReturnValue(multipleKeys) } as unknown as Resolver, + ({ resolve: jest.fn().mockReturnValue(multipleKeys) } as unknown) as Resolver, alg, did ) @@ -527,7 +533,7 @@ describe('resolveAuthenticator()', () => { it('only list authenticators able to authenticate a user', async () => { const authenticators = await resolveAuthenticator( - { resolve: jest.fn().mockReturnValue(multipleKeys) } as unknown as Resolver, + ({ resolve: jest.fn().mockReturnValue(multipleKeys) } as unknown) as Resolver, alg, did, true @@ -541,7 +547,7 @@ describe('resolveAuthenticator()', () => { it('lists authenticators with multiple key types in doc', async () => { const authenticators = await resolveAuthenticator( - { resolve: jest.fn().mockReturnValue(multipleAuthTypes) } as unknown as Resolver, + ({ resolve: jest.fn().mockReturnValue(multipleAuthTypes) } as unknown) as Resolver, alg, did, true @@ -555,7 +561,11 @@ describe('resolveAuthenticator()', () => { it('errors if no suitable public keys exist', async () => { return await expect( - resolveAuthenticator({ resolve: jest.fn().mockReturnValue(unsupportedFormat) } as unknown as Resolver, alg, did) + resolveAuthenticator( + ({ resolve: jest.fn().mockReturnValue(unsupportedFormat) } as unknown) as Resolver, + alg, + did + ) ).rejects.toEqual(new Error(`DID document for ${did} does not have public keys for ${alg}`)) }) }) @@ -564,7 +574,7 @@ describe('resolveAuthenticator()', () => { const alg = 'Ed25519' it('filters out irrelevant public keys', async () => { const authenticators = await resolveAuthenticator( - { resolve: jest.fn().mockReturnValue(multipleKeys) } as unknown as Resolver, + ({ resolve: jest.fn().mockReturnValue(multipleKeys) } as unknown) as Resolver, alg, did ) @@ -577,7 +587,7 @@ describe('resolveAuthenticator()', () => { it('only list authenticators able to authenticate a user', async () => { const authenticators = await resolveAuthenticator( - { resolve: jest.fn().mockReturnValue(multipleKeys) } as unknown as Resolver, + ({ resolve: jest.fn().mockReturnValue(multipleKeys) } as unknown) as Resolver, alg, did, true @@ -591,7 +601,7 @@ describe('resolveAuthenticator()', () => { it('lists authenticators with multiple key types in doc', async () => { const authenticators = await resolveAuthenticator( - { resolve: jest.fn().mockReturnValue(multipleAuthTypes) } as unknown as Resolver, + ({ resolve: jest.fn().mockReturnValue(multipleAuthTypes) } as unknown) as Resolver, alg, did, true @@ -605,37 +615,45 @@ describe('resolveAuthenticator()', () => { it('errors if no suitable public keys exist', async () => { return await expect( - resolveAuthenticator({ resolve: jest.fn().mockReturnValue(unsupportedFormat) } as unknown as Resolver, alg, did) + resolveAuthenticator( + ({ resolve: jest.fn().mockReturnValue(unsupportedFormat) } as unknown) as Resolver, + alg, + did + ) ).rejects.toEqual(new Error(`DID document for ${did} does not have public keys for ${alg}`)) }) }) it('errors if no suitable public keys exist for authentication', async () => { return await expect( - resolveAuthenticator({ resolve: jest.fn().mockReturnValue(singleKey) } as unknown as Resolver, alg, did, true) - ).rejects.toEqual( - new Error(`DID document for ${did} does not have public keys suitable for authenticating user`) - ) + resolveAuthenticator(({ resolve: jest.fn().mockReturnValue(singleKey) } as unknown) as Resolver, alg, did, true) + ).rejects.toEqual(new Error(`DID document for ${did} does not have public keys suitable for authenticating user`)) }) it('errors if no public keys exist', async () => { return await expect( - resolveAuthenticator({ resolve: jest.fn().mockReturnValue(noPublicKey) } as unknown as Resolver, alg, did) + resolveAuthenticator(({ resolve: jest.fn().mockReturnValue(noPublicKey) } as unknown) as Resolver, alg, did) ).rejects.toEqual(new Error(`DID document for ${did} does not have public keys for ${alg}`)) }) it('errors if no DID document exists', async () => { - return await expect(resolveAuthenticator({ resolve: jest.fn().mockReturnValue({ - didResolutionMetadata: { error: 'notFound' }, - didDocument: null - }) } as unknown as Resolver, alg, did)).rejects.toEqual( - new Error(`Unable to resolve DID document for ${did}: notFound, `) - ) + return await expect( + resolveAuthenticator( + ({ + resolve: jest.fn().mockReturnValue({ + didResolutionMetadata: { error: 'notFound' }, + didDocument: null + }) + } as unknown) as Resolver, + alg, + did + ) + ).rejects.toEqual(new Error(`Unable to resolve DID document for ${did}: notFound, `)) }) it('errors if no supported signature types exist', async () => { return await expect( - resolveAuthenticator({ resolve: jest.fn().mockReturnValue(singleKey) } as unknown as Resolver, 'ESBAD', did) + resolveAuthenticator(({ resolve: jest.fn().mockReturnValue(singleKey) } as unknown) as Resolver, 'ESBAD', did) ).rejects.toEqual(new Error('No supported signature types for algorithm ESBAD')) }) }) diff --git a/src/__tests__/VerifierAlgorithm-test.ts b/src/__tests__/VerifierAlgorithm-test.ts index b11663a0..3af4acbd 100644 --- a/src/__tests__/VerifierAlgorithm-test.ts +++ b/src/__tests__/VerifierAlgorithm-test.ts @@ -5,7 +5,7 @@ import NaclSigner from '../signers/NaclSigner' import { toEthereumAddress } from '../Digest' import nacl from 'tweetnacl' import { ec as EC } from 'elliptic' -import { base64ToBytes, bytesToBase64 } from '../util' +import { base64ToBytes, bytesToBase58, bytesToBase64, hexToBytes } from '../util' import * as u8a from 'uint8arrays' const secp256k1 = new EC('secp256k1') @@ -34,6 +34,8 @@ const privateKey = '278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25 const kp = secp256k1.keyFromPrivate(privateKey) const publicKey = String(kp.getPublic('hex')) const compressedPublicKey = String(kp.getPublic().encode('hex', true)) +const publicKeyBase64 = bytesToBase64(hexToBytes(publicKey)) +const publicKeyBase58 = bytesToBase58(hexToBytes(publicKey)) const address = toEthereumAddress(publicKey) const signer = SimpleSigner(privateKey) @@ -72,6 +74,13 @@ const compressedKey = { publicKeyHex: compressedPublicKey } +const recoveryMethod2020Key = { + id: `${did}#keys-recovery`, + type: 'EcdsaSecp256k1RecoveryMethod2020', + controller: did, + ethereumAddress: address +} + const edKey = { id: `${did}#keys-5`, type: 'ED25519SignatureVerification', @@ -121,12 +130,19 @@ describe('ES256K', () => { it('validates with publicKeyBase58', async () => { const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer }) const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) - const publicKeyBase58 = u8a.toString(u8a.fromString(ecKey2.publicKeyHex, 'base16'), 'base58btc') const pubkey = Object.assign({ publicKeyBase58 }, ecKey2) delete pubkey.publicKeyHex return expect(verifier(parts[1], parts[2], [pubkey])).toEqual(pubkey) }) + it('validates with publicKeyBase64', async () => { + const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer }) + const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) + const pubkey = Object.assign({ publicKeyBase64 }, ecKey2) + delete pubkey.publicKeyHex + return expect(verifier(parts[1], parts[2], [pubkey])).toEqual(pubkey) + }) + it('validates signature with compressed public key and picks correct public key', async () => { const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer }) const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) @@ -158,6 +174,12 @@ describe('ES256K', () => { const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) return expect(verifier(parts[1], parts[2], [ethAddress])).toEqual(ethAddress) }) + + it('validates signature produced by EcdsaSecp256k1RecoveryMethod2020 - github #152', async () => { + const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer }) + const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) + return expect(verifier(parts[1], parts[2], [recoveryMethod2020Key])).toEqual(recoveryMethod2020Key) + }) }) describe('ES256K-R', () => { @@ -181,6 +203,28 @@ describe('ES256K-R', () => { return expect(verifier(parts[1], parts[2], [ecKey1, ethAddress])).toEqual(ethAddress) }) + it('validates signature with EcdsaSecp256k1RecoveryMethod2020 - github #152', async () => { + const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer, alg: 'ES256K-R' }) + const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) + return expect(verifier(parts[1], parts[2], [ecKey1, recoveryMethod2020Key])).toEqual(recoveryMethod2020Key) + }) + + it('validates with publicKeyBase58', async () => { + const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer, alg: 'ES256K-R' }) + const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) + const pubkey = Object.assign({ publicKeyBase58 }, ecKey2) + delete pubkey.publicKeyHex + return expect(verifier(parts[1], parts[2], [pubkey])).toEqual(pubkey) + }) + + it('validates with publicKeyBase64', async () => { + const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer, alg: 'ES256K-R' }) + const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) + const pubkey = Object.assign({ publicKeyBase64 }, ecKey2) + delete pubkey.publicKeyHex + return expect(verifier(parts[1], parts[2], [pubkey])).toEqual(pubkey) + }) + it('throws error if invalid signature', async () => { const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer, alg: 'ES256K-R' }) const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/) diff --git a/src/__tests__/didkey-test.ts b/src/__tests__/didkey-test.ts index 22bdbff6..aaaf6978 100644 --- a/src/__tests__/didkey-test.ts +++ b/src/__tests__/didkey-test.ts @@ -20,7 +20,7 @@ describe('Ed25519', () => { }) it('handles EdDSA algorithm with did:key', async () => { - const resolver = { + const resolver = ({ resolve: async () => ({ didResolutionMetadata: {}, didDocumentMetadata: {}, @@ -38,7 +38,7 @@ describe('Ed25519', () => { ] } }) - } as unknown as Resolver + } as unknown) as Resolver const jwt = 'eyJhbGciOiJFZERTQSJ9.eyJleHAiOjE3NjQ4Nzg5MDgsImlzcyI6ImRpZDprZXk6ejZNa29USHNnTk5yYnk4SnpDTlExaVJMeVc1UVE2UjhYdXU2QUE4aWdHck1WUFVNIiwibmJmIjoxNjA3MTEyNTA4LCJzdWIiOiJkaWQ6a2V5Ono2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSIsInZjIjp7IkBjb250ZXh0IjpbImh0dHBzOi8vd3d3LnczLm9yZy8yMDE4L2NyZWRlbnRpYWxzL3YxIiwiaHR0cHM6Ly9pZGVudGl0eS5mb3VuZGF0aW9uLy53ZWxsLWtub3duL2RpZC1jb25maWd1cmF0aW9uL3YxIl0sImNyZWRlbnRpYWxTdWJqZWN0Ijp7ImlkIjoiZGlkOmtleTp6Nk1rb1RIc2dOTnJieThKekNOUTFpUkx5VzVRUTZSOFh1dTZBQThpZ0dyTVZQVU0iLCJvcmlnaW4iOiJpZGVudGl0eS5mb3VuZGF0aW9uIn0sImV4cGlyYXRpb25EYXRlIjoiMjAyNS0xMi0wNFQxNDowODoyOC0wNjowMCIsImlzc3VhbmNlRGF0ZSI6IjIwMjAtMTItMDRUMTQ6MDg6MjgtMDY6MDAiLCJpc3N1ZXIiOiJkaWQ6a2V5Ono2TWtvVEhzZ05OcmJ5OEp6Q05RMWlSTHlXNVFRNlI4WHV1NkFBOGlnR3JNVlBVTSIsInR5cGUiOlsiVmVyaWZpYWJsZUNyZWRlbnRpYWwiLCJEb21haW5MaW5rYWdlQ3JlZGVudGlhbCJdfX0.6ovgQ-T_rmYueviySqXhzMzgqJMAizOGUKAObQr2iikoRNsb8DHfna4rh1puwWqYwgT3QJVpzdO_xZARAYM9Dw' const { payload } = await verifyJWT(jwt, { resolver }) diff --git a/src/__tests__/xc20pEncryption-test.ts b/src/__tests__/xc20pEncryption-test.ts index 505f8977..eea9051c 100644 --- a/src/__tests__/xc20pEncryption-test.ts +++ b/src/__tests__/xc20pEncryption-test.ts @@ -1,7 +1,4 @@ -import { - x25519Decrypter, - resolveX25519Encrypters -} from '../xc20pEncryption' +import { x25519Decrypter, resolveX25519Encrypters } from '../xc20pEncryption' import { decryptJWE, createJWE } from '../JWE' import * as u8a from 'uint8arrays' import { randomBytes } from '@stablelib/random' @@ -22,20 +19,23 @@ describe('xc20pEncryption', () => { decrypter1 = x25519Decrypter(kp1.secretKey) decrypter2 = x25519Decrypter(kp2.secretKey) resolver = { - resolve: jest.fn(did => { + resolve: jest.fn((did) => { if (did === did1) { return { didDocument: { - verificationMethod: [{ - id: did1 + '#abc', - type: 'X25519KeyAgreementKey2019', - controller: did1, - publicKeyBase58: u8a.toString(kp1.publicKey, 'base58btc') - }], - keyAgreement: [{ - id: 'irrelevant key' - }, - did1 + '#abc' + verificationMethod: [ + { + id: did1 + '#abc', + type: 'X25519KeyAgreementKey2019', + controller: did1, + publicKeyBase58: u8a.toString(kp1.publicKey, 'base58btc') + } + ], + keyAgreement: [ + { + id: 'irrelevant key' + }, + did1 + '#abc' ] } } @@ -43,12 +43,14 @@ describe('xc20pEncryption', () => { return { didDocument: { verificationMethod: [], - keyAgreement: [{ - id: did2 + '#abc', - type: 'X25519KeyAgreementKey2019', - controller: did2, - publicKeyBase58: u8a.toString(kp2.publicKey, 'base58btc') - }] + keyAgreement: [ + { + id: did2 + '#abc', + type: 'X25519KeyAgreementKey2019', + controller: did2, + publicKeyBase58: u8a.toString(kp2.publicKey, 'base58btc') + } + ] } } } else if (did === did3) { @@ -72,8 +74,12 @@ describe('xc20pEncryption', () => { }) it('throws error if key is not found', async () => { - await expect(resolveX25519Encrypters([did3], resolver)).rejects.toThrow('Could not find x25519 key for did:test:3') - await expect(resolveX25519Encrypters([did4], resolver)).rejects.toThrow('Could not find x25519 key for did:test:4') + await expect(resolveX25519Encrypters([did3], resolver)).rejects.toThrow( + 'Could not find x25519 key for did:test:3' + ) + await expect(resolveX25519Encrypters([did4], resolver)).rejects.toThrow( + 'Could not find x25519 key for did:test:4' + ) }) }) }) diff --git a/src/index.ts b/src/index.ts index afd7a288..3e6750cd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,7 +12,7 @@ import { Signer, JWTHeader, JWTPayload, - JWTVerified, + JWTVerified } from './JWT' import { toEthereumAddress } from './Digest' export { JWE, createJWE, decryptJWE, Encrypter, Decrypter } from './JWE' @@ -39,5 +39,5 @@ export { Signer, JWTHeader, JWTPayload, - JWTVerified, + JWTVerified } diff --git a/src/signers/EdDSASigner.ts b/src/signers/EdDSASigner.ts index 6cb7c55e..55e1938c 100644 --- a/src/signers/EdDSASigner.ts +++ b/src/signers/EdDSASigner.ts @@ -22,7 +22,7 @@ export function EdDSASigner(secretKey: string | Uint8Array): Signer { throw new Error(`Invalid private key format. Expecting 64 bytes, but got ${privateKeyBytes.length}`) } return async (data: string | Uint8Array): Promise => { - const dataBytes: Uint8Array = (typeof data === 'string') ? stringToBytes(data) : data + const dataBytes: Uint8Array = typeof data === 'string' ? stringToBytes(data) : data const sig: Uint8Array = sign(privateKeyBytes, dataBytes) return bytesToBase64url(sig) } diff --git a/src/signers/EllipticSigner.ts b/src/signers/EllipticSigner.ts index d70bafe2..3a48352d 100644 --- a/src/signers/EllipticSigner.ts +++ b/src/signers/EllipticSigner.ts @@ -12,7 +12,7 @@ import { ES256KSigner } from './ES256KSigner' * ... * }) * ``` - * + * * @param {String} hexPrivateKey a hex encoded private key * @return {Function} a configured signer function */ diff --git a/src/signers/NaclSigner.ts b/src/signers/NaclSigner.ts index 9342ff1a..f2357efc 100644 --- a/src/signers/NaclSigner.ts +++ b/src/signers/NaclSigner.ts @@ -1,11 +1,9 @@ -import { sign } from '@stablelib/ed25519' import { EdDSASigner as EdDSASigner } from './EdDSASigner' import { Signer } from '../JWT' -import { base64ToBytes, bytesToBase64url, stringToBytes } from '../util' /** * @deprecated Please use EdDSASigner - * + * * The NaclSigner returns a configured function for signing data using the Ed25519 algorithm. * * The signing function itself takes the data as a `string` or `Uint8Array` parameter and returns a `base64Url`-encoded signature. diff --git a/src/util.ts b/src/util.ts index d62f8e5c..6a3c278b 100644 --- a/src/util.ts +++ b/src/util.ts @@ -26,6 +26,10 @@ export function base58ToBytes(s: string): Uint8Array { return u8a.fromString(s, 'base58btc') } +export function bytesToBase58(b: Uint8Array): string { + return u8a.toString(b, 'base58btc') +} + export function hexToBytes(s: string): Uint8Array { const input = s.startsWith('0x') ? s.substring(2) : s return u8a.fromString(input.toLowerCase(), 'base16') diff --git a/src/xc20pEncryption.ts b/src/xc20pEncryption.ts index 9884c876..2430fe9e 100644 --- a/src/xc20pEncryption.ts +++ b/src/xc20pEncryption.ts @@ -27,7 +27,7 @@ export function xc20pDirEncrypter(key: Uint8Array): Encrypter { const protHeader = encodeBase64url(JSON.stringify(Object.assign({ alg }, protectedHeader, { enc }))) const encodedAad = new Uint8Array(Buffer.from(aad ? `${protHeader}.${bytesToBase64url(aad)}` : protHeader)) return { - ...(xc20pEncrypt(cleartext, encodedAad)), + ...xc20pEncrypt(cleartext, encodedAad), protectedHeader: protHeader } } @@ -83,15 +83,16 @@ export async function resolveX25519Encrypters(dids: string[], resolver: Resolver dids.map(async (did) => { const { didResolutionMetadata, didDocument } = await resolver.resolve(did) if (didResolutionMetadata?.error) { - throw new Error(`Could not find x25519 key for ${did}: ${didResolutionMetadata.error}, ${didResolutionMetadata.message}`) + throw new Error( + `Could not find x25519 key for ${did}: ${didResolutionMetadata.error}, ${didResolutionMetadata.message}` + ) } if (!didDocument.keyAgreement) throw new Error(`Could not find x25519 key for ${did}`) const agreementKeys: VerificationMethod[] = didDocument.keyAgreement?.map((key) => { if (typeof key === 'string') { - return [ - ...(didDocument.publicKey || []), - ...(didDocument.verificationMethod || []) - ].find((pk) => pk.id === key) + return [...(didDocument.publicKey || []), ...(didDocument.verificationMethod || [])].find( + (pk) => pk.id === key + ) } return key }) @@ -105,11 +106,7 @@ export async function resolveX25519Encrypters(dids: string[], resolver: Resolver } function validateHeader(header: Record) { - if(!( - header.epk && - header.iv && - header.tag - )) { + if (!(header.epk && header.iv && header.tag)) { throw new Error('Invalid JWE') } }