Skip to content

Commit

Permalink
feat: homogenize createCredentialJwt/PresentationJwt API
Browse files Browse the repository at this point in the history
BREAKING CHANGE: renamed `createPresentationJWT` to `createVerifiablePresentationAJwt`
  • Loading branch information
mirceanis committed Jun 25, 2020
1 parent b96b467 commit 3999382
Show file tree
Hide file tree
Showing 6 changed files with 127 additions and 68 deletions.
12 changes: 6 additions & 6 deletions src/__tests__/converters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,15 +48,15 @@ describe('credential', () => {
it('merges credentialSubject objects', () => {
const result = normalizeCredential({
credentialSubject: { foo: 'bar' },
vc: { credentialSubject: { bar: 'baz' } }
vc: { credentialSubject: { bar: 'baz' }, '@context': [], type: [] }
})
expect(result).toMatchObject({ credentialSubject: { foo: 'bar', bar: 'baz' } })
})

it('merges credentialSubject objects with JWT precedence', () => {
const result = normalizeCredential({
credentialSubject: { foo: 'bar' },
vc: { credentialSubject: { foo: 'bazzz' } }
vc: { credentialSubject: { foo: 'bazzz' }, '@context': [], type: [] }
})
expect(result).toMatchObject({ credentialSubject: { foo: 'bazzz' } })
})
Expand Down Expand Up @@ -133,12 +133,12 @@ describe('credential', () => {
})

it('merges type as arrays for single items', () => {
const result = normalizeCredential({ type: 'bar', vc: { type: 'foo' } })
const result = normalizeCredential({ type: 'bar', vc: { type: 'foo', '@context': [], credentialSubject: {} } })
expect(result).toMatchObject({ type: ['bar', 'foo'] })
})

it('merges type as arrays uniquely', () => {
const result = normalizeCredential({ type: 'foo', vc: { type: 'foo' } })
const result = normalizeCredential({ type: 'foo', vc: { type: 'foo', '@context': [], credentialSubject: {} } })
expect(result).toMatchObject({ type: ['foo'] })
expect(result).not.toHaveProperty('vc')
})
Expand All @@ -156,7 +156,7 @@ describe('credential', () => {
})

it('merges @context as arrays for single items', () => {
const result = normalizeCredential({ context: 'baz', '@context': 'bar', vc: { '@context': 'foo' } })
const result = normalizeCredential({ context: 'baz', '@context': 'bar', vc: { '@context': 'foo', type: [], credentialSubject: {} } })
expect(result).toMatchObject({ '@context': ['baz', 'bar', 'foo'] })
})

Expand Down Expand Up @@ -646,7 +646,7 @@ describe('presentation', () => {
const result = normalizePresentation({
'@context': ['foo', 'bar'],
context: ['bar', 'baz', undefined, null],
vp: { '@context': ['bar', 'baz', 'bak'] }
vp: { '@context': ['bar', 'baz', 'bak'], type: [], verifiableCredential: [] }
})
expect(result).toMatchObject({ '@context': ['bar', 'baz', 'foo', 'bak'] })
})
Expand Down
25 changes: 12 additions & 13 deletions src/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import EthrDID from 'ethr-did'
import { createVerifiableCredentialJwt, createPresentationJwt, verifyCredential, verifyPresentation } from '../index'
import { createVerifiableCredentialJwt, verifyCredential, verifyPresentation, createVerifiablePresentationJwt } from '../index'
import { verifyJWT, decodeJWT } from 'did-jwt'
import { DEFAULT_VC_TYPE, DEFAULT_VP_TYPE, DEFAULT_CONTEXT } from '../constants'
import {
Expand Down Expand Up @@ -111,19 +111,19 @@ describe('createVerifiableCredential', () => {

describe('createPresentation', () => {
it('creates a valid Presentation JWT with required fields', async () => {
const presentationJwt = await createPresentationJwt(presentationPayload, did)
const presentationJwt = await createVerifiablePresentationJwt(presentationPayload, did)
const decodedPresentation = await decodeJWT(presentationJwt)
const { iat, ...payload } = decodedPresentation.payload
expect(payload).toMatchSnapshot()
})
it('creates a valid Presentation JWT with extra optional fields', async () => {
const presentationJwt = await createPresentationJwt({ ...presentationPayload, extra: 42 }, did)
const presentationJwt = await createVerifiablePresentationJwt({ ...presentationPayload, extra: 42 }, did)
const decodedPresentation = await decodeJWT(presentationJwt)
const { iat, ...payload } = decodedPresentation.payload
expect(payload).toMatchSnapshot()
})
it('calls functions to validate required fields', async () => {
await createPresentationJwt(presentationPayload, did)
await createVerifiablePresentationJwt(presentationPayload, did)
expect(mockValidateContext).toHaveBeenCalledWith(presentationPayload.vp['@context'])
expect(mockValidateVpType).toHaveBeenCalledWith(presentationPayload.vp.type)
for (const vc of presentationPayload.vp.verifiableCredential) {
Expand All @@ -132,7 +132,7 @@ describe('createPresentation', () => {
})
it('throws a TypeError if vp.verifiableCredential is empty', async () => {
await expect(
createPresentationJwt(
createVerifiablePresentationJwt(
{
...presentationPayload,
vp: {
Expand All @@ -147,7 +147,7 @@ describe('createPresentation', () => {
})
it('calls functions to validate optional fields if they are present', async () => {
const timestamp = Math.floor(new Date().getTime())
await createPresentationJwt(
await createVerifiablePresentationJwt(
{
...presentationPayload,
exp: timestamp
Expand All @@ -162,29 +162,28 @@ describe('verifyCredential', () => {
it('verifies a valid Verifiable Credential', async () => {
const verified = await verifyCredential(VC_JWT, resolver)
expect(verified.payload.vc).toBeDefined()
expect(verified.verifiableCredential).toBeDefined()
})

it('verifies and converts a legacy format attestation into a Verifiable Credential', async () => {
// tslint:disable-next-line: max-line-length
const LEGACY_FORMAT_ATTESTATION =
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NkstUiJ9.eyJpYXQiOjE1NjM4MjQ4MDksImV4cCI6OTk2Mjk1MDI4Miwic3ViIjoiZGlkOmV0aHI6MHhmMTIzMmY4NDBmM2FkN2QyM2ZjZGFhODRkNmM2NmRhYzI0ZWZiMTk4IiwiY2xhaW0iOnsiZGVncmVlIjp7InR5cGUiOiJCYWNoZWxvckRlZ3JlZSIsIm5hbWUiOiJCYWNjYWxhdXLDqWF0IGVuIG11c2lxdWVzIG51bcOpcmlxdWVzIn19LCJpc3MiOiJkaWQ6ZXRocjoweGYzYmVhYzMwYzQ5OGQ5ZTI2ODY1ZjM0ZmNhYTU3ZGJiOTM1YjBkNzQifQ.OsKmaxoA2pt3_ixWK61BaMDc072g2PymBX_CCUSo-irvtIRUP5qBCcerhpASe5hOcTg5nNpNg0XYXnqyF9I4XwE'
'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NkstUiJ9.eyJpYXQiOjE1NjM4MjQ4MDksImV4cCI6OTk2Mjk1MDI4Miwic3ViIjoiZGlkOmV0aHI6MHhmMTIzMmY4NDBmM2FkN2QyM2ZjZGFhODRkNmM2NmRhYzI0ZWZiMTk4IiwiY2xhaW0iOnsiZGVncmVlIjp7InR5cGUiOiJCYWNoZWxvckRlZ3JlZSIsIm5hbWUiOiJCYWNjYWxhdXLDqWF0IGVuIG11c2lxdWVzIG51bcOpcmlxdWVzIn19LCJpc3MiOiJkaWQ6ZXRocjoweGYzYmVhYzMwYzQ5OGQ5ZTI2ODY1ZjM0ZmNhYTU3ZGJiOTM1YjBkNzQifQ.OsKmaxoA2pt3_ixWK61BaMDc072g2PymBX_CCUSo-irvtIRUP5qBCcerhpASe5hOcTg5nNpNg0XYXnqyF9I4XwE'
const verified = await verifyCredential(LEGACY_FORMAT_ATTESTATION, resolver)
expect(verified.payload.vc).toBeDefined()
// expect(verified.payload.vc).toBeDefined()
expect(verified.verifiableCredential).toBeDefined()
})

it('rejects an invalid JWT', () => {
expect(verifyCredential('not a jwt', resolver)).rejects.toThrow()
})

it('rejects a valid JWT that is missing VC attributes', () => {
expect(verifyCredential(BASIC_JWT, resolver)).rejects.toThrow()
})
})

describe('verifyPresentation', () => {
it('verifies a valid Presentation', async () => {
const verified = await verifyPresentation(PRESENTATION_JWT, resolver)
expect(verified.payload.vp).toBeDefined()
expect(verified.verifiablePresentation).toBeDefined()
})

it('rejects an invalid JWT', () => {
Expand Down
27 changes: 26 additions & 1 deletion src/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
Presentation
} from './types'
import { decodeJWT } from 'did-jwt'
import { JWT_FORMAT, DEFAULT_JWT_PROOF_TYPE } from './constants'
import { JWT_FORMAT, DEFAULT_JWT_PROOF_TYPE, DEFAULT_CONTEXT, DEFAULT_VC_TYPE } from './constants'

function asArray(input: any) {
return Array.isArray(input) ? input : [input]
Expand All @@ -29,9 +29,33 @@ function cleanUndefined<T>(input: T): T {
return obj
}

export function isLegacyAttestationFormat(payload: any): boolean {
// payload is an object and has all the required fields of old attestation format
return payload instanceof Object && payload.sub && payload.iss && payload.claim && payload.iat
}

export function attestationToVcFormat(payload: any): JwtCredentialPayload {
const { iat, nbf, claim, vc, ...rest } = payload
const result: JwtCredentialPayload = {
...rest,
nbf: nbf ? nbf : iat,
vc: {
'@context': [DEFAULT_CONTEXT],
type: [DEFAULT_VC_TYPE],
credentialSubject: payload.claim
}
}
if (vc) payload.issVc = vc
return result
}

function normalizeJwtCredentialPayload(input: Partial<JwtCredentialPayload>): Credential {
let result: Partial<CredentialPayload> = { ...input }

if (isLegacyAttestationFormat(input)) {
result = attestationToVcFormat(input)
}

//FIXME: handle case when credentialSubject(s) are not object types
result.credentialSubject = { ...input.credentialSubject, ...input.vc?.credentialSubject }
if (input.sub && !input.credentialSubject?.id) {
Expand Down Expand Up @@ -225,6 +249,7 @@ function normalizeJwtPresentationPayload(input: DeepPartial<JwtPresentationPaylo
...asArray(input.vp?.verifiableCredential)
].filter(notEmpty)
result.verifiableCredential = result.verifiableCredential.map(normalizeCredential)
delete result.vp?.verifiableCredential

if (input.iss && !input.holder) {
result.holder = input.iss
Expand Down
65 changes: 40 additions & 25 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,45 @@
import { createJWT, verifyJWT } from 'did-jwt'
import { JWT_ALG, DEFAULT_CONTEXT, DEFAULT_VC_TYPE } from './constants'
import { JWT_ALG, DEFAULT_CONTEXT, DEFAULT_VC_TYPE, DEFAULT_JWT_PROOF_TYPE } from './constants'
import * as validators from './validators'
import {
JwtCredentialPayload,
Issuer,
JwtPresentationPayload,
JWT,
VerifiablePresentation,
VerifiableCredential
VerifiableCredential,
CredentialPayload,
PresentationPayload,
Verifiable,
Credential,
Presentation,
VerifiedCredential,
VerifiedPresentation,
Verified
} from './types'
import { DIDDocument } from 'did-resolver'
import { transformCredentialInput, transformPresentationInput, normalizePresentation, normalizeCredential, isLegacyAttestationFormat, attestationToVcFormat } from './converters'

export { Issuer, JwtCredentialPayload, JwtPresentationPayload, VerifiableCredential, VerifiablePresentation }
export { Issuer, JwtCredentialPayload, JwtPresentationPayload, VerifiableCredential, VerifiablePresentation, VerifiedCredential, VerifiedPresentation }

interface Resolvable {
resolve: (did: string) => Promise<DIDDocument>
}

export async function createVerifiableCredentialJwt(payload: JwtCredentialPayload, issuer: Issuer): Promise<JWT> {
validateJwtVerifiableCredentialPayload(payload)
return createJWT(payload, {
export async function createVerifiableCredentialJwt(payload: JwtCredentialPayload | CredentialPayload, issuer: Issuer): Promise<JWT> {
const parsedPayload = transformCredentialInput(payload)
validateJwtVerifiableCredentialPayload(parsedPayload)
return createJWT(parsedPayload, {
issuer: issuer.did,
signer: issuer.signer,
alg: issuer.alg || JWT_ALG
})
}

export async function createPresentationJwt(payload: JwtPresentationPayload, issuer: Issuer): Promise<JWT> {
validateJwtPresentationPayload(payload)
return createJWT(payload, {
export async function createVerifiablePresentationJwt(payload: JwtPresentationPayload | PresentationPayload, issuer: Issuer): Promise<JWT> {
const parsedPayload = transformPresentationInput(payload)
validateJwtPresentationPayload(parsedPayload)
return createJWT(parsedPayload, {
issuer: issuer.did,
signer: issuer.signer,
alg: issuer.alg || JWT_ALG
Expand All @@ -43,6 +54,14 @@ export function validateJwtVerifiableCredentialPayload(payload: JwtCredentialPay
if (payload.exp) validators.validateTimestamp(payload.exp)
}

export function validateVerifiableCredentialPayload(payload: CredentialPayload): void {
validators.validateContext(payload['@context'])
validators.validateVcType(payload.type)
validators.validateCredentialSubject(payload.credentialSubject)
if (payload.issuanceDate) validators.validateTimestamp(new Date(payload.issuanceDate).valueOf() / 1000)
if (payload.expirationDate) validators.validateTimestamp(new Date(payload.expirationDate).valueOf() / 1000)
}

export function validateJwtPresentationPayload(payload: JwtPresentationPayload): void {
validators.validateContext(payload.vp['@context'])
validators.validateVpType(payload.vp.type)
Expand All @@ -55,24 +74,20 @@ export function validateJwtPresentationPayload(payload: JwtPresentationPayload):
if (payload.exp) validators.validateTimestamp(payload.exp)
}

function isLegacyAttestationFormat(payload: any): boolean {
// payload is an object and has all the required fields of old attestation format
return payload instanceof Object && payload.sub && payload.iss && payload.claim && payload.iat
}

function attestationToVcFormat(payload: any): JwtCredentialPayload {
const { iat, nbf, claim, vc, ...rest } = payload
const result: JwtCredentialPayload = {
...rest,
nbf: nbf ? nbf : iat,
vc: {
'@context': [DEFAULT_CONTEXT],
type: [DEFAULT_VC_TYPE],
credentialSubject: payload.claim
export function validatePresentationPayload(payload: PresentationPayload): void {
validators.validateContext(payload['@context'])
validators.validateVpType(payload.type)
if (payload.verifiableCredential.length < 1) {
throw new TypeError('vp.verifiableCredential must not be empty')
}
for (const vc of payload.verifiableCredential) {
if (typeof vc === 'string') {
validators.validateJwtFormat(vc)
} else {
validateVerifiableCredentialPayload(vc)
}
}
if (vc) payload.issVc = vc
return result
if (payload.expirationDate) validators.validateTimestamp(payload.expirationDate)
}

export async function verifyCredential(vc: JWT, resolver: Resolvable): Promise<any> {
Expand Down
Loading

0 comments on commit 3999382

Please sign in to comment.