Skip to content

Commit

Permalink
Merge pull request #86 from decentralized-identity/develop
Browse files Browse the repository at this point in the history
chore: new release
  • Loading branch information
mirceanis committed Apr 26, 2020
2 parents e5c6b46 + ad7a61c commit 964f5e7
Show file tree
Hide file tree
Showing 12 changed files with 256 additions and 348 deletions.
2 changes: 1 addition & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"jsxBracketSameLine": false,
"trailingComma": "none",
"tabWidth": 2,
"printWidth": 80,
"printWidth": 120,
"singleQuote": true,
"semi": false
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,15 @@
"esm"
],
"scripts": {
"test": "./node_modules/.bin/standard && jest --updateSnapshot",
"test": "./node_modules/.bin/standard && jest",
"build:js": "./node_modules/.bin/microbundle",
"build:browser": "./node_modules/.bin/webpack --config webpack.config.js",
"build": "npm run build:js && npm test && npm run build:browser",
"build:docs": "echo 'PLEASE UPDATE REFERENCE DOCS MANUALLY'",
"format": "prettier --write \"src/**/*.ts\" \"src/**/*.js\"",
"lint": "tslint -p tsconfig.json",
"prepublish": "npm run build",
"prepublishOnly": "npm test && npm run lint",
"prepare": "npm run build"
},
"author": "Pelle Braendgaard <pelle.braendgaard@consensys.net>",
Expand Down
10 changes: 6 additions & 4 deletions src/Digest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import { sha256 as sha256js, Message } from 'js-sha256'
import { keccak_256 } from 'js-sha3' // eslint-disable-line
import { Buffer } from 'buffer'

