Skip to content

Commit

Permalink
feat: add option to canonicalize JSON payloads (#161)
Browse files Browse the repository at this point in the history
Use canonicalize package and make canonicalization optional
  • Loading branch information
zbarbuto authored May 18, 2021
1 parent 10ace31 commit 4cfd3ee
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 22 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"@stablelib/sha256": "^1.0.0",
"@stablelib/x25519": "^1.0.0",
"@stablelib/xchacha20poly1305": "^1.0.0",
"canonicalize": "^1.0.5",
"did-resolver": "^3.1.0",
"elliptic": "^6.5.4",
"js-sha3": "^0.8.0",
Expand Down
48 changes: 30 additions & 18 deletions src/JWT.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import VerifierAlgorithm from './VerifierAlgorithm'
import canonicalizeData from 'canonicalize'
import type { DIDDocument, DIDResolutionResult, Resolvable, VerificationMethod } from 'did-resolver'
import SignerAlg from './SignerAlgorithm'
import { encodeBase64url, decodeBase64url, EcdsaSignature } from './util'
import type { Resolvable, VerificationMethod, DIDResolutionResult, DIDDocument } from 'did-resolver'
import { decodeBase64url, EcdsaSignature, encodeBase64url } from './util'
import VerifierAlgorithm from './VerifierAlgorithm'

export type Signer = (data: string | Uint8Array) => Promise<EcdsaSignature | string>
export type SignerAlgorithm = (payload: string, signer: Signer) => Promise<string>
Expand All @@ -14,6 +15,7 @@ export interface JWTOptions {
*/
alg?: string
expiresIn?: number
canonicalize?: boolean
}

export interface JWTVerifyOptions {
Expand All @@ -27,6 +29,10 @@ export interface JWTVerifyOptions {
proofPurpose?: 'authentication' | 'assertionMethod' | 'capabilityDelegation' | 'capabilityInvocation' | string
}

export interface JWSCreationOptions {
canonicalize?: boolean
}

export interface DIDAuthenticator {
authenticators: VerificationMethod[]
issuer: string
Expand Down Expand Up @@ -122,8 +128,12 @@ export const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = {
const defaultAlg = 'ES256K'
const DID_JSON = 'application/did+json'

function encodeSection(data: any): string {
return encodeBase64url(JSON.stringify(data))
function encodeSection(data: any, shouldCanonicalize: boolean = false): string {
if (shouldCanonicalize) {
return encodeBase64url(JSON.stringify(canonicalizeData(data)))
} else {
return encodeBase64url(JSON.stringify(data))
}
}

export const NBF_SKEW: number = 300
Expand Down Expand Up @@ -178,11 +188,12 @@ export function decodeJWT(jwt: string): JWTDecoded {
export async function createJWS(
payload: string | any,
signer: Signer,
header: Partial<JWTHeader> = {}
header: Partial<JWTHeader> = {},
options: JWSCreationOptions = {}
): Promise<string> {
if (!header.alg) header.alg = defaultAlg
const encodedPayload = typeof payload === 'string' ? payload : encodeSection(payload)
const signingInput: string = [encodeSection(header), encodedPayload].join('.')
const encodedPayload = typeof payload === 'string' ? payload : encodeSection(payload, options.canonicalize)
const signingInput: string = [encodeSection(header, options.canonicalize), encodedPayload].join('.')

const jwtSigner: SignerAlgorithm = SignerAlg(header.alg)
const signature: string = await jwtSigner(signingInput, signer)
Expand All @@ -198,18 +209,19 @@ export async function createJWS(
* ...
* })
*
* @param {Object} payload payload object
* @param {Object} [options] an unsigned credential object
* @param {String} options.issuer The DID of the issuer (signer) of JWT
* @param {String} options.alg [DEPRECATED] The JWT signing algorithm to use. Supports: [ES256K, ES256K-R, Ed25519, EdDSA], Defaults to: ES256K.
* Please use `header.alg` to specify the algorithm
* @param {Signer} options.signer a `Signer` function, Please see `ES256KSigner` or `EdDSASigner`
* @param {Object} header optional object to specify or customize the JWT header
* @return {Promise<Object, Error>} a promise which resolves with a signed JSON Web Token or rejects with an error
* @param {Object} payload payload object
* @param {Object} [options] an unsigned credential object
* @param {String} options.issuer The DID of the issuer (signer) of JWT
* @param {String} options.alg [DEPRECATED] The JWT signing algorithm to use. Supports: [ES256K, ES256K-R, Ed25519, EdDSA], Defaults to: ES256K.
* Please use `header.alg` to specify the algorithm
* @param {Signer} options.signer a `Signer` function, Please see `ES256KSigner` or `EdDSASigner`
* @param {boolean} options.canonicalize optional flag to canonicalize header and payload before signing
* @param {Object} header optional object to specify or customize the JWT header
* @return {Promise<Object, Error>} a promise which resolves with a signed JSON Web Token or rejects with an error
*/
export async function createJWT(
payload: any,
{ issuer, signer, alg, expiresIn }: JWTOptions,
{ issuer, signer, alg, expiresIn, canonicalize }: JWTOptions,
header: Partial<JWTHeader> = {}
): Promise<string> {
if (!signer) throw new Error('No Signer functionality has been configured')
Expand All @@ -228,7 +240,7 @@ export async function createJWT(
}
}
const fullPayload = { ...timestamps, ...payload, iss: issuer }
return createJWS(fullPayload, signer, header)
return createJWS(fullPayload, signer, header, { canonicalize })
}

function verifyJWSDecoded(
Expand Down
44 changes: 40 additions & 4 deletions src/__tests__/JWT.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createJWT, verifyJWT, decodeJWT, createJWS, verifyJWS, resolveAuthenticator, NBF_SKEW } from '../JWT'
import { VerificationMethod } from 'did-resolver'
import { TokenVerifier } from 'jsontokens'
import { bytesToBase64url, decodeBase64url } from '../util'
import MockDate from 'mockdate'
import { VerificationMethod, Resolver } from 'did-resolver'
import { ES256KSigner } from '../signers/ES256KSigner'
import { createJWS, createJWT, decodeJWT, NBF_SKEW, resolveAuthenticator, verifyJWS, verifyJWT } from '../JWT'
import { EdDSASigner } from '../signers/EdDSASigner'
import { ES256KSigner } from '../signers/ES256KSigner'
import { bytesToBase64url, decodeBase64url } from '../util'

const NOW = 1485321133
MockDate.set(NOW * 1000 + 123)
Expand Down Expand Up @@ -173,6 +173,34 @@ describe('createJWT()', () => {
})
})

it('can create a jwt in the default non-canonical way', async () => {
expect.assertions(1)
// Same payload, slightly different ordering
const jwtA = await createJWT(
{ reason: 'verification', requested: ['name', 'phone'] },
{ alg, issuer: did, signer }
)
const jwtB = await createJWT(
{ requested: ['name', 'phone'], reason: 'verification' },
{ alg, issuer: did, signer }
)
expect(jwtA).not.toEqual(jwtB)
})

it('can create a jwt in a canonical way', async () => {
expect.assertions(1)
// Same payload, slightly different ordering
const jwtA = await createJWT(
{ reason: 'verification', requested: ['name', 'phone'] },
{ alg, issuer: did, signer, canonicalize: true }
)
const jwtB = await createJWT(
{ requested: ['name', 'phone'], reason: 'verification' },
{ alg, issuer: did, signer, canonicalize: true }
)
expect(jwtA).toEqual(jwtB)
})

it('creates a JWT with correct format', async () => {
expect.assertions(1)
const jwt = await createJWT({ requested: ['name', 'phone'] }, { alg, issuer: did, signer })
Expand Down Expand Up @@ -522,6 +550,14 @@ describe('JWS', () => {
expect(JSON.parse(decodeBase64url(jws.split('.')[1]))).toEqual(payload)
})

it('createJWS can canonicalize a JSON payload', async () => {
expect.assertions(2)
const payload = { z: 'z', a: 'a' }
const jws = await createJWS(payload, signer, {}, { canonicalize: true })
expect(jws).toMatchSnapshot()
expect(JSON.parse(decodeBase64url(jws.split('.')[1]))).toEqual(JSON.stringify({ a: 'a', z: 'z' }))
})

it('createJWS works with base64url payload', async () => {
expect.assertions(2)
// use the hex public key as an arbitrary payload
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/__snapshots__/JWT.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`JWS createJWS can canonicalize a JSON payload 1`] = `"IntcImFsZ1wiOlwiRVMyNTZLXCJ9Ig.IntcImFcIjpcImFcIixcInpcIjpcInpcIn0i.aKyOmGo5Dx2VsvDZAAHMxNtgqBeI0-qij4OB2AsISFdGsnNDJNEptZkQjY497mzX1LdMPS84pzykqalPtMwqMQ"`;

exports[`JWS createJWS works with JSON payload 1`] = `"eyJhbGciOiJFUzI1NksifQ.eyJzb21lIjoiZGF0YSJ9.dblNz-7BVLknOFIBPmt5VTG0MDls_Q69WI8OfQuqNdUp4y50-b8Ubn0xujm1ijfmfqRunpks5TyWqgMsQkR_GQ"`;

exports[`JWS createJWS works with base64url payload 1`] = `"eyJhbGciOiJFUzI1NksifQ.A_3Vet7D1DjqI3_kazPuHgFu2mtYXD4n6mZobC6lNYR5.n5ZZQZe1J7e76TGTLBpQO2R22JFoHDBi5ScfoxHz__Qy7Q6r3R11GdXmY_0ntFx6nC9QbDD19y8tTDMLUM4DAw"`;
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3287,6 +3287,11 @@ caniuse-lite@^1.0.30001165:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001170.tgz#0088bfecc6a14694969e391cc29d7eb6362ca6a7"
integrity sha512-Dd4d/+0tsK0UNLrZs3CvNukqalnVTRrxb5mcQm8rHL49t7V5ZaTygwXkrq+FB+dVDf++4ri8eJnFEJAB8332PA==

canonicalize@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/canonicalize/-/canonicalize-1.0.5.tgz#b43b390ce981d397908bb847c3a8d9614323a47b"
integrity sha512-mAjKJPIyP0xqqv6IAkvso07StOmz6cmGtNDg3pXCSzXVZOqka7StIkAhJl/zHOi4M2CgpYfD6aeRWbnrmtvBEA==

capture-exit@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/capture-exit/-/capture-exit-2.0.0.tgz#fb953bfaebeb781f62898239dabb426d08a509a4"
Expand Down

0 comments on commit 4cfd3ee

Please sign in to comment.