Skip to content

Commit

Permalink
feat: CAIP 10 support for bip122 & cosmos (#205)
Browse files Browse the repository at this point in the history
* blockchainAccountId (CAIP - 10)

* update test code

* remove crypto-js

* remove ripemd160

* remove Buffer

Buffer -> u8a

* add Ripemd160.test.ts

This contains a typescript implementation of RIPEMD160:
https://homes.esat.kuleuven.be/~bosselae/ripemd160.html
  • Loading branch information
daoauth authored Nov 10, 2021
1 parent 3ae3ee4 commit 73cba89
Show file tree
Hide file tree
Showing 9 changed files with 407 additions and 9 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
"@stablelib/sha256": "^1.0.1",
"@stablelib/x25519": "^1.0.1",
"@stablelib/xchacha20poly1305": "^1.0.1",
"bech32": "^2.0.0",
"canonicalize": "^1.0.5",
"did-resolver": "^3.1.1",
"elliptic": "^6.5.4",
Expand Down
10 changes: 6 additions & 4 deletions src/VerifierAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { verify } from '@stablelib/ed25519'
import type { VerificationMethod } from 'did-resolver'
import { bases } from 'multiformats/basics'
import { hexToBytes, base58ToBytes, base64ToBytes, bytesToHex, EcdsaSignature, stringToBytes } from './util'
import { verifyBlockchainAccountId } from './blockchains'

const secp256k1 = new EC('secp256k1')

Expand Down Expand Up @@ -60,7 +61,7 @@ export function verifyES256K(
const fullPublicKeys = authenticators.filter(({ ethereumAddress, blockchainAccountId }) => {
return typeof ethereumAddress === 'undefined' && typeof blockchainAccountId === 'undefined'
})
const ethAddressKeys = authenticators.filter(({ ethereumAddress, blockchainAccountId }) => {
const blockchainAddressKeys = authenticators.filter(({ ethereumAddress, blockchainAccountId }) => {
return typeof ethereumAddress !== 'undefined' || typeof blockchainAccountId !== undefined
})

Expand All @@ -73,8 +74,8 @@ export function verifyES256K(
}
})

if (!signer && ethAddressKeys.length > 0) {
signer = verifyRecoverableES256K(data, signature, ethAddressKeys)
if (!signer && blockchainAddressKeys.length > 0) {
signer = verifyRecoverableES256K(data, signature, blockchainAddressKeys)
}

if (!signer) throw new Error('invalid_signature: Signature invalid for JWT')
Expand Down Expand Up @@ -111,7 +112,8 @@ export function verifyRecoverableES256K(
keyHex === recoveredPublicKeyHex ||
keyHex === recoveredCompressedPublicKeyHex ||
pk.ethereumAddress?.toLowerCase() === recoveredAddress ||
pk.blockchainAccountId?.split('@eip155')?.[0].toLowerCase() === recoveredAddress
pk.blockchainAccountId?.split('@eip155')?.[0].toLowerCase() === recoveredAddress || // CAIP-2
verifyBlockchainAccountId(recoveredPublicKeyHex, pk.blockchainAccountId) // CAIP-10
)
})

Expand Down
70 changes: 70 additions & 0 deletions src/__tests__/Ripemd160.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// https://homes.esat.kuleuven.be/~bosselae/ripemd160.html

import * as u8a from 'uint8arrays'
import { Ripemd160 } from '../blockchains/utils/ripemd160'

describe('Ripemd160', () => {
it('message: "" (empty string)', () => {
expect.assertions(1)
const rawString = ''
const expectedHash = '9c1185a5c5e9fc54612808977ee8f548b2258d31'
const actualHash = u8a.toString(new Ripemd160().update(u8a.fromString(rawString, 'ascii')).digest(), 'hex')
return expect(actualHash).toBe(expectedHash)
})
it('message: "a"', () => {
expect.assertions(1)
const rawString = 'a'
const expectedHash = '0bdc9d2d256b3ee9daae347be6f4dc835a467ffe'
const actualHash = u8a.toString(new Ripemd160().update(u8a.fromString(rawString, 'ascii')).digest(), 'hex')
return expect(actualHash).toBe(expectedHash)
})
it('message: "abc"', () => {
expect.assertions(1)
const rawString = 'abc'
const expectedHash = '8eb208f7e05d987a9b044a8e98c6b087f15a0bfc'
const actualHash = u8a.toString(new Ripemd160().update(u8a.fromString(rawString, 'ascii')).digest(), 'hex')
return expect(actualHash).toBe(expectedHash)
})
it('message: "message digest"', () => {
expect.assertions(1)
const rawString = 'message digest'
const expectedHash = '5d0689ef49d2fae572b881b123a85ffa21595f36'
const actualHash = u8a.toString(new Ripemd160().update(u8a.fromString(rawString, 'ascii')).digest(), 'hex')
return expect(actualHash).toBe(expectedHash)
})
it('message: "abcdefghijklmnopqrstuvwxyz"', () => {
expect.assertions(1)
const rawString = 'abcdefghijklmnopqrstuvwxyz'
const expectedHash = 'f71c27109c692c1b56bbdceb5b9d2865b3708dbc'
const actualHash = u8a.toString(new Ripemd160().update(u8a.fromString(rawString, 'ascii')).digest(), 'hex')
return expect(actualHash).toBe(expectedHash)
})
it('message: "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"', () => {
expect.assertions(1)
const rawString = 'abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq'
const expectedHash = '12a053384a9c0c88e405a06c27dcf49ada62eb2b'
const actualHash = u8a.toString(new Ripemd160().update(u8a.fromString(rawString, 'ascii')).digest(), 'hex')
return expect(actualHash).toBe(expectedHash)
})
it('message: "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"', () => {
expect.assertions(1)
const rawString = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
const expectedHash = 'b0e20b6e3116640286ed3a87a5713079b21f5189'
const actualHash = u8a.toString(new Ripemd160().update(u8a.fromString(rawString, 'ascii')).digest(), 'hex')
return expect(actualHash).toBe(expectedHash)
})
it('message: 8 times "1234567890"', () => {
expect.assertions(1)
const rawString = '1234567890'.repeat(8)
const expectedHash = '9b752e45573d4b39f4dbd3323cab82bf63326bfb'
const actualHash = u8a.toString(new Ripemd160().update(u8a.fromString(rawString, 'ascii')).digest(), 'hex')
return expect(actualHash).toBe(expectedHash)
})
it('message: 1 million times "a"', () => {
expect.assertions(1)
const rawString = 'a'.repeat(1000000)
const expectedHash = '52783243c1697bdbe16d37f97f68f08325dc1528'
const actualHash = u8a.toString(new Ripemd160().update(u8a.fromString(rawString, 'ascii')).digest(), 'hex')
return expect(actualHash).toBe(expectedHash)
})
});
78 changes: 73 additions & 5 deletions src/__tests__/VerifierAlgorithm.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import VerifierAlgorithm from '../VerifierAlgorithm'
import { createJWT } from '../JWT'
import { toEthereumAddress } from '../Digest'
import nacl from 'tweetnacl'
import { ec as EC } from 'elliptic'
import { base64ToBytes, bytesToBase58, bytesToBase64, hexToBytes, bytesToBase64url, bytesToMultibase } from '../util'
import * as u8a from 'uint8arrays'
import { EdDSASigner } from '../signers/EdDSASigner'
import { ES256KSigner } from '../signers/ES256KSigner'
import { toEthereumAddress } from '../Digest'
import { publicKeyToAddress as toBip122Address } from '../blockchains/bip122'
import { publicKeyToAddress as toCosmosAddressWithoutPrefix } from '../blockchains/cosmos'

const secp256k1 = new EC('secp256k1')

Expand Down Expand Up @@ -43,7 +45,10 @@ const publicKeyJwk = {
y: bytesToBase64url(hexToBytes(kp.getPublic().getY().toString('hex'))),
}
const publicKeyMultibase = bytesToMultibase(hexToBytes(publicKey), 'base58btc')
const address = toEthereumAddress(publicKey)
const eip155 = toEthereumAddress(publicKey)
const bip122 = toBip122Address(publicKey)
const cosmosPrefix = 'example'
const cosmos = toCosmosAddressWithoutPrefix(publicKey, cosmosPrefix)
const signer = ES256KSigner(privateKey)
const recoverySigner = ES256KSigner(privateKey, true)

Expand Down Expand Up @@ -72,14 +77,35 @@ const ethAddress = {
id: `${did}#keys-3`,
type: 'Secp256k1VerificationKey2018',
controller: did,
ethereumAddress: address,
ethereumAddress: eip155,
}

const blockchainAddress = {
id: `${did}#keys-blockchain`,
type: 'EcdsaSecp256k1RecoveryMethod2020',
controller: did,
blockchainAccountId: `${address}@eip155:1`,
blockchainAccountId: `${eip155}@eip155:1`,
}

const blockchainAddressCaip10 = {
id: `${did}#keys-blockchain`,
type: 'EcdsaSecp256k1RecoveryMethod2020',
controller: did,
blockchainAccountId: `eip155:1:${eip155}`,
}

const blockchainAddressBip122 = {
id: `${did}#keys-blockchain`,
type: 'EcdsaSecp256k1RecoveryMethod2020',
controller: did,
blockchainAccountId: `bip122:000000000019d6689c085ae165831e93:${bip122}`,
}

const blockchainAddressCosmos = {
id: `${did}#keys-blockchain`,
type: 'EcdsaSecp256k1RecoveryMethod2020',
controller: did,
blockchainAccountId: `cosmos:${cosmosPrefix}:${cosmos}`,
}

const compressedKey = {
Expand All @@ -93,7 +119,7 @@ const recoveryMethod2020Key = {
id: `${did}#keys-recovery`,
type: 'EcdsaSecp256k1RecoveryMethod2020',
controller: did,
ethereumAddress: address,
ethereumAddress: eip155,
}

const edKey = {
Expand Down Expand Up @@ -224,6 +250,27 @@ describe('ES256K', () => {
return expect(verifier(parts[1], parts[2], [blockchainAddress])).toEqual(blockchainAddress)
})

it('validates signature produced by blockchainAccountId - CAIP 10 (EIP 155)', async () => {
expect.assertions(1)
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], [blockchainAddressCaip10])).toEqual(blockchainAddressCaip10)
})