export function sha256 (payload: Message): Buffer {
export function sha256(payload: Message): Buffer {
return Buffer.from(sha256js.arrayBuffer(payload))
}

export function keccak (data: Message): Buffer {
export function keccak(data: Message): Buffer {
return Buffer.from(keccak_256.arrayBuffer(data))
}

export function toEthereumAddress (hexPublicKey: string): string {
return `0x${keccak(Buffer.from(hexPublicKey.slice(2), 'hex')).slice(-20).toString('hex')}`
export function toEthereumAddress(hexPublicKey: string): string {
return `0x${keccak(Buffer.from(hexPublicKey.slice(2), 'hex'))
.slice(-20)
.toString('hex')}`
}
172 changes: 70 additions & 102 deletions src/JWT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,7 @@ export interface EcdsaSignature {
}

export type Signer = (data: string) => Promise<EcdsaSignature | string>
export type SignerAlgorithm = (
payload: string,
signer: Signer
) => Promise<string>
export type SignerAlgorithm = (payload: string, signer: Signer) => Promise<string>

interface JWTOptions {
issuer: string
Expand Down Expand Up @@ -48,7 +45,7 @@ interface JWTHeader {
interface JWTPayload {
iss?: string
sub?: string
aud?: string
aud?: string | string[]
iat?: number
nbf?: number
type?: string
Expand Down Expand Up @@ -76,16 +73,8 @@ interface PublicKeyTypes {
[name: string]: string[]
}
const SUPPORTED_PUBLIC_KEY_TYPES: PublicKeyTypes = {
ES256K: [
'Secp256k1VerificationKey2018',
'Secp256k1SignatureVerificationKey2018',
'EcdsaPublicKeySecp256k1'
],
'ES256K-R': [
'Secp256k1VerificationKey2018',
'Secp256k1SignatureVerificationKey2018',
'EcdsaPublicKeySecp256k1'
],
ES256K: ['Secp256k1VerificationKey2018', 'Secp256k1SignatureVerificationKey2018', 'EcdsaPublicKeySecp256k1'],
'ES256K-R': ['Secp256k1VerificationKey2018', 'Secp256k1SignatureVerificationKey2018', 'EcdsaPublicKeySecp256k1'],
Ed25519: ['ED25519SignatureVerification']
}

Expand All @@ -99,23 +88,6 @@ export const NBF_SKEW: number = 300

/** @module did-jwt/JWT */

function isMNID(id: string): RegExpMatchArray {
return id.match(
/^[123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz]+$/
)
}

function isDIDOrMNID(mnidOrDid: string): RegExpMatchArray {
return mnidOrDid && (mnidOrDid.match(/^did:/) || isMNID(mnidOrDid))
}

export function normalizeDID(mnidOrDid: string): string {
if (mnidOrDid.match(/^did:/)) return mnidOrDid
// Backwards compatibility
if (isMNID(mnidOrDid)) return `did:uport:${mnidOrDid}`
throw new Error(`Not a valid DID '${mnidOrDid}'`)
}

/**
* Decodes a JWT and returns an object representing the payload
*
Expand All @@ -127,9 +99,7 @@ export function normalizeDID(mnidOrDid: string): string {
*/
export function decodeJWT(jwt: string): JWTDecoded {
if (!jwt) throw new Error('no JWT passed into decodeJWT')
const parts: RegExpMatchArray = jwt.match(
/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/
)
const parts: RegExpMatchArray = jwt.match(/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/)
if (parts) {
return {
header: JSON.parse(base64url.decode(parts[1])),
Expand All @@ -141,6 +111,27 @@ export function decodeJWT(jwt: string): JWTDecoded {
throw new Error('Incorrect format JWT')
}

/**
* Creates a signed JWS given a payload, a signer, and an optional header.
*
* @example
* const signer = SimpleSigner(process.env.PRIVATE_KEY)
* const jws = await createJWS({ my: 'payload' }, signer)
*
* @param {Object} payload payload object
* @param {SimpleSigner} signer a signer, reference our SimpleSigner.js
* @param {Object} header optional object to specify or customize the JWS header
* @return {Promise<Object, Error>} a promise which resolves with a JWS string or rejects with an error
*/
export async function createJWS(payload: any, signer: Signer, header: Partial<JWTHeader> = {}): Promise<string> {
if (!header.alg) header.alg = defaultAlg
const signingInput: string = [encodeSection(header), encodeSection(payload)].join('.')

const jwtSigner: SignerAlgorithm = SignerAlgorithm(header.alg)
const signature: string = await jwtSigner(signingInput, signer)
return [signingInput, signature].join('.')
}

/**
* Creates a signed JWT given an address which becomes the issuer, a signer, and a payload for which the signature is over.
*
Expand All @@ -159,16 +150,15 @@ export function decodeJWT(jwt: string): JWTDecoded {
* @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, { issuer, signer, alg, expiresIn }, header) {
export async function createJWT(
payload: any,
{ issuer, signer, alg, expiresIn }: JWTOptions,
header: Partial<JWTHeader> = {},
header: Partial<JWTHeader> = {}
): Promise<string> {
if (!signer) throw new Error('No Signer functionality has been configured')
if (!issuer) throw new Error('No issuing DID has been configured')
if (!header.typ) header.typ = 'JWT'
if (!header.alg) header.alg = alg || defaultAlg
if (!header.alg) header.alg = alg
const timestamps: Partial<JWTPayload> = {
iat: Math.floor(Date.now() / 1000),
exp: undefined
Expand All @@ -180,14 +170,26 @@ export async function createJWT(
throw new Error('JWT expiresIn is not a number')
}
}
const signingInput: string = [
encodeSection(header),
encodeSection({ ...timestamps, ...payload, iss: issuer })
].join('.')
const fullPayload = { ...timestamps, ...payload, iss: issuer }
return createJWS(fullPayload, signer, header)
}

const jwtSigner: SignerAlgorithm = SignerAlgorithm(header.alg)
const signature: string = await jwtSigner(signingInput, signer)
return [signingInput, signature].join('.')
/**
* Verifies given JWS. If the JWS is valid, returns the public key that was
* used to sign the JWS, or throws an `Error` if none of the `pubkeys` match.
*
* @example
* 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
*/
export function verifyJWS(jws: string, pubkeys: PublicKey | PublicKey[]): PublicKey {
if (!Array.isArray(pubkeys)) pubkeys = [pubkeys]
const { header, data, signature }: JWTDecoded = decodeJWT(jws)
const signer: PublicKey = VerifierAlgorithm(header.alg)(data, signature, pubkeys)
return signer
}

/**
Expand All @@ -213,28 +215,22 @@ export async function createJWT(
*/
export async function verifyJWT(
jwt: string,
options: JWTVerifyOptions = { resolver: null, auth: null, audience: null, callbackUrl: null }
options: JWTVerifyOptions = {
resolver: null,
auth: null,
audience: null,
callbackUrl: null
}
): Promise<Verified> {
if (!options.resolver) throw new Error('No DID resolver has been configured')
const aud: string = options.audience
? normalizeDID(options.audience)
: undefined
const { payload, header, signature, data }: JWTDecoded = decodeJWT(jwt)
const {
doc,
authenticators,
issuer
}: DIDAuthenticator = await resolveAuthenticator(
const { doc, authenticators, issuer }: DIDAuthenticator = await resolveAuthenticator(
options.resolver,
header.alg,
payload.iss,
options.auth
)
const signer: PublicKey = VerifierAlgorithm(header.alg)(
data,
signature,
authenticators
)
const signer: PublicKey = await verifyJWS(jwt, authenticators)
const now: number = Math.floor(Date.now() / 1000)
if (signer) {
const nowSkewed = now + NBF_SKEW
Expand All @@ -249,33 +245,14 @@ export async function verifyJWT(
throw new Error(`JWT has expired: exp: ${payload.exp} < now: ${now}`)
}
if (payload.aud) {
if (isDIDOrMNID(payload.aud)) {
if (!aud) {
throw new Error(
'JWT audience is required but your app address has not been configured'
)
}
if (!options.audience && !options.callbackUrl) {
throw new Error('JWT audience is required but your app address has not been configured')
}
const audArray = Array.isArray(payload.aud) ? payload.aud : [payload.aud]
const matchedAudience = audArray.find(item => options.audience === item || options.callbackUrl === item)

if (aud !== normalizeDID(payload.aud)) {
throw new Error(
`JWT audience does not match your DID: aud: ${
payload.aud
} !== yours: ${aud}`
)
}
} else {
if (!options.callbackUrl) {
throw new Error(
"JWT audience matching your callback url is required but one wasn't passed in"
)
}
if (payload.aud !== options.callbackUrl) {
throw new Error(
`JWT audience does not match the callback url: aud: ${
payload.aud
} !== url: ${options.callbackUrl}`
)
}
if (typeof matchedAudience === 'undefined') {
throw new Error(`JWT audience does not match your DID or callback url`)
}
}
return { payload, doc, issuer, signer, jwt }
Expand All @@ -301,40 +278,31 @@ export async function verifyJWT(
export async function resolveAuthenticator(
resolver: Resolvable,
alg: string,
mnidOrDid: string,
issuer: string,
auth?: boolean
): 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 issuer: string = normalizeDID(mnidOrDid)
const doc: DIDDocument = await resolver.resolve(issuer)
if (!doc) throw new Error(`Unable to resolve DID document for ${issuer}`)
// is there some way to have authenticationKeys be a single type?
const authenticationKeys: boolean | string[] = auth
? (doc.authentication || []).map(({ publicKey }) => publicKey)
: true
const authenticators: PublicKey[] = (doc.publicKey || []).filter(
({ type, id }) =>
types.find(
supported =>
supported === type &&
(!auth ||
(Array.isArray(authenticationKeys) &&
authenticationKeys.indexOf(id) >= 0))
)
const authenticators: PublicKey[] = (doc.publicKey || []).filter(({ type, id }) =>
types.find(
supported =>
supported === type && (!auth || (Array.isArray(authenticationKeys) && authenticationKeys.indexOf(id) >= 0))
)
)

if (auth && (!authenticators || authenticators.length === 0)) {
throw new Error(
`DID document for ${issuer} does not have public keys suitable for authenticationg user`
)
throw new Error(`DID document for ${issuer} does not have public keys suitable for authenticationg user`)
}
if (!authenticators || authenticators.length === 0) {
throw new Error(
`DID document for ${issuer} does not have public keys for ${alg}`
)
throw new Error(`DID document for ${issuer} does not have public keys for ${alg}`)
}
return { authenticators, issuer, doc }
}
8 changes: 2 additions & 6 deletions src/SignerAlgorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ export function ES256KSigner(recoverable?: boolean): SignerAlgorithm {
if (instanceOfEcdsaSignature(signature)) {
return toJose(signature)
} else {
throw new Error(
'expected a signer function that returns a signature object instead of string'
)
throw new Error('expected a signer function that returns a signature object instead of string')
}
}
}
Expand All @@ -38,9 +36,7 @@ export function Ed25519Signer(): SignerAlgorithm {
if (!instanceOfEcdsaSignature(signature)) {
return signature
} else {
throw new Error(
'expected a signer function that returns a string instead of signature object'
)
throw new Error('expected a signer function that returns a string instead of signature object')
}
}
}
Expand Down
Loading

0 comments on commit 964f5e7

Please sign in to comment.