Skip to content

Commit

Permalink
feat(sd-jwt-vc): Module for Issuer, Holder and verifier (openwallet-f…
Browse files Browse the repository at this point in the history
…oundation#1607)

Signed-off-by: Berend Sliedrecht <blu3beri@proton.me>
Signed-off-by: Martin Auer <martin.auer97@gmail.com>
  • Loading branch information
berendsliedrecht authored and auer-martin committed Nov 15, 2023
1 parent 7587b96 commit ae0f413
Show file tree
Hide file tree
Showing 27 changed files with 1,618 additions and 19 deletions.
5 changes: 1 addition & 4 deletions packages/core/src/crypto/jose/jwk/Jwk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,7 @@ export abstract class Jwk {
public use?: string

public toJson(): JwkJson {
return {
kty: this.kty,
use: this.use,
}
return { use: this.use, kty: this.kty }
}

public get key() {
Expand Down
9 changes: 2 additions & 7 deletions packages/core/src/crypto/jose/jwk/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
export {
getJwkFromJson,
getJwkFromKey,
getJwkClassFromJwaSignatureAlgorithm,
getJwkClassFromKeyType,
} from './transform'
export * from './transform'
export { Ed25519Jwk } from './Ed25519Jwk'
export { X25519Jwk } from './X25519Jwk'
export { P256Jwk } from './P256Jwk'
export { P384Jwk } from './P384Jwk'
export { P521Jwk } from './P521Jwk'
export { Jwk } from './Jwk'
export { Jwk, JwkJson } from './Jwk'
3 changes: 2 additions & 1 deletion packages/core/src/crypto/jose/jwk/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { JwkJson, Jwk } from './Jwk'
import type { Key } from '../../Key'
import type { JwaSignatureAlgorithm } from '../jwa'

import { AriesFrameworkError } from '../../../error'
import { KeyType } from '../../KeyType'
import { JwaCurve, JwaKeyType } from '../jwa'

Expand Down Expand Up @@ -37,7 +38,7 @@ export function getJwkFromKey(key: Key) {
if (key.keyType === KeyType.P384) return P384Jwk.fromPublicKey(key.publicKey)
if (key.keyType === KeyType.P521) return P521Jwk.fromPublicKey(key.publicKey)

throw new Error(`Cannot create JWK from key. Unsupported key with type '${key.keyType}'.`)
throw new AriesFrameworkError(`Cannot create JWK from key. Unsupported key with type '${key.keyType}'.`)
}

export function getJwkClassFromJwaSignatureAlgorithm(alg: JwaSignatureAlgorithm | string) {
Expand Down
14 changes: 11 additions & 3 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,18 +61,26 @@ export * from './modules/oob'
export * from './modules/dids'
export * from './modules/vc'
export * from './modules/cache'
export { JsonEncoder, JsonTransformer, isJsonObject, isValidJweStructure, TypedArrayEncoder, Buffer } from './utils'
export {
JsonEncoder,
JsonTransformer,
isJsonObject,
isValidJweStructure,
TypedArrayEncoder,
Buffer,
deepEquality,
} from './utils'
export * from './logger'
export * from './error'
export * from './wallet/error'
export { parseMessageType, IsValidMessageType, replaceLegacyDidSovPrefix } from './utils/messageType'
export type { Constructor } from './utils/mixins'
export type { Constructor, Constructable } from './utils/mixins'
export * from './agent/Events'
export * from './crypto/'

// TODO: clean up util exports
export { encodeAttachment, isLinkedAttachment } from './utils/attachment'
export { Hasher } from './utils/Hasher'
export { Hasher, HashName } from './utils/Hasher'
export { MessageValidator } from './utils/MessageValidator'
export { LinkedAttachment, LinkedAttachmentOptions } from './utils/LinkedAttachment'
import { parseInvitationUrl } from './utils/parseInvitation'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import type { RecordTags, TagsBase } from '../../../storage/BaseRecord'
import type { TagsBase } from '../../../storage/BaseRecord'

import { BaseRecord } from '../../../storage/BaseRecord'
import { uuid } from '../../../utils/uuid'

export type GenericRecordTags = TagsBase

export type BasicMessageTags = RecordTags<GenericRecord>

export interface GenericRecordStorageProps {
id?: string
createdAt?: Date
Expand Down
2 changes: 1 addition & 1 deletion packages/openid4vc-client/src/OpenId4VcClientModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export class OpenId4VcClientModule implements Module {
public readonly api = OpenId4VcClientApi

/**
* Registers the dependencies of the question answer module on the dependency manager.
* Registers the dependencies of the openid4vc-client module on the dependency manager.
*/
public register(dependencyManager: DependencyManager) {
// Warn about experimental module
Expand Down
57 changes: 57 additions & 0 deletions packages/sd-jwt-vc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<p align="center">
<br />
<img
alt="Hyperledger Aries logo"
src="https://raw.githubusercontent.com/hyperledger/aries-framework-javascript/aa31131825e3331dc93694bc58414d955dcb1129/images/aries-logo.png"
height="250px"
/>
</p>
<h1 align="center"><b>Aries Framework JavaScript Selective Disclosure JWT VC Module</b></h1>
<p align="center">
<a
href="https://raw.githubusercontent.com/hyperledger/aries-framework-javascript/main/LICENSE"
><img
alt="License"
src="https://img.shields.io/badge/License-Apache%202.0-blue.svg"
/></a>
<a href="https://www.typescriptlang.org/"
><img
alt="typescript"
src="https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg"
/></a>
<a href="https://www.npmjs.com/package/@aries-framework/sd-jwt-vc"
><img
alt="@aries-framework/sd-jwt-vc version"
src="https://img.shields.io/npm/v/@aries-framework/sd-jwt-vc"
/></a>
</p>
<br />

### Installation

Add the `sd-jwt-vc` module to your project.

```sh
yarn add @aries-framework/sd-jwt-vc
```

### Quick start

After the installation you can follow the [guide to setup your agent](https://aries.js.org/guides/0.4/getting-started/set-up) and add the following to your agent modules.

```ts
import { SdJwtVcModule } from '@aries-framework/sd-jwt-vc'

const agent = new Agent({
config: {
/* config */
},
dependencies: agentDependencies,
modules: {
sdJwtVc: new SdJwtVcModule(),
/* other custom modules */
},
})

await agent.initialize()
```
13 changes: 13 additions & 0 deletions packages/sd-jwt-vc/jest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Config } from '@jest/types'

import base from '../../jest.config.base'

import packageJson from './package.json'

const config: Config.InitialOptions = {
...base,
displayName: packageJson.name,
setupFilesAfterEnv: ['./tests/setup.ts'],
}

export default config
39 changes: 39 additions & 0 deletions packages/sd-jwt-vc/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@aries-framework/sd-jwt-vc",
"main": "build/index",
"types": "build/index",
"version": "0.4.2",
"files": [
"build"
],
"license": "Apache-2.0",
"publishConfig": {
"access": "public"
},
"homepage": "https://github.com/hyperledger/aries-framework-javascript/tree/main/packages/sd-jwt-vc",
"repository": {
"type": "git",
"url": "https://github.com/hyperledger/aries-framework-javascript",
"directory": "packages/sd-jwt-vc"
},
"scripts": {
"build": "yarn run clean && yarn run compile",
"clean": "rimraf ./build",
"compile": "tsc -p tsconfig.build.json",
"prepublishOnly": "yarn run build",
"test": "jest"
},
"dependencies": {
"@aries-framework/askar": "^0.4.2",
"@aries-framework/core": "^0.4.2",
"class-transformer": "0.5.1",
"class-validator": "0.14.0",
"jwt-sd": "^0.1.2"
},
"devDependencies": {
"@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^4.4.0",
"typescript": "~4.9.5"
}
}
93 changes: 93 additions & 0 deletions packages/sd-jwt-vc/src/SdJwtVcApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import type {
SdJwtVcCreateOptions,
SdJwtVcPresentOptions,
SdJwtVcReceiveOptions,
SdJwtVcVerifyOptions,
} from './SdJwtVcOptions'
import type { SdJwtVcVerificationResult } from './SdJwtVcService'
import type { SdJwtVcRecord } from './repository'
import type { Query } from '@aries-framework/core'

import { AgentContext, injectable } from '@aries-framework/core'

import { SdJwtVcService } from './SdJwtVcService'

/**
* @public
*/
@injectable()
export class SdJwtVcApi {
private agentContext: AgentContext
private sdJwtVcService: SdJwtVcService

public constructor(agentContext: AgentContext, sdJwtVcService: SdJwtVcService) {
this.agentContext = agentContext
this.sdJwtVcService = sdJwtVcService
}

public async create<Payload extends Record<string, unknown> = Record<string, unknown>>(
payload: Payload,
options: SdJwtVcCreateOptions<Payload>
): Promise<{ sdJwtVcRecord: SdJwtVcRecord; compact: string }> {
return await this.sdJwtVcService.create<Payload>(this.agentContext, payload, options)
}

/**
*
* Validates and stores an sd-jwt-vc from the perspective of an holder
*
*/
public async storeCredential(sdJwtVcCompact: string, options: SdJwtVcReceiveOptions): Promise<SdJwtVcRecord> {
return await this.sdJwtVcService.storeCredential(this.agentContext, sdJwtVcCompact, options)
}

/**
*
* Create a compact presentation of the sd-jwt.
* This presentation can be send in- or out-of-band to the verifier.
*
* Within the `options` field, you can supply the indicies of the disclosures you would like to share with the verifier.
* Also, whether to include the holder key binding.
*
*/
public async present(sdJwtVcRecord: SdJwtVcRecord, options: SdJwtVcPresentOptions): Promise<string> {
return await this.sdJwtVcService.present(this.agentContext, sdJwtVcRecord, options)
}

/**
*
* Verify an incoming sd-jwt. It will check whether everything is valid, but also returns parts of the validation.
*
* For example, you might still want to continue with a flow if not all the claims are included, but the signature is valid.
*
*/
public async verify<
Header extends Record<string, unknown> = Record<string, unknown>,
Payload extends Record<string, unknown> = Record<string, unknown>
>(
sdJwtVcCompact: string,
options: SdJwtVcVerifyOptions
): Promise<{ sdJwtVcRecord: SdJwtVcRecord<Header, Payload>; validation: SdJwtVcVerificationResult }> {
return await this.sdJwtVcService.verify<Header, Payload>(this.agentContext, sdJwtVcCompact, options)
}

public async getById(id: string): Promise<SdJwtVcRecord> {
return await this.sdJwtVcService.getCredentialRecordById(this.agentContext, id)
}

public async getAll(): Promise<Array<SdJwtVcRecord>> {
return await this.sdJwtVcService.getAllCredentialRecords(this.agentContext)
}

public async findAllByQuery(query: Query<SdJwtVcRecord>): Promise<Array<SdJwtVcRecord>> {
return await this.sdJwtVcService.findCredentialRecordsByQuery(this.agentContext, query)
}

public async remove(id: string) {
return await this.sdJwtVcService.removeCredentialRecord(this.agentContext, id)
}

public async update(sdJwtVcRecord: SdJwtVcRecord) {
return await this.sdJwtVcService.updateCredentialRecord(this.agentContext, sdJwtVcRecord)
}
}
3 changes: 3 additions & 0 deletions packages/sd-jwt-vc/src/SdJwtVcError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { AriesFrameworkError } from '@aries-framework/core'

export class SdJwtVcError extends AriesFrameworkError {}
35 changes: 35 additions & 0 deletions packages/sd-jwt-vc/src/SdJwtVcModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { DependencyManager, Module } from '@aries-framework/core'

import { AgentConfig } from '@aries-framework/core'

import { SdJwtVcApi } from './SdJwtVcApi'
import { SdJwtVcService } from './SdJwtVcService'
import { SdJwtVcRepository } from './repository'

/**
* @public
*/
export class SdJwtVcModule implements Module {
public readonly api = SdJwtVcApi

/**
* Registers the dependencies of the sd-jwt-vc module on the dependency manager.
*/
public register(dependencyManager: DependencyManager) {
// Warn about experimental module
dependencyManager
.resolve(AgentConfig)
.logger.warn(
"The '@aries-framework/sd-jwt-vc' module is experimental and could have unexpected breaking changes. When using this module, make sure to use strict versions for all @aries-framework packages."
)

// Api
dependencyManager.registerContextScoped(this.api)

// Services
dependencyManager.registerSingleton(SdJwtVcService)

// Repositories
dependencyManager.registerSingleton(SdJwtVcRepository)
}
}
44 changes: 44 additions & 0 deletions packages/sd-jwt-vc/src/SdJwtVcOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import type { HashName, JwaSignatureAlgorithm } from '@aries-framework/core'
import type { DisclosureFrame } from 'jwt-sd'

export type SdJwtVcCreateOptions<Payload extends Record<string, unknown> = Record<string, unknown>> = {
holderDidUrl: string
issuerDidUrl: string
jsonWebAlgorithm?: JwaSignatureAlgorithm
disclosureFrame?: DisclosureFrame<Payload>
hashingAlgorithm?: HashName
}

export type SdJwtVcReceiveOptions = {
issuerDidUrl: string
holderDidUrl: string
}

/**
* `includedDisclosureIndices` is not the best API, but it is the best alternative until something like `PEX` is supported
*/
export type SdJwtVcPresentOptions = {
jsonWebAlgorithm?: JwaSignatureAlgorithm
includedDisclosureIndices?: Array<number>

/**
* This information is received out-of-band from the verifier.
* The claims will be used to create a normal JWT, used for key binding.
*/
verifierMetadata: {
verifierDid: string
nonce: string
issuedAt: number
}
}

/**
* `requiredClaimKeys` is not the best API, but it is the best alternative until something like `PEX` is supported
*/
export type SdJwtVcVerifyOptions = {
holderDidUrl: string
challenge: {
verifierDid: string
}
requiredClaimKeys?: Array<string>
}
Loading

0 comments on commit ae0f413

Please sign in to comment.