it('validates signature produced by blockchainAccountId - CAIP 10 (BIP 122)', async () => {
expect.assertions(1)
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], [blockchainAddressBip122])).toEqual(blockchainAddressBip122)
})

it('validates signature produced by blockchainAccountId - CAIP 10 (Cosmos)', async () => {
expect.assertions(1)
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], [blockchainAddressCosmos])).toEqual(blockchainAddressCosmos)
})

it('validates signature produced by EcdsaSecp256k1RecoveryMethod2020 - github #152', async () => {
expect.assertions(1)
const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer })
Expand Down Expand Up @@ -263,6 +310,27 @@ describe('ES256K-R', () => {
return expect(verifier(parts[1], parts[2], [ecKey1, blockchainAddress])).toEqual(blockchainAddress)
})

it('validates signature with blockchainAccountId - CAIP 10 (EIP 155)', async () => {
expect.assertions(1)
const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer: recoverySigner, 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, blockchainAddressCaip10])).toEqual(blockchainAddressCaip10)
})

it('validates signature with blockchainAccountId - CAIP 10 (BIP 122)', async () => {
expect.assertions(1)
const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer: recoverySigner, 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, blockchainAddressBip122])).toEqual(blockchainAddressBip122)
})

it('validates signature with blockchainAccountId - CAIP 10 (COSMOS)', async () => {
expect.assertions(1)
const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer: recoverySigner, 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, blockchainAddressCosmos])).toEqual(blockchainAddressCosmos)
})

it('validates signature with EcdsaSecp256k1RecoveryMethod2020 - github #152', async () => {
expect.assertions(1)
const jwt = await createJWT({ bla: 'bla' }, { issuer: did, signer: recoverySigner, alg: 'ES256K-R' })
Expand Down
15 changes: 15 additions & 0 deletions src/blockchains/bip122.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import * as u8a from 'uint8arrays'
import { bytesToBase58 } from '../util'
import { sha256 } from '../Digest'
import { Ripemd160 } from './utils/ripemd160'

export const publicKeyToAddress = (publicKey: string): string => {
const publicKeyBuffer = u8a.fromString(publicKey, 'hex')
const publicKeyHash = new Ripemd160().update(sha256(publicKeyBuffer)).digest()
const step1 = '00' + u8a.toString(publicKeyHash, 'hex')
const step2 = sha256(u8a.fromString(step1, 'hex'))
const step3 = sha256(step2)
const checksum = u8a.toString(step3, 'hex').substring(0, 8)
const step4 = step1 + checksum
return bytesToBase58(u8a.fromString(step4, 'hex'))
}
14 changes: 14 additions & 0 deletions src/blockchains/cosmos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ec as EC } from 'elliptic'
import { bech32 } from 'bech32'
import * as u8a from 'uint8arrays'
import { sha256 } from '../Digest'
import { Ripemd160 } from './utils/ripemd160'

export const publicKeyToAddress = (publicKey: string, prefix: string): string => {
const ec = new EC('secp256k1')
const compressedPublicKey = ec.keyFromPublic(publicKey, 'hex').getPublic().encode('hex', true)
const publicKeyBuffer = u8a.fromString(compressedPublicKey, 'hex')
const hash = new Ripemd160().update(sha256(publicKeyBuffer)).digest()
const words = bech32.toWords(hash)
return bech32.encode(prefix, words).replace(prefix, '')
}
24 changes: 24 additions & 0 deletions src/blockchains/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { publicKeyToAddress as bip122 } from './bip122'
import { publicKeyToAddress as cosmos } from './cosmos'
import { toEthereumAddress } from '../Digest'

export const verifyBlockchainAccountId = (publicKey: string, blockchainAccountId: string | undefined): boolean => {
if (blockchainAccountId) {
const chain = blockchainAccountId.split(':')
switch (chain[0]) {
case 'bip122':
chain[chain.length - 1] = bip122(publicKey)
break
case 'cosmos':
chain[chain.length - 1] = cosmos(publicKey, chain[1])
break
case 'eip155':
chain[chain.length - 1] = toEthereumAddress(publicKey)
break
default:
return false
}
return chain.join(':') === blockchainAccountId
}
return false
}
Loading

0 comments on commit 73cba89

Please sign in to comment.