-
Notifications
You must be signed in to change notification settings - Fork 200
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: use jws service to create and verify jws
Signed-off-by: Timo Glastra <timo@animo.id>
- Loading branch information
1 parent
afe7531
commit b6b9dcc
Showing
18 changed files
with
277 additions
and
489 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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[] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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([]) | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
...ent/__tests__/__fixtures__/didJwsz6Mkv.ts → ...pto/__tests__/__fixtures__/didJwsz6Mkv.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.