Skip to content

Commit

Permalink
refactor: use jws service to create and verify jws
Browse files Browse the repository at this point in the history
Signed-off-by: Timo Glastra <timo@animo.id>
  • Loading branch information
TimoGlastra committed Jan 13, 2022
1 parent afe7531 commit b6b9dcc
Show file tree
Hide file tree
Showing 18 changed files with 277 additions and 489 deletions.
121 changes: 121 additions & 0 deletions packages/core/src/crypto/JwsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import type { Buffer } from '../utils'
import type { Jws, JwsGeneralFormat } from './JwsTypes'

import { inject, Lifecycle, scoped } from 'tsyringe'

import { InjectionSymbols } from '../constants'
import { AriesFrameworkError } from '../error'
import { JsonEncoder, BufferEncoder } from '../utils'
import { Wallet } from '../wallet'
import { WalletError } from '../wallet/error'

// TODO: support more key types, more generic jws format
const JWS_KEY_TYPE = 'OKP'
const JWS_CURVE = 'Ed25519'
const JWS_ALG = 'EdDSA'

@scoped(Lifecycle.ContainerScoped)
export class JwsService {
private wallet: Wallet

public constructor(@inject(InjectionSymbols.Wallet) wallet: Wallet) {
this.wallet = wallet
}

public async createJws({ payload, verkey, header }: CreateJwsOptions): Promise<JwsGeneralFormat> {
const base64Payload = BufferEncoder.toBase64URL(payload)
const base64Protected = JsonEncoder.toBase64URL(this.buildProtected(verkey))

const signature = BufferEncoder.toBase64URL(
await this.wallet.sign(BufferEncoder.fromString(`${base64Protected}.${base64Payload}`), verkey)
)

return {
protected: base64Protected,
signature,
header,
}
}

/**
* Verify a a JWS
*/
public async verifyJws({ jws, payload }: VerifyJwsOptions): Promise<VerifyJwsResult> {
const base64Payload = BufferEncoder.toBase64URL(payload)
const signatures = 'signatures' in jws ? jws.signatures : [jws]

const signerVerkeys = []
for (const jws of signatures) {
const protectedJson = JsonEncoder.fromBase64(jws.protected)

const isValidKeyType = protectedJson?.jwk?.kty === JWS_KEY_TYPE
const isValidCurve = protectedJson?.jwk?.crv === JWS_CURVE
const isValidAlg = protectedJson?.alg === JWS_ALG

if (!isValidKeyType || !isValidCurve || !isValidAlg) {
throw new AriesFrameworkError('Invalid protected header')
}

const data = BufferEncoder.fromString(`${jws.protected}.${base64Payload}`)
const signature = BufferEncoder.fromBase64(jws.signature)

const verkey = BufferEncoder.toBase58(BufferEncoder.fromBase64(protectedJson?.jwk?.x))
signerVerkeys.push(verkey)

try {
const isValid = await this.wallet.verify(verkey, data, signature)

if (!isValid) {
return {
isValid: false,
signerVerkeys: [],
}
}
} catch (error) {
// WalletError probably means signature verification failed. Would be useful to add
// more specific error type in wallet.verify method
if (error instanceof WalletError) {
return {
isValid: false,
signerVerkeys: [],
}
}

throw error
}
}

return { isValid: true, signerVerkeys }
}

/**
* @todo This currently only work with a single alg, key type and curve
* This needs to be extended with other formats in the future
*/
private buildProtected(verkey: string) {
return {
alg: 'EdDSA',
jwk: {
kty: 'OKP',
crv: 'Ed25519',
x: BufferEncoder.toBase64URL(BufferEncoder.fromBase58(verkey)),
},
}
}
}

export interface CreateJwsOptions {
verkey: string
payload: Buffer
header: Record<string, unknown>
}

export interface VerifyJwsOptions {
jws: Jws
payload: Buffer
}

export interface VerifyJwsResult {
isValid: boolean
signerVerkeys: string[]
}
11 changes: 11 additions & 0 deletions packages/core/src/crypto/JwsTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface JwsGeneralFormat {
header: Record<string, unknown>
signature: string
protected: string
}

