-
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.
feat: add support for signed attachments (#595)
Signed-off-by: Timo Glastra <timo@animo.id> BREAKING CHANGE: attachment method `getDataAsJson` is now located one level up. So instead of `attachment.data.getDataAsJson()` you should now call `attachment.getDataAsJson()`
- Loading branch information
1 parent
8e03f35
commit 1c9d6b9
Showing
17 changed files
with
360 additions
and
37 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([]) | ||
}) | ||
}) | ||
}) |
26 changes: 26 additions & 0 deletions
26
packages/core/src/crypto/__tests__/__fixtures__/didJwsz6Mkf.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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
export const SEED = '00000000000000000000000000000My2' | ||
export const VERKEY = 'kqa2HyagzfMAq42H5f9u3UMwnSBPQx2QfrSyXbUPxMn' | ||
|
||
export const DATA_JSON = { | ||
did: 'did', | ||
did_doc: { | ||
'@context': 'https://w3id.org/did/v1', | ||
service: [ | ||
{ | ||
id: 'did:example:123456789abcdefghi#did-communication', | ||
type: 'did-communication', | ||
priority: 0, | ||
recipientKeys: ['someVerkey'], | ||
routingKeys: [], | ||
serviceEndpoint: 'https://agent.example.com/', | ||
}, | ||
], | ||
}, | ||
} | ||
|
||
export const JWS_JSON = { | ||
header: { kid: 'did:key:z6MkfD6ccYE22Y9pHKtixeczk92MmMi2oJCP6gmNooZVKB9A' }, | ||
protected: | ||
'eyJhbGciOiJFZERTQSIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6IkN6cmtiNjQ1MzdrVUVGRkN5SXI4STgxUWJJRGk2MnNrbU41Rm41LU1zVkUifX0', | ||
signature: 'OsDP4FM8792J9JlessA9IXv4YUYjIGcIAnPPrEJmgxYomMwDoH-h2DMAF5YF2VtsHHyhGN_0HryDjWSEAZdYBQ', | ||
} |
28 changes: 28 additions & 0 deletions
28
packages/core/src/crypto/__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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
export const SEED = '00000000000000000000000000000My1' | ||
export const VERKEY = 'GjZWsBLgZCR18aL468JAT7w9CZRiBnpxUPPgyQxh4voa' | ||
|
||
export const DATA_JSON = { | ||
did: 'did', | ||
did_doc: { | ||
'@context': 'https://w3id.org/did/v1', | ||
service: [ | ||
{ | ||
id: 'did:example:123456789abcdefghi#did-communication', | ||
type: 'did-communication', | ||
priority: 0, | ||
recipientKeys: ['someVerkey'], | ||
routingKeys: [], | ||
serviceEndpoint: 'https://agent.example.com/', | ||
}, | ||
], | ||
}, | ||
} | ||
|
||
export const JWS_JSON = { | ||
header: { | ||
kid: 'did:key:z6MkvBpZTRb7tjuUF5AkmhG1JDV928hZbg5KAQJcogvhz9ax', | ||
}, | ||
protected: | ||
'eyJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa3ZCcFpUUmI3dGp1VUY1QWttaEcxSkRWOTI4aFpiZzVLQVFKY29ndmh6OWF4IiwiandrIjp7Imt0eSI6Ik9LUCIsImNydiI6IkVkMjU1MTkiLCJ4IjoiNmNaMmJaS21LaVVpRjlNTEtDVjhJSVlJRXNPTEhzSkc1cUJKOVNyUVlCayIsImtpZCI6ImRpZDprZXk6ejZNa3ZCcFpUUmI3dGp1VUY1QWttaEcxSkRWOTI4aFpiZzVLQVFKY29ndmh6OWF4In19', | ||
signature: 'eA3MPRpSTt5NR8EZkDNb849E9qfrlUm8-StWPA4kMp-qcH7oEc2-1En4fgpz_IWinEbVxCLbmKhWNyaTAuHNAg', | ||
} |
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.