Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adapt to did core spec #156

Merged
merged 10 commits into from
Mar 24, 2021
17 changes: 15 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,27 @@
"request": "launch",
"name": "Jest All",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand", "--coverage=false", "JWT-test"],
"args": ["--runInBand", "--coverage=false"],
"sourceMaps": true,
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
},
"resolveSourceMapLocations": ["${workspaceFolder}/**", "!**/node_modules/typescript/lib/typescript.js.map"],
"runtimeArgs": ["--preserve-symlinks"]
}
},
{
"type": "node",
"request": "launch",
"name": "Jest Current File",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["${relativeFile}", "--detectOpenHandles", "--runInBand", "--coverage=false"],
"sourceMaps": true,
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
}
},
]
}
1 change: 0 additions & 1 deletion _config.yml

This file was deleted.

3 changes: 3 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
presets: [['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript']
}
Empty file removed docs/reference/index.md
Empty file.
17 changes: 0 additions & 17 deletions js2doc.conf

This file was deleted.

23 changes: 6 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,31 +34,20 @@
],
"license": "Apache-2.0",
"jest": {
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json"
],
"coverageDirectory": "./coverage/",
"collectCoverage": true,
"clearMocks": true,
"collectCoverageFrom": [
"src/**/*.{ts,tsx}",
"!src/**/*.d.ts",
"!**/node_modules/**",
",!src/**/*.d.ts",
",!**/node_modules/**",
"!src/**/index.ts"
],
"testEnvironment": "node",
"unmockedModulePathPatterns": [
"<rootDir>/node_modules/nock"
"testMatch": [
"**/__tests__/**/*.test.[jt]s"
]
},
"devDependencies": {
"@babel/preset-typescript": "^7.13.0",
"@semantic-release/changelog": "5.0.1",
"@semantic-release/git": "9.0.0",
"@types/elliptic": "6.4.12",
Expand Down
78 changes: 54 additions & 24 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 SignerAlg from './SignerAlgorithm'
import { encodeBase64url, decodeBase64url, EcdsaSignature } from './util'
import type { Resolver, VerificationMethod, DIDResolutionResult } from 'did-resolver'
import type { Resolver, VerificationMethod, DIDResolutionResult, DIDDocument } from 'did-resolver'

export type Signer = (data: string | Uint8Array) => Promise<EcdsaSignature | string>
export type SignerAlgorithm = (payload: string, signer: Signer) => Promise<string>
Expand All @@ -17,11 +17,14 @@ export interface JWTOptions {
}

export interface JWTVerifyOptions {
/** @deprecated Please use `proofPurpose: 'authentication' instead` */
auth?: boolean
audience?: string
callbackUrl?: string
resolver?: Resolver
skewTime?: number
/** See https://www.w3.org/TR/did-spec-registries/#verification-relationships */
proofPurpose?: 'authentication' | 'assertionMethod' | 'capabilityDelegation' | 'capabilityInvocation' | string
}

export interface DIDAuthenticator {
Expand Down Expand Up @@ -181,7 +184,7 @@ export async function createJWS(
const encodedPayload = typeof payload === 'string' ? payload : encodeSection(payload)
const signingInput: string = [encodeSection(header), encodedPayload].join('.')

const jwtSigner: SignerAlgorithm = SignerAlgorithm(header.alg)
const jwtSigner: SignerAlgorithm = SignerAlg(header.alg)
const signature: string = await jwtSigner(signingInput, signer)
return [signingInput, signature].join('.')
}
Expand Down Expand Up @@ -281,16 +284,22 @@ export async function verifyJWT(
auth: null,
audience: null,
callbackUrl: null,
skewTime: null
skewTime: null,
proofPurpose: null
}
): Promise<JWTVerified> {
if (!options.resolver) throw new Error('No DID resolver has been configured')
const { payload, header, signature, data }: JWTDecoded = decodeJWT(jwt)
const proofPurpose: string | undefined = options.hasOwnProperty('auth')
? options.auth
? 'authentication'
: undefined
: options.proofPurpose
const { didResolutionResult, authenticators, issuer }: DIDAuthenticator = await resolveAuthenticator(
options.resolver,
header.alg,
payload.iss,
options.auth
proofPurpose
)
const signer: VerificationMethod = await verifyJWSDecoded({ header, data, signature } as JWSDecoded, authenticators)
const now: number = Math.floor(Date.now() / 1000)
Expand Down Expand Up @@ -336,21 +345,34 @@ export async function verifyJWT(
* @param {String} alg a JWT algorithm
* @param {String} did a Decentralized IDentifier (DID) to lookup
* @param {Boolean} auth Restrict public keys to ones specifically listed in the 'authentication' section of DID document
* @return {Promise<Object, Error>} a promise which resolves with a response object containing an array of authenticators or if non exist rejects with an error
* @return {Promise<DIDAuthenticator>} a promise which resolves with a response object containing an array of authenticators or if non exist rejects with an error
*/
export async function resolveAuthenticator(
resolver: Resolver,
alg: string,
issuer: string,
auth?: boolean
proofPurpose?: string
): Promise<DIDAuthenticator> {
const types: string[] = SUPPORTED_PUBLIC_KEY_TYPES[alg]
if (!types || types.length === 0) {
throw new Error(`No supported signature types for algorithm ${alg}`)
}
const result: DIDResolutionResult = await resolver.resolve(issuer, { accept: DID_JSON })
if (result.didResolutionMetadata?.error) {
const { error, message } = result.didResolutionMetadata
let didResult: DIDResolutionResult

const result = (await resolver.resolve(issuer, { accept: DID_JSON })) as unknown
// support legacy resolvers that do not produce DIDResolutionResult
if (Object.getOwnPropertyNames(result).indexOf('didDocument') === -1) {
didResult = {
didDocument: result as DIDDocument,
didDocumentMetadata: {},
didResolutionMetadata: { contentType: DID_JSON }
}
} else {
didResult = result as DIDResolutionResult
}

if (didResult.didResolutionMetadata?.error) {
const { error, message } = didResult.didResolutionMetadata
throw new Error(`Unable to resolve DID document for ${issuer}: ${error}, ${message || ''}`)
}

Expand All @@ -359,19 +381,25 @@ export async function resolveAuthenticator(
return filtered.length > 0 ? filtered[0] : null
}

let publicKeysToCheck: VerificationMethod[] = []
if (result.didDocument.verificationMethod) publicKeysToCheck.push(...result.didDocument.verificationMethod)
if (result.didDocument.publicKey) publicKeysToCheck.push(...result.didDocument.publicKey)
if (auth) {
publicKeysToCheck = (result.didDocument.authentication || [])
.map((authEntry) => {
if (typeof authEntry === 'string') {
return getPublicKeyById(publicKeysToCheck, authEntry)
} else if (typeof (<any>authEntry).publicKey === 'string') {
let publicKeysToCheck: VerificationMethod[] = [
...(didResult?.didDocument?.verificationMethod || []),
...(didResult?.didDocument?.publicKey || [])
]
if (typeof proofPurpose === 'string') {
// support legacy DID Documents that do not list assertionMethod
if (proofPurpose.startsWith('assertion') && !didResult.didDocument.hasOwnProperty('assertionMethod')) {
didResult.didDocument.assertionMethod = [...publicKeysToCheck.map((pk) => pk.id)]
}

publicKeysToCheck = (didResult.didDocument[proofPurpose] || [])
.map((verificationMethod) => {
if (typeof verificationMethod === 'string') {
return getPublicKeyById(publicKeysToCheck, verificationMethod)
} else if (typeof (<any>verificationMethod).publicKey === 'string') {
// this is a legacy format
return getPublicKeyById(publicKeysToCheck, (<any>authEntry).publicKey)
return getPublicKeyById(publicKeysToCheck, (<any>verificationMethod).publicKey)
} else {
return <VerificationMethod>authEntry
return <VerificationMethod>verificationMethod
}
})
.filter((key) => key != null)
Expand All @@ -381,11 +409,13 @@ export async function resolveAuthenticator(
types.find((supported) => supported === type)
)

if (auth && (!authenticators || authenticators.length === 0)) {
throw new Error(`DID document for ${issuer} does not have public keys suitable for authenticating user`)
if (typeof proofPurpose === 'string' && (!authenticators || authenticators.length === 0)) {
throw new Error(
`DID document for ${issuer} does not have public keys suitable for ${alg} with ${proofPurpose} purpose`
)
}
if (!authenticators || authenticators.length === 0) {
throw new Error(`DID document for ${issuer} does not have public keys for ${alg}`)
}
return { authenticators, issuer, didResolutionResult: result }
return { authenticators, issuer, didResolutionResult: didResult }
}
4 changes: 2 additions & 2 deletions src/SignerAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@ const algorithms: SignerAlgorithms = {
EdDSA: Ed25519SignerAlg()
}

function SignerAlgorithm(alg: string): SignerAlgorithm {
function SignerAlg(alg: string): SignerAlgorithm {
const impl: SignerAlgorithm = algorithms[alg]
if (!impl) throw new Error(`Unsupported algorithm ${alg}`)
return impl
}

export default SignerAlgorithm
export default SignerAlg
11 changes: 6 additions & 5 deletions src/VerifierAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ export function verifyES256K(
): VerificationMethod {
const hash: Uint8Array = sha256(data)
const sigObj: EcdsaSignature = toSignatureObject(signature)
const fullPublicKeys = authenticators.filter(({ ethereumAddress }) => {
return typeof ethereumAddress === 'undefined'
const fullPublicKeys = authenticators.filter(({ ethereumAddress, blockchainAccountId }) => {
return typeof ethereumAddress === 'undefined' && typeof blockchainAccountId === 'undefined'
})
const ethAddressKeys = authenticators.filter(({ ethereumAddress }) => {
return typeof ethereumAddress !== 'undefined'
const ethAddressKeys = authenticators.filter(({ ethereumAddress, blockchainAccountId }) => {
return typeof ethereumAddress !== 'undefined' || typeof blockchainAccountId !== undefined
})

let signer: VerificationMethod = fullPublicKeys.find((pk: VerificationMethod) => {
Expand Down Expand Up @@ -95,7 +95,8 @@ export function verifyRecoverableES256K(
return (
keyHex === recoveredPublicKeyHex ||
keyHex === recoveredCompressedPublicKeyHex ||
pk.ethereumAddress === recoveredAddress
pk.ethereumAddress?.toLowerCase() === recoveredAddress ||
pk.blockchainAccountId?.split('@eip155')?.[0].toLowerCase() === recoveredAddress
)
})

Expand Down
35 changes: 26 additions & 9 deletions src/__tests__/ES256KSigner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,65 @@ import { ES256KSigner } from '../signers/ES256KSigner'

describe('Secp256k1 Signer', () => {
it('signs data, given a hex private key', async () => {
expect.assertions(1)
const privateKey = '278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'
const signer = ES256KSigner(privateKey)
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
return await expect(signer(plaintext)).resolves.toMatchSnapshot()
await expect(signer(plaintext)).resolves.toEqual(
'jsvdLwqr-O206hkegoq6pbo7LJjCaflEKHCvfohBP9XJ4C7mG2TPL9YjyKEpYSXqqkUrfRoCxQecHR11Uh7POw'
)
})

it('signs data: privateKey with 0x prefix', async () => {
expect.assertions(1)
const privateKey = '0x278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'
const signer2 = ES256KSigner(privateKey)
const signer = ES256KSigner(privateKey)
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
return await expect(signer2(plaintext)).resolves.toMatchSnapshot()
await expect(signer(plaintext)).resolves.toEqual(
'jsvdLwqr-O206hkegoq6pbo7LJjCaflEKHCvfohBP9XJ4C7mG2TPL9YjyKEpYSXqqkUrfRoCxQecHR11Uh7POw'
)
})

it('signs data: privateKey base58', async () => {
expect.assertions(1)
const privateKey = '3fMGokRKc5yGVqbCXyGNTrp3vP1cXs86tsVSVwzhNvXQ'
const signer2 = ES256KSigner(privateKey)
const signer = ES256KSigner(privateKey)
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
return await expect(signer2(plaintext)).resolves.toMatchSnapshot()
await expect(signer(plaintext)).resolves.toEqual(
'jsvdLwqr-O206hkegoq6pbo7LJjCaflEKHCvfohBP9XJ4C7mG2TPL9YjyKEpYSXqqkUrfRoCxQecHR11Uh7POw'
)
})

it('signs data: privateKey base64url', async () => {
expect.assertions(1)
const privateKey = 'J4pd5wDin6ro5A42bsUBK17GPTbsd-iiQXFUzB0lOD8'
const signer2 = ES256KSigner(privateKey)
const signer = ES256KSigner(privateKey)
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
return await expect(signer2(plaintext)).resolves.toMatchSnapshot()
await expect(signer(plaintext)).resolves.toEqual(
'jsvdLwqr-O206hkegoq6pbo7LJjCaflEKHCvfohBP9XJ4C7mG2TPL9YjyKEpYSXqqkUrfRoCxQecHR11Uh7POw'
)
})

it('signs data: privateKey base64', async () => {
expect.assertions(1)
const privateKey = 'J4pd5wDin6ro5A42bsUBK17GPTbsd+iiQXFUzB0lOD8='
const signer2 = ES256KSigner(privateKey)
const signer = ES256KSigner(privateKey)
const plaintext = 'thequickbrownfoxjumpedoverthelazyprogrammer'
return await expect(signer2(plaintext)).resolves.toMatchSnapshot()
await expect(signer(plaintext)).resolves.toEqual(
'jsvdLwqr-O206hkegoq6pbo7LJjCaflEKHCvfohBP9XJ4C7mG2TPL9YjyKEpYSXqqkUrfRoCxQecHR11Uh7POw'
)
})

it('refuses wrong key size (too short)', async () => {
expect.assertions(1)
const privateKey = '278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d2538'
expect(() => {
ES256KSigner(privateKey)
}).toThrowError(/^Invalid private key format.*/)
})

it('refuses wrong key size (double)', async () => {
expect.assertions(1)
const privateKey =
'278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f278a5de700e29faae8e40e366ec5012b5ec63d36ec77e8a2417154cc1d25383f'
expect(() => {
Expand Down
Loading