Skip to content

Commit

Permalink
feat: add ES256 to JWT verifier (#254)
Browse files Browse the repository at this point in the history
  • Loading branch information
bshambaugh committed Oct 15, 2022
1 parent 86010a6 commit 86a4d23
Show file tree
Hide file tree
Showing 5 changed files with 7,510 additions and 8,050 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,20 @@
"@semantic-release/git": "10.0.1",
"@types/elliptic": "6.4.14",
"@types/jest": "28.1.6",
"@types/jsonwebtoken": "^8.5.9",
"@types/jwk-to-pem": "^2.0.1",
"@typescript-eslint/eslint-plugin": "5.33.0",
"@typescript-eslint/parser": "5.33.0",
"codecov": "3.8.3",
"did-key-creator": "^0.3.2",
"eslint": "8.22.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-jest": "26.8.2",
"eslint-plugin-prettier": "4.2.1",
"jest": "28.1.3",
"jsontokens": "3.1.1",
"jsonwebtoken": "^8.5.1",
"jwk-to-pem": "^2.0.5",
"microbundle": "0.15.1",
"mockdate": "3.0.5",
"prettier": "2.7.1",
Expand Down
1 change: 1 addition & 0 deletions src/JWT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export interface PublicKeyTypes {
}

export const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = {
ES256: ['JsonWebKey2020'],
ES256K: [
'EcdsaSecp256k1VerificationKey2019',
/**
Expand Down
274 changes: 273 additions & 1 deletion src/__tests__/JWT.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ import { EdDSASigner } from '../signers/EdDSASigner'
import { ES256KSigner } from '../signers/ES256KSigner'
import { bytesToBase64url, decodeBase64url } from '../util'

// add declarations for ES256 Tests
import { ES256Signer } from '../signers/ES256Signer'
import * as jwt from 'jsonwebtoken'
import * as u8a from 'uint8arrays'
import * as jwkToPem from 'jwk-to-pem'
import { encodeDIDfromHexString } from 'did-key-creator'

const NOW = 1485321133
MockDate.set(NOW * 1000 + 123)

Expand Down Expand Up @@ -112,6 +119,157 @@ const audDidDoc = {
},
}

describe('createJWT()', () => {
describe('ES256', () => {
const alg = 'ES256'
const privateKey = '736f625c9dda78a94bb16840c82779bb7bc18014b8ede52f0f03429902fc4ba8'
const publicKey = '0314c58e581c7656ba153195669fe4ce53ff78dd5ede60a4039771a90c58cb41de'
const publicKey_x = '14c58e581c7656ba153195669fe4ce53ff78dd5ede60a4039771a90c58cb41de'
const publicKey_y = 'ec41869995bd661849414c523c7dff9a96f1c8dbc2e5e78172118f91c7199869'
// construct did:key for secp256r1 (unlike did for secp256k1 which is for an Ethereum Address)
const multicodecName = 'p256-pub';
const did = encodeDIDfromHexString(multicodecName,publicKey)

const signer = ES256Signer(hexToBytes(privateKey))

interface privateJsonWebKey extends JsonWebKey {
d?: string
}

// verifyTokenFormAndValidity
function verifyTokenFormAndValidity(token: string, pemPublic: string): boolean {
let result = null
try {
jwt.verify(token, pemPublic)
result = true
} catch (e) {
console.error(e.name + ': ' + e.message)
result = false
}
return result
}

// input public key in hex, and export pem
function publicToJWK(
publicPointHex_x: string,
publicPointHex_y: string,
kty_value: string,
crv_value: string
): JsonWebKey {
if (publicPointHex_x.length % 2 != 0) {
publicPointHex_x = '0' + publicPointHex_x
}
if (publicPointHex_y.length % 2 != 0) {
publicPointHex_y = '0' + publicPointHex_y
}
const publicPointUint8_x = u8a.fromString(publicPointHex_x, 'hex')
const publicPointBase64URL_x = u8a.toString(publicPointUint8_x, 'base64url')
const publicPointUint8_y = u8a.fromString(publicPointHex_y, 'hex')
const publicPointBase64URL_y = u8a.toString(publicPointUint8_y, 'base64url')
return {
kty: kty_value,
crv: crv_value,
x: publicPointBase64URL_x,
y: publicPointBase64URL_y,
}
}

// input private key in hex, and export pem
function privateToJWK(privatePointHex: string, kty_value: string, crv_value: string): privateJsonWebKey {
if (privatePointHex.length % 2 != 0) {
privatePointHex = '0' + privatePointHex
}
const privatePointUint8 = u8a.fromString(privatePointHex, 'hex')
const privatePointBase64URL = u8a.toString(privatePointUint8, 'base64url')
return {
kty: kty_value,
crv: crv_value,
d: privatePointBase64URL,
}
}

it('creates a valid JWT', async () => {
expect.assertions(1)
const jwt = await createJWT({ requested: ['name', 'phone'] }, { issuer: did, signer },{alg: 'ES256'})
const pemPublic = jwkToPem.default(publicToJWK(publicKey_x,publicKey_y,'EC','P-256'))
expect(verifyTokenFormAndValidity(jwt,pemPublic)).toBe(true)
})

it('creates a valid JWT using a MNID', async () => {
expect.assertions(1)
const jwt = await createJWT({ requested: ['name', 'phone'] }, { issuer: address, signer },{alg: 'ES256'})
const pemPublic = jwkToPem.default(publicToJWK(publicKey_x,publicKey_y,'EC','P-256'))
expect(verifyTokenFormAndValidity(jwt,pemPublic)).toBe(true)
})

it('creates a JWT with correct format', async () => {
expect.assertions(1)
const jwt = await createJWT({ requested: ['name', 'phone'] }, { issuer: did, signer },{alg: 'ES256'})
return expect(decodeJWT(jwt)).toMatchSnapshot()
})

it('creates a JWT with correct legacy format', async () => {
expect.assertions(1)
const jwt = await createJWT({ requested: ['name', 'phone'] }, { issuer: address, signer },{alg: 'ES256'})
return expect(decodeJWT(jwt)).toMatchSnapshot()
})

it('creates a JWT with expiry in 10000 seconds', async () => {
expect.assertions(1)
const jwt = await createJWT(
{
requested: ['name', 'phone'],
nbf: Math.floor(new Date().getTime() / 1000),
},
{ issuer: did, signer, expiresIn: 10000 },
{alg: 'ES256'}
)
const { payload } = decodeJWT(jwt)
return expect(payload.exp).toEqual(payload.nbf + 10000)
})

it('Uses iat if nbf is not defined but expiresIn is included', async () => {
expect.assertions(1)
const { payload } = decodeJWT(
await createJWT({ requested: ['name', 'phone'] }, { issuer: did, signer, expiresIn: 10000 },{alg: 'ES256'})
)
return expect(payload.exp).toEqual(payload.iat + 10000)
})

it('sets iat to the current time by default', async () => {
expect.assertions(1)
const timestamp = Math.floor(Date.now() / 1000)
const { payload } = decodeJWT(await createJWT({ requested: ['name', 'phone'] }, { issuer: did, signer },{alg: 'ES256'}))
return expect(payload.iat).toEqual(timestamp)
})

it('sets iat to the value passed in payload', async () => {
expect.assertions(1)
const timestamp = 2000000
const { payload } = decodeJWT(
await createJWT({ requested: ['name', 'phone'], iat: timestamp }, { issuer: did, signer },{alg: 'ES256'})
)
return expect(payload.iat).toEqual(timestamp)
})

it('does not set iat if value in payload is undefined', async () => {
expect.assertions(1)
const { payload } = decodeJWT(
await createJWT({ requested: ['name', 'phone'], iat: undefined }, { issuer: did, signer },{alg: 'ES256'})
)
return expect(payload.iat).toBeUndefined()
})

it('throws an error if unsupported algorithm is passed in', async () => {
expect.assertions(1)
await expect(
createJWT({ requested: ['name', 'phone'] }, { issuer: did, signer, alg: 'BADALGO' })
).rejects.toThrowError('Unsupported algorithm BADALGO')
})

})
})

describe('createJWT()', () => {
describe('ES256K', () => {
it('creates a valid JWT', async () => {
Expand Down Expand Up @@ -275,7 +433,121 @@ describe('createJWT()', () => {
})
})

describe('verifyJWT()', () => {
describe('verifyJWT() for ES256', () => {

const alg = 'ES256'
const privateKey = '736f625c9dda78a94bb16840c82779bb7bc18014b8ede52f0f03429902fc4ba8'
const publicKey = '0314c58e581c7656ba153195669fe4ce53ff78dd5ede60a4039771a90c58cb41de'
const publicKey_x = '14c58e581c7656ba153195669fe4ce53ff78dd5ede60a4039771a90c58cb41de'
const publicKey_y = 'ec41869995bd661849414c523c7dff9a96f1c8dbc2e5e78172118f91c7199869'
// construct did:key for secp256r1 (unlike did for secp256k1 which is for an Ethereum Address)
const multicodecName = 'p256-pub';
const did = encodeDIDfromHexString(multicodecName,publicKey)

const didDoc = {
didDocument: {
'@context': 'https://w3id.org/did/v1',
id: did,
verificationMethod: [
{
id: `${did}#keys-1`,
type: 'JsonWebKey2020',
controller: did,
publicKeyHex: publicKey,
},
],
authentication: [`${did}#keys-1`],
assertionMethod: [`${did}#keys-1`],
capabilityInvocation: [`${did}#keys-1`],
capabilityDelegation: [`${did}#some-key-that-does-not-exist`],
},
}

const resolver = {
resolve: jest.fn().mockImplementation((didUrl: string) => {
if (didUrl.includes(did)) {
return {
didDocument: didDoc.didDocument,
didDocumentMetadata: {},
didResolutionMetadata: { contentType: 'application/did+ld+json' },
}
}

return {
didDocument: null,
didDocumentMetadata: {},
didResolutionMetadata: {
error: 'notFound',
message: 'resolver_error: DID document not found',
},
}
}),
}

describe('pregenerated JWT', () => {
const incomingJwt =
'eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0ODUzMjExMzMsInJlcXVlc3RlZCI6WyJuYW1lIiwicGhvbmUiXSwiaXNzIjoiZGlkOmtleTp6RG5hZWo0TkhudGRhNHJOVzRGQlVKZ0Z6ZGNnRUFYS0dSVkdFOEx1VmZSYnVNdWMxIn0.aMYFY0jitx2Bq9_wGBhEeIyVvzr2XkouCyEP662P8TbAPTpXOC3UrGQONaPD7wleLrMhGdvfod7idSxKXLl64Q'
it('verifies the JWT and return correct payload', async () => {
expect.assertions(1)
const { payload } = await verifyJWT(incomingJwt, { resolver })
return expect(payload).toMatchSnapshot()
})
it('verifies the JWT and return correct profile', async () => {
expect.assertions(1)
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 () => {
expect.assertions(1)
const { issuer } = await verifyJWT(incomingJwt, { resolver })
return expect(issuer).toEqual(did)
})
it('verifies the JWT and return correct signer', async () => {
expect.assertions(1)
const { signer } = await verifyJWT(incomingJwt, { resolver })
return expect(signer).toEqual(didDoc.didDocument.verificationMethod[0])
})
it('verifies the JWT requiring authentication and return correct signer', async () => {
expect.assertions(1)
const { signer } = await verifyJWT(incomingJwt, { resolver, auth: true })
return expect(signer).toEqual(didDoc.didDocument.verificationMethod[0])
})
it('verifies the JWT requiring authentication proofPurpose and return correct signer', async () => {
expect.assertions(1)
const { signer } = await verifyJWT(incomingJwt, { resolver, proofPurpose: 'authentication' })
return expect(signer).toEqual(didDoc.didDocument.verificationMethod[0])
})
it('verifies the JWT requiring assertionMethod and return correct signer', async () => {
expect.assertions(1)
const { signer } = await verifyJWT(incomingJwt, { resolver, proofPurpose: 'assertionMethod' })
return expect(signer).toEqual(didDoc.didDocument.verificationMethod[0])
})
it('verifies the JWT requiring capabilityInvocation and return correct signer', async () => {
expect.assertions(1)
const { signer } = await verifyJWT(incomingJwt, { resolver, proofPurpose: 'capabilityInvocation' })
return expect(signer).toEqual(didDoc.didDocument.verificationMethod[0])
})
it('rejects the JWT requiring capabilityDelegation when not present in document', async () => {
expect.assertions(1)
await expect(() =>
verifyJWT(incomingJwt, { resolver, proofPurpose: 'capabilityDelegation' })
).rejects.toThrowError(
`DID document for ${did} does not have public keys suitable for ES256 with capabilityDelegation purpose`
)
})
it('rejects the JWT requiring unknown proofPurpose', async () => {
expect.assertions(1)
await expect(() => verifyJWT(incomingJwt, { resolver, proofPurpose: 'impossible' })).rejects.toThrowError(
`DID document for ${did} does not have public keys suitable for ES256 with impossible purpose`
)
})
})
})


describe('verifyJWT() for ES256K', () => {
const resolver = {
resolve: jest.fn().mockImplementation((didUrl: string) => {
if (didUrl.includes(did)) {
Expand Down
Loading

0 comments on commit 86a4d23

Please sign in to comment.