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!: Upgrade did-resolver to v3 #151

Merged
merged 3 commits into from
Mar 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@
"@stablelib/sha256": "^1.0.0",
"@stablelib/x25519": "^1.0.0",
"@stablelib/xchacha20poly1305": "^1.0.0",
"did-resolver": "^2.1.2",
"did-resolver": "^3.0.1",
"elliptic": "^6.5.3",
"js-sha3": "^0.8.0",
"uint8arrays": "^2.0.0"
Expand Down
63 changes: 33 additions & 30 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 { DIDDocument, PublicKey, Authentication } from 'did-resolver'
import type { Resolver, DIDDocument, VerificationMethod, DIDResolutionResult } from 'did-resolver'

export type Signer = (data: string | Uint8Array) => Promise<EcdsaSignature | string>
export type SignerAlgorithm = (payload: string, signer: Signer) => Promise<string>
Expand All @@ -16,22 +16,18 @@ export interface JWTOptions {
expiresIn?: number
}

export interface Resolvable {
mirceanis marked this conversation as resolved.
Show resolved Hide resolved
resolve: (did: string) => Promise<DIDDocument | null>
}

export interface JWTVerifyOptions {
mirceanis marked this conversation as resolved.
Show resolved Hide resolved
auth?: boolean
audience?: string
callbackUrl?: string
resolver?: Resolvable
resolver?: Resolver
skewTime?: number
}

export interface DIDAuthenticator {
authenticators: PublicKey[]
authenticators: VerificationMethod[]
issuer: string
doc: DIDDocument
didResolutionResult: DIDResolutionResult
}

