Skip to content

Commit

Permalink
fix: add explicit support for EcdsaSecp256k1RecoveryMethod2020 (#153)
Browse files Browse the repository at this point in the history
  • Loading branch information
mirceanis authored Mar 11, 2021
1 parent f00fe76 commit 2b04c34
Show file tree
Hide file tree
Showing 16 changed files with 231 additions and 124 deletions.
4 changes: 2 additions & 2 deletions src/Digest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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]))
}
27 changes: 14 additions & 13 deletions src/JWE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,32 +41,28 @@ export interface Encrypter {
export interface Decrypter {
alg: string
enc: string
decrypt: (sealed: Uint8Array, iv: Uint8Array, aad?: Uint8Array, recipient?: Record<string, any>) => Promise<Uint8Array>
decrypt: (
sealed: Uint8Array,
iv: Uint8Array,
aad?: Uint8Array,
recipient?: Record<string, any>
) => Promise<Uint8Array>
}

function validateJWE(jwe: JWE) {
if (!(jwe.protected && jwe.iv && jwe.ciphertext && jwe.tag)) {
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')
}
})
}
}

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),
Expand All @@ -78,7 +74,12 @@ function encodeJWE(
return jwe
}

export async function createJWE(cleartext: Uint8Array, encrypters: Encrypter[], protectedHeader = {}, aad?: Uint8Array): Promise<JWE> {
export async function createJWE(
cleartext: Uint8Array,
encrypters: Encrypter[],
protectedHeader = {},
aad?: Uint8Array
): Promise<JWE> {
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)
Expand Down
41 changes: 35 additions & 6 deletions src/JWT.ts
Original file line number Diff line number Diff line change
@@ -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<EcdsaSignature | string>
export type SignerAlgorithm = (payload: string, signer: Signer) => Promise<string>
Expand Down Expand Up @@ -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']
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/SignerAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
37 changes: 25 additions & 12 deletions src/VerifierAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand All @@ -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)]
Expand All @@ -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
}
Expand Down
25 changes: 10 additions & 15 deletions src/__tests__/JWE-test.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -64,22 +59,22 @@ 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)
})

it('Creates with aad', async () => {
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')
Expand All @@ -101,22 +96,22 @@ 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)
})

it('Creates with aad', async () => {
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')
Expand All @@ -142,15 +137,15 @@ 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)
})

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)
})
Expand All @@ -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
Expand Down
Loading

0 comments on commit 2b04c34

Please sign in to comment.