Skip to content

Commit

Permalink
fix: support multiple pubkey encodings (#139)
Browse files Browse the repository at this point in the history
* chore: remove utf8 dep

* fix: support multiple public key encodings

* fix: add test for base58 encoded public keys

fixes #128
fixes #127
  • Loading branch information
oed authored Nov 10, 2020
1 parent a4cf35d commit c4ae63a
Show file tree
Hide file tree
Showing 6 changed files with 52 additions and 16 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,6 @@
"@stablelib/ed25519": "^1.0.1",
"@stablelib/random": "^1.0.0",
"@stablelib/sha256": "^1.0.0",
"@stablelib/utf8": "^1.0.0",
"@stablelib/x25519": "^1.0.0",
"@stablelib/xchacha20poly1305": "^1.0.0",
"did-resolver": "^2.1.1",
Expand Down
5 changes: 2 additions & 3 deletions src/NaclSigner.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { sign } from '@stablelib/ed25519'
import { encode } from '@stablelib/utf8'
import { Signer } from './JWT'
import { base64ToBytes, bytesToBase64url } from './util'
import { base64ToBytes, bytesToBase64url, stringToBytes } from './util'

/**
* The NaclSigner returns a configured function for signing data using the Ed25519 algorithm. It also defines
Expand All @@ -22,7 +21,7 @@ import { base64ToBytes, bytesToBase64url } from './util'
function NaclSigner(base64PrivateKey: string): Signer {
const privateKey: Uint8Array = base64ToBytes(base64PrivateKey)
return async data => {
const dataBytes: Uint8Array = encode(data)
const dataBytes: Uint8Array = stringToBytes(data)
const sig: Uint8Array = sign(privateKey, dataBytes)
const b64UrlSig: string = bytesToBase64url(sig)
return b64UrlSig
Expand Down
30 changes: 21 additions & 9 deletions src/VerifierAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@ import { ec as EC } from 'elliptic'
import { sha256, toEthereumAddress } from './Digest'
import { verify } from '@stablelib/ed25519'
import { PublicKey } from 'did-resolver'
import { encode } from '@stablelib/utf8'
import { base64ToBytes, base64urlToBytes, bytesToHex, EcdsaSignature } from './util'
import { hexToBytes, base58ToBytes, base64ToBytes, base64urlToBytes, bytesToHex, EcdsaSignature, stringToBytes } from './util'

const secp256k1 = new EC('secp256k1')

Expand All @@ -22,19 +21,31 @@ export function toSignatureObject(signature: string, recoverable = false): Ecdsa
return sigObj
}

function extractPublicKeyBytes(pk: PublicKey): Uint8Array {
if (pk.publicKeyBase58) {
return base58ToBytes(pk.publicKeyBase58)
} else if (pk.publicKeyBase64) {
return base64ToBytes(pk.publicKeyBase64)
} else if (pk.publicKeyHex) {
return hexToBytes(pk.publicKeyHex)
}
return new Uint8Array()
}

export function verifyES256K(data: string, signature: string, authenticators: PublicKey[]): PublicKey {
const hash: Uint8Array = sha256(data)
const sigObj: EcdsaSignature = toSignatureObject(signature)
const fullPublicKeys = authenticators.filter(({ publicKeyHex }) => {
return typeof publicKeyHex !== 'undefined'
const fullPublicKeys = authenticators.filter(({ ethereumAddress }) => {
return typeof ethereumAddress === 'undefined'
})
const ethAddressKeys = authenticators.filter(({ ethereumAddress }) => {
return typeof ethereumAddress !== 'undefined'
})

let signer: PublicKey = fullPublicKeys.find(({ publicKeyHex }) => {
let signer: PublicKey = fullPublicKeys.find((pk: PublicKey) => {
try {
return secp256k1.keyFromPublic(publicKeyHex, 'hex').verify(hash, sigObj)
const pubBytes = extractPublicKeyBytes(pk)
return secp256k1.keyFromPublic(pubBytes).verify(hash, sigObj)
} catch (err) {
return false
}
Expand Down Expand Up @@ -84,10 +95,11 @@ export function verifyRecoverableES256K(data: string, signature: string, authent
}

export function verifyEd25519(data: string, signature: string, authenticators: PublicKey[]): PublicKey {
const clear: Uint8Array = encode(data)
const clear: Uint8Array = stringToBytes(data)
const sig: Uint8Array = base64urlToBytes(signature)
const signer: PublicKey = authenticators.find(({ publicKeyBase64 }) =>
verify(base64ToBytes(publicKeyBase64), clear, sig)
const signer: PublicKey = authenticators.find((pk: PublicKey) => {
return verify(extractPublicKeyBytes(pk), clear, sig)
}
)
if (!signer) throw new Error('Signature invalid for JWT')
return signer
Expand Down
5 changes: 2 additions & 3 deletions src/__tests__/SignerAlgorithm-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import EllipticSigner from '../EllipticSigner'
import NaclSigner from '../NaclSigner'
import { ec as EC } from 'elliptic'
import nacl from 'tweetnacl'
import { base64ToBytes, base64urlToBytes } from '../util'
import { base64ToBytes, base64urlToBytes, stringToBytes } from '../util'
import { sha256 } from '../Digest'
import { encode } from '@stablelib/utf8'
const secp256k1 = new EC('secp256k1')
const privateKey = '278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a241154cc1d25383f'
const ed25519PrivateKey = 'nlXR4aofRVuLqtn9+XVQNlX4s1nVQvp+TOhBBtYls1IG+sHyIkDP/WN+rWZHGIQp+v2pyct+rkM4asF/YRFQdQ=='
Expand Down Expand Up @@ -123,6 +122,6 @@ describe('Ed25519', () => {

it('can verify the signature', async () => {
const signature = await jwtSigner('hello', edSigner)
expect(nacl.sign.detached.verify(encode('hello'), base64urlToBytes(signature), edKp.publicKey)).toBeTruthy()
expect(nacl.sign.detached.verify(stringToBytes('hello'), base64urlToBytes(signature), edKp.publicKey)).toBeTruthy()
})
})
19 changes: 19 additions & 0 deletions src/__tests__/VerifierAlgorithm-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { toEthereumAddress } from '../Digest'
import nacl from 'tweetnacl'
import { ec as EC } from 'elliptic'
import { base64ToBytes, bytesToBase64 } from '../util'
import * as u8a from 'uint8arrays'

const secp256k1 = new EC('secp256k1')

Expand Down Expand Up @@ -117,6 +118,15 @@ describe('ES256K', () => {
return expect(verifier(parts[1], parts[2], [ecKey1, ecKey2])).toEqual(ecKey2)
})

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 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_-]+)$/)
Expand Down Expand Up @@ -186,6 +196,15 @@ describe('Ed25519', () => {
return expect(verifier(parts[1], parts[2], [edKey, edKey2])).toEqual(edKey)
})

it('validates with publicKeyBase58', async () => {
const jwt = await createJWT({ bla: 'bla' }, { alg: 'Ed25519', issuer: did, signer: edSigner })
const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/)
const publicKeyBase58 = u8a.toString(u8a.fromString(edKey.publicKeyBase64, 'base64pad'), 'base58btc')
const pubkey = Object.assign({ publicKeyBase58 }, edKey)
delete pubkey.publicKeyBase64
return expect(verifier(parts[1], parts[2], [pubkey])).toEqual(pubkey)
})

it('throws error if invalid signature', async () => {
const jwt = await createJWT({ bla: 'bla' }, { alg: 'Ed25519', issuer: did, signer: edSigner })
const parts = jwt.match(/^([a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/)
Expand Down
8 changes: 8 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ export function base58ToBytes(s: string): Uint8Array {
return u8a.fromString(s, 'base58btc')
}

export function hexToBytes(s: string): Uint8Array {
return u8a.fromString(s, 'base16')
}

export function encodeBase64url(s: string): string {
return bytesToBase64url(u8a.fromString(s))
}
Expand All @@ -38,6 +42,10 @@ export function bytesToHex(b: Uint8Array): string {
return u8a.toString(b, 'base16')
}

export function stringToBytes(s: string): Uint8Array {
return u8a.fromString(s)
}

export function toJose({ r, s, recoveryParam }: EcdsaSignature, recoverable?: boolean): string {
const jose = new Uint8Array(recoverable ? 65 : 64)
jose.set(u8a.fromString(r, 'base16'), 0)
Expand Down

0 comments on commit c4ae63a

Please sign in to comment.