export interface JWTHeader {
Expand Down Expand Up @@ -68,7 +64,7 @@ export interface JWSDecoded {

export interface JWTVerified {
payload: any
doc: DIDDocument
didResolutionResult: DIDResolutionResult
issuer: string
signer: object
jwt: string
Expand All @@ -95,6 +91,7 @@ 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))
Expand Down Expand Up @@ -205,9 +202,9 @@ export async function createJWT(
return createJWS(fullPayload, signer, header)
}

function verifyJWSDecoded({ header, data, signature }: JWSDecoded, pubkeys: PublicKey | PublicKey[]): PublicKey {
function verifyJWSDecoded({ header, data, signature }: JWSDecoded, pubkeys: VerificationMethod | VerificationMethod[]): VerificationMethod {
if (!Array.isArray(pubkeys)) pubkeys = [pubkeys]
const signer: PublicKey = VerifierAlgorithm(header.alg)(data, signature, pubkeys)
const signer: VerificationMethod = VerifierAlgorithm(header.alg)(data, signature, pubkeys)
return signer
}

Expand All @@ -219,10 +216,10 @@ function verifyJWSDecoded({ header, data, signature }: JWSDecoded, pubkeys: Publ
* const pubkey = verifyJWT('eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NksifQ.eyJyZXF1Z....', { publicKeyHex: '0x12341...' })
*
* @param {String} jws A JWS string to verify
* @param {Array<PublicKey> | PublicKey} pubkeys The public keys used to verify the JWS
* @return {PublicKey} The public key used to sign the JWS
* @param {Array<VerificationMethod> | VerificationMethod} pubkeys The public keys used to verify the JWS
* @return {VerificationMethod} The public key used to sign the JWS
*/
export function verifyJWS(jws: string, pubkeys: PublicKey | PublicKey[]): PublicKey {
export function verifyJWS(jws: string, pubkeys: VerificationMethod | VerificationMethod[]): VerificationMethod {
const jwsDecoded: JWSDecoded = decodeJWS(jws)
return verifyJWSDecoded(jwsDecoded, pubkeys)
}
Expand Down Expand Up @@ -260,13 +257,13 @@ export async function verifyJWT(
): Promise<JWTVerified> {
if (!options.resolver) throw new Error('No DID resolver has been configured')
const { payload, header, signature, data }: JWTDecoded = decodeJWT(jwt)
const { doc, authenticators, issuer }: DIDAuthenticator = await resolveAuthenticator(
const { didResolutionResult, authenticators, issuer }: DIDAuthenticator = await resolveAuthenticator(
options.resolver,
header.alg,
payload.iss,
options.auth
)
const signer: PublicKey = await verifyJWSDecoded({ header, data, signature } as JWSDecoded, authenticators)
const signer: VerificationMethod = await verifyJWSDecoded({ header, data, signature } as JWSDecoded, authenticators)
const now: number = Math.floor(Date.now() / 1000)
const skewTime = options.skewTime >= 0 ? options.skewTime : NBF_SKEW
if (signer) {
Expand All @@ -292,7 +289,7 @@ export async function verifyJWT(
throw new Error(`JWT audience does not match your DID or callback url`)
}
}
return { payload, doc, issuer, signer, jwt }
return { payload, didResolutionResult, issuer, signer, jwt }
}
}

Expand All @@ -313,7 +310,7 @@ export async function verifyJWT(
* @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
*/
export async function resolveAuthenticator(
resolver: Resolvable,
resolver: Resolver,
alg: string,
issuer: string,
auth?: boolean
Expand All @@ -322,30 +319,36 @@ export async function resolveAuthenticator(
if (!types || types.length === 0) {
throw new Error(`No supported signature types for algorithm ${alg}`)
}
const doc: DIDDocument = await resolver.resolve(issuer)
if (!doc) throw new Error(`Unable to resolve DID document for ${issuer}`)
const result: DIDResolutionResult = await resolver.resolve(issuer, { accept: DID_JSON })
mirceanis marked this conversation as resolved.
Show resolved Hide resolved
if (result.didResolutionMetadata?.error) {
const { error, message } = result.didResolutionMetadata
throw new Error(`Unable to resolve DID document for ${issuer}: ${error}, ${message || ''}`)
}

const getPublicKeyById = (doc: DIDDocument, pubid: string): PublicKey | null => {
const filtered = doc.publicKey.filter(({ id }) => pubid === id)
const getPublicKeyById = (verificationMethods: VerificationMethod[], pubid: string): VerificationMethod | null => {
const filtered = verificationMethods.filter(({ id }) => pubid === id)
return filtered.length > 0 ? filtered[0] : null
}

let publicKeysToCheck: PublicKey[] = doc.publicKey || []
let publicKeysToCheck: VerificationMethod[] = []
if (result.didDocument.verificationMethod) publicKeysToCheck.push(...result.didDocument.verificationMethod)
if (result.didDocument.publicKey) publicKeysToCheck.push(...result.didDocument.publicKey)
if (auth) {
publicKeysToCheck = (doc.authentication || [])
publicKeysToCheck = (result.didDocument.authentication || [])
.map((authEntry) => {
if (typeof authEntry === 'string') {
return getPublicKeyById(doc, authEntry)
} else if (typeof (<Authentication>authEntry).publicKey === 'string') {
return getPublicKeyById(doc, (<Authentication>authEntry).publicKey)
return getPublicKeyById(publicKeysToCheck, authEntry)
} else if (typeof (<any>authEntry).publicKey === 'string') {
// this is a legacy format
return getPublicKeyById(publicKeysToCheck, (<any>authEntry).publicKey)
} else {
return <PublicKey>authEntry
return <VerificationMethod>authEntry
}
})
.filter((key) => key != null)
}

const authenticators: PublicKey[] = publicKeysToCheck.filter(({ type }) =>
const authenticators: VerificationMethod[] = publicKeysToCheck.filter(({ type }) =>
types.find((supported) => supported === type)
)

Expand All @@ -355,5 +358,5 @@ export async function resolveAuthenticator(
if (!authenticators || authenticators.length === 0) {
throw new Error(`DID document for ${issuer} does not have public keys for ${alg}`)
}
return { authenticators, issuer, doc }
return { authenticators, issuer, didResolutionResult: result }
}
30 changes: 17 additions & 13 deletions src/VerifierAlgorithm.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ec as EC } from 'elliptic'
import { sha256, toEthereumAddress } from './Digest'
import { verify } from '@stablelib/ed25519'
import { PublicKey } from 'did-resolver'
import type { VerificationMethod } from 'did-resolver'
import { hexToBytes, base58ToBytes, base64ToBytes, bytesToHex, EcdsaSignature, stringToBytes } from './util'

const secp256k1 = new EC('secp256k1')
Expand All @@ -21,18 +21,22 @@ export function toSignatureObject(signature: string, recoverable = false): Ecdsa
return sigObj
}

function extractPublicKeyBytes(pk: PublicKey): Uint8Array {
interface LegacyVerificationMethod extends VerificationMethod {
publicKeyBase64: string
}

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

export function verifyES256K(data: string, signature: string, authenticators: PublicKey[]): PublicKey {
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 @@ -42,7 +46,7 @@ export function verifyES256K(data: string, signature: string, authenticators: Pu
return typeof ethereumAddress !== 'undefined'
})

let signer: PublicKey = fullPublicKeys.find((pk: PublicKey) => {
let signer: VerificationMethod = fullPublicKeys.find((pk: VerificationMethod) => {
try {
const pubBytes = extractPublicKeyBytes(pk)
return secp256k1.keyFromPublic(pubBytes).verify(hash, sigObj)
Expand All @@ -59,7 +63,7 @@ export function verifyES256K(data: string, signature: string, authenticators: Pu
return signer
}

export function verifyRecoverableES256K(data: string, signature: string, authenticators: PublicKey[]): PublicKey {
export function verifyRecoverableES256K(data: string, signature: string, authenticators: VerificationMethod[]): VerificationMethod {
let signatures: EcdsaSignature[]
if (signature.length > 86) {
signatures = [toSignatureObject(signature, true)]
Expand All @@ -71,14 +75,14 @@ export function verifyRecoverableES256K(data: string, signature: string, authent
]
}

const checkSignatureAgainstSigner = (sigObj: EcdsaSignature): PublicKey => {
const checkSignatureAgainstSigner = (sigObj: EcdsaSignature): VerificationMethod => {
const hash: Uint8Array = sha256(data)
const recoveredKey: any = secp256k1.recoverPubKey(hash, sigObj, sigObj.recoveryParam)
const recoveredPublicKeyHex: string = recoveredKey.encode('hex')
const recoveredCompressedPublicKeyHex: string = recoveredKey.encode('hex', true)
const recoveredAddress: string = toEthereumAddress(recoveredPublicKeyHex)

const signer: PublicKey = authenticators.find(
const signer: VerificationMethod = authenticators.find(
({ publicKeyHex, ethereumAddress }) =>
publicKeyHex === recoveredPublicKeyHex ||
publicKeyHex === recoveredCompressedPublicKeyHex ||
Expand All @@ -88,24 +92,24 @@ export function verifyRecoverableES256K(data: string, signature: string, authent
return signer
}

const signer: PublicKey[] = 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: PublicKey[]): PublicKey {
export function verifyEd25519(data: string, signature: string, authenticators: VerificationMethod[]): VerificationMethod {
const clear: Uint8Array = stringToBytes(data)
const sig: Uint8Array = base64ToBytes(signature)
const signer: PublicKey = authenticators.find((pk: PublicKey) => {
const signer: VerificationMethod = authenticators.find((pk: VerificationMethod) => {
return verify(extractPublicKeyBytes(pk), clear, sig)
}
)
if (!signer) throw new Error('Signature invalid for JWT')
return signer
}

type Verifier = (data: string, signature: string, authenticators: PublicKey[]) => PublicKey
type Verifier = (data: string, signature: string, authenticators: VerificationMethod[]) => VerificationMethod
interface Algorithms {
[name: string]: Verifier
}
Expand Down
Loading