export interface JwsFlattenedFormat {
signatures: JwsGeneralFormat[]
}

export type Jws = JwsGeneralFormat | JwsFlattenedFormat
82 changes: 82 additions & 0 deletions packages/core/src/crypto/__tests__/JwsService.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { Wallet } from '@aries-framework/core'

import { getAgentConfig } from '../../../tests/helpers'
import { DidKey, KeyType } from '../../modules/dids'
import { JsonEncoder } from '../../utils'
import { IndyWallet } from '../../wallet/IndyWallet'
import { JwsService } from '../JwsService'

import * as didJwsz6Mkf from './__fixtures__/didJwsz6Mkf'
import * as didJwsz6Mkv from './__fixtures__/didJwsz6Mkv'

describe('JwsService', () => {
let wallet: Wallet
let jwsService: JwsService

beforeAll(async () => {
const config = getAgentConfig('JwsService')
wallet = new IndyWallet(config)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await wallet.initialize(config.walletConfig!)

jwsService = new JwsService(wallet)
})

afterAll(async () => {
await wallet.delete()
})

describe('createJws', () => {
it('creates a jws for the payload with the key associated with the verkey', async () => {
const { verkey } = await wallet.createDid({ seed: didJwsz6Mkf.SEED })

const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON)
const kid = DidKey.fromPublicKeyBase58(verkey, KeyType.ED25519).did

const jws = await jwsService.createJws({
payload,
verkey,
header: { kid },
})

expect(jws).toEqual(didJwsz6Mkf.JWS_JSON)
})
})

describe('verifyJws', () => {
it('returns true if the jws signature matches the payload', async () => {
const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON)

const { isValid, signerVerkeys } = await jwsService.verifyJws({
payload,
jws: didJwsz6Mkf.JWS_JSON,
})

expect(isValid).toBe(true)
expect(signerVerkeys).toEqual([didJwsz6Mkf.VERKEY])
})

it('returns all verkeys that signed the jws', async () => {
const payload = JsonEncoder.toBuffer(didJwsz6Mkf.DATA_JSON)

const { isValid, signerVerkeys } = await jwsService.verifyJws({
payload,
jws: { signatures: [didJwsz6Mkf.JWS_JSON, didJwsz6Mkv.JWS_JSON] },
})

expect(isValid).toBe(true)
expect(signerVerkeys).toEqual([didJwsz6Mkf.VERKEY, didJwsz6Mkv.VERKEY])
})
it('returns false if the jws signature does not match the payload', async () => {
const payload = JsonEncoder.toBuffer({ ...didJwsz6Mkf.DATA_JSON, did: 'another_did' })

const { isValid, signerVerkeys } = await jwsService.verifyJws({
payload,
jws: didJwsz6Mkf.JWS_JSON,
})

expect(isValid).toBe(false)
expect(signerVerkeys).toMatchObject([])
})
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const SEED = '00000000000000000000000000000My2'
export const VERKEY = 'kqa2HyagzfMAq42H5f9u3UMwnSBPQx2QfrSyXbUPxMn'

export const DATA_JSON = {
did: 'did',
Expand All @@ -19,7 +20,7 @@ export const DATA_JSON = {

export const JWS_JSON = {
header: { kid: 'did:key:z6MkfD6ccYE22Y9pHKtixeczk92MmMi2oJCP6gmNooZVKB9A' },
signature: 'fmJcjnkAmdjYw37ztAizmnW7YepR26M0CEc2fpZeGgFcZTrkbrZTDB8hjLwoQjay8UnY1nvrWx-KGPNVbAKJCA',
protected:
'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2ZENmNjWUUyMlk5cEhLdGl4ZWN6azkyTW1NaTJvSkNQNmdtTm9vWlZLQjlBIiwiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiQ3pya2I2NDUzN2tVRUZGQ3lJcjhJODFRYklEaTYyc2ttTjVGbjUtTXNWRSIsImtpZCI6ImRpZDprZXk6ejZNa2ZENmNjWUUyMlk5cEhLdGl4ZWN6azkyTW1NaTJvSkNQNmdtTm9vWlZLQjlBIn19',
'eyJhbGciOiJFZERTQSIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IkN6cmtiNjQ1MzdrVUVGRkN5SXI4STgxUWJJRGk2MnNrbU41Rm41LU1zVkUifX0',
signature: 'OsDP4FM8792J9JlessA9IXv4YUYjIGcIAnPPrEJmgxYomMwDoH-h2DMAF5YF2VtsHHyhGN_0HryDjWSEAZdYBQ',
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export const SEED = '00000000000000000000000000000My1'
export const VERKEY = 'GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa'

export const DATA_JSON = {
did: 'did',
Expand Down
78 changes: 34 additions & 44 deletions packages/core/src/decorators/attachment/Attachment.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Wallet } from '../../wallet/Wallet'
import type { JwsGeneralFormat } from '../../crypto/JwsTypes'

import { Expose, Type } from 'class-transformer'
import {
Expand All @@ -13,12 +13,11 @@ import {
ValidateNested,
} from 'class-validator'

import { Jws } from '../../crypto/JwsTypes'
import { AriesFrameworkError } from '../../error'
import { JsonEncoder } from '../../utils/JsonEncoder'
import { uuid } from '../../utils/uuid'

import { AttachmentJws, getVerkeysForJws, signAttachmentJws, verifyAttachmentJws } from './AttachmentJwsUtil'

export interface AttachmentOptions {
id?: string
description?: string
Expand All @@ -33,7 +32,7 @@ export interface AttachmentDataOptions {
base64?: string
json?: Record<string, unknown>
links?: string[]
jws?: AttachmentJws
jws?: Jws
sha256?: string
}

Expand Down Expand Up @@ -65,9 +64,7 @@ export class AttachmentData {
* A JSON Web Signature over the content of the attachment. Optional.
*/
@IsOptional()
@Type(() => AttachmentJws)
@ValidateNested()
public jws?: AttachmentJws
public jws?: Jws

/**
* The hash of the content. Optional.
Expand All @@ -85,43 +82,6 @@ export class AttachmentData {
this.sha256 = options.sha256
}
}

public get jwsVerkeys(): string[] {
if (!this.jws) return []

return getVerkeysForJws(this.jws)
}

/*
* Helper function returning JSON representation of attachment data (if present). Tries to obtain the data from .base64 or .json, throws an error otherwise
*/
public getDataAsJson<T>(): T {
if (typeof this.base64 === 'string') {
return JsonEncoder.fromBase64(this.base64) as T
} else if (this.json) {
return this.json as T
} else {
throw new AriesFrameworkError('No attachment data found in `json` or `base64` data fields.')
}
}

public async sign(wallet: Wallet, verkeys: string[]) {
if (!this.base64) {
throw new AriesFrameworkError('Missing base64 data on attachment')
}

this.jws = await signAttachmentJws(wallet, verkeys, this.base64)
}

public async verify(wallet: Wallet) {
if (!this.jws || !this.base64) {
throw new AriesFrameworkError('Missing JWS and/or base64 parameters')
}

const isValid = await verifyAttachmentJws(wallet, this.jws, this.base64)

return isValid
}
}

/**
Expand Down Expand Up @@ -187,4 +147,34 @@ export class Attachment {
@ValidateNested()
@IsInstance(AttachmentData)
public data!: AttachmentData

/*
* Helper function returning JSON representation of attachment data (if present). Tries to obtain the data from .base64 or .json, throws an error otherwise
*/
public getDataAsJson<T>(): T {
if (typeof this.data.base64 === 'string') {
return JsonEncoder.fromBase64(this.data.base64) as T
} else if (this.data.json) {
return this.data.json as T
} else {
throw new AriesFrameworkError('No attachment data found in `json` or `base64` data fields.')
}
}

public addJws(jws: JwsGeneralFormat) {
// If no JWS yet, assign to current JWS
if (!this.data.jws) {
this.data.jws = jws
}
// Is already jws array, add to it
else if ('signatures' in this.data.jws) {
this.data.jws.signatures.push(jws)
}
// If already single JWS, transform to general jws format
else {
this.data.jws = {
signatures: [this.data.jws, jws],
}
}
}
}
Loading

0 comments on commit b6b9dcc

Please sign in to comment.