Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sd-jwt-vc): Module for Issuer, Holder and verifier #1607

Merged
merged 27 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d50a28d
chore(sd-jwt): project setup
berendsliedrecht Oct 12, 2023
cba3684
chore(sd-jwt): updated with api, module and service
berendsliedrecht Oct 12, 2023
f6042c0
feat(sd-jwt): create sd-jwt as an issuer
berendsliedrecht Oct 17, 2023
60ffc12
chore(sd-jwt): update deps
berendsliedrecht Oct 20, 2023
92f50ad
fix(sd-jwt): revamped the create API
berendsliedrecht Oct 24, 2023
6a1dc9a
feat(sd-jwt): implemented sdJwt.receive
berendsliedrecht Oct 25, 2023
4bf9709
improved verification
berendsliedrecht Oct 25, 2023
4d91c07
update jwt-sd package
berendsliedrecht Oct 25, 2023
106b9a1
removed useless ts-ignores
berendsliedrecht Oct 25, 2023
12945b6
fixed some minor typing issues
berendsliedrecht Oct 25, 2023
43ba3de
feat(sd-jwt): pretty claims on record and cleanup
berendsliedrecht Oct 26, 2023
acf3711
test(sd-jwt): included tests for the sd-jwt record
berendsliedrecht Oct 26, 2023
6404af5
fix(sd-jwt): stricten the tests
berendsliedrecht Oct 26, 2023
aec8e8a
feat(sd-jwt): changed API to take did urls
berendsliedrecht Oct 27, 2023
24f04ba
test(sd-jwt): include full e2e test
berendsliedrecht Oct 27, 2023
55a96c0
fix(sd-jwt): use the claim in the header
berendsliedrecht Oct 30, 2023
b857e4c
fix(sd-jwt): additional check for 'kty' claim
berendsliedrecht Oct 30, 2023
10831cb
feat(sd-jwt): store holder did on record
berendsliedrecht Oct 30, 2023
625f09b
feat(sd-jwt): include repository commands in the API
berendsliedrecht Oct 30, 2023
d697756
fix(sd-jwt): get the issuerDidUrl from the sd-jwt itself
berendsliedrecht Oct 31, 2023
7eed255
fix(sd-jwt): resolved most of the feedback
berendsliedrecht Nov 2, 2023
afdd0dc
fix(sd-jwt): removed ts-ignore and updated public API
berendsliedrecht Nov 2, 2023
270382d
fix(sd-jwt): made api more inline with w3c
berendsliedrecht Nov 2, 2023
8fe5b77
chore(sd-jwt): update jwt-sd to next patch version
berendsliedrecht Nov 2, 2023
c5063e1
feat(sd-jwt-vc): rename sd-jwt to sd-jwt-vc
berendsliedrecht Nov 2, 2023
d0b3c98
fix(sd-jwt-vc): remove cast
berendsliedrecht Nov 2, 2023
c95fe6a
fix(sd-jwt-vc): updated jwt-sd
berendsliedrecht Nov 3, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
berendsliedrecht marked this conversation as resolved.
Show resolved Hide resolved
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
58 changes: 58 additions & 0 deletions packages/sd-jwt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<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 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"
><img
alt="@aries-framework/sd-jwt version"
src="https://img.shields.io/npm/v/@aries-framework/sd-jwt"
/></a>

</p>
<br />

### Installation

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

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

### 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 { SdJwtModule } from '@aries-framework/sd-jwt'

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

await agent.initialize()
```
13 changes: 13 additions & 0 deletions packages/sd-jwt/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/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@aries-framework/sd-jwt",
"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",
"repository": {
"type": "git",
"url": "https://github.com/hyperledger/aries-framework-javascript",
"directory": "packages/sd-jwt"
},
"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.0"
},
berendsliedrecht marked this conversation as resolved.
Show resolved Hide resolved
"devDependencies": {
"@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1",
"reflect-metadata": "^0.1.13",
"rimraf": "^4.4.0",
"typescript": "~4.9.5"
}
}
88 changes: 88 additions & 0 deletions packages/sd-jwt/src/SdJwtApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import type { SdJwtCreateOptions, SdJwtPresentOptions, SdJwtReceiveOptions, SdJwtVerifyOptions } from './SdJwtOptions'
import type { SdJwtVcVerificationResult } from './SdJwtService'
import type { SdJwtRecord } from './repository'
import type { Query } from '@aries-framework/core'

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

import { SdJwtService } from './SdJwtService'

/**
* @public
*/
@injectable()
export class SdJwtApi {
private agentContext: AgentContext
private sdJwtService: SdJwtService

public constructor(agentContext: AgentContext, sdJwtService: SdJwtService) {
this.agentContext = agentContext
this.sdJwtService = sdJwtService
}

public async create<Payload extends Record<string, unknown> = Record<string, unknown>>(
payload: Payload,
options: SdJwtCreateOptions<Payload>
): Promise<{ sdJwtRecord: SdJwtRecord; compact: string }> {
return await this.sdJwtService.create<Payload>(this.agentContext, payload, options)
}

/**
*
* Validates and stores an sd-jwt from the perspective of an holder
*
*/
public async receive(sdJwtCompact: string, options: SdJwtReceiveOptions): Promise<SdJwtRecord> {
berendsliedrecht marked this conversation as resolved.
Show resolved Hide resolved
return await this.sdJwtService.receive(this.agentContext, sdJwtCompact, 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(sdJwtRecord: SdJwtRecord, options: SdJwtPresentOptions): Promise<string> {
return await this.sdJwtService.present(this.agentContext, sdJwtRecord, 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>
>(
sdJwtCompact: string,
options: SdJwtVerifyOptions
): Promise<{ sdJwtRecord: SdJwtRecord<Header, Payload>; validation: SdJwtVcVerificationResult }> {
return await this.sdJwtService.verify<Header, Payload>(this.agentContext, sdJwtCompact, options)
}

public async getCredentialRecordById(id: string): Promise<SdJwtRecord> {
return await this.sdJwtService.getCredentialRecordById(this.agentContext, id)
}
berendsliedrecht marked this conversation as resolved.
Show resolved Hide resolved

public async getAllCredentialRecords(): Promise<Array<SdJwtRecord>> {
return await this.sdJwtService.getAllCredentialRecords(this.agentContext)
}

public async findCredentialRecordsByQuery(query: Query<SdJwtRecord>): Promise<Array<SdJwtRecord>> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following the convention used in other module APIs (and services in general) we'd call it findAllCredentialRecordsByQuery, to make more explicit about the fact that we expect multiple records to be retrieved. Actually, in most modules we make naming simpler (e.g. findAllByQuery), but I see it's not the case here and W3cCredentialsApi (the API this module is based on I guess).

Of course if we change this here and leave W3cCredentialsApi as is, it would not be consistent. And if we change it in W3cCredentialsApi as well we'll introduce a breaking change. But I just wanted to point out this to bother you 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good point! I think we can change this in a separate PR which changes both the w3c and this one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am actually not sure what the best name would be here. I think findAllByQuery is the best as it is already scoped under sdJwt within the agent.modules api.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think all of them to findById, findAllByQuery. They will be removed in the future and I think they were too verbose before.

return await this.sdJwtService.findCredentialRecordsByQuery(this.agentContext, query)
}

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

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

export class SdJwtError extends AriesFrameworkError {}
35 changes: 35 additions & 0 deletions packages/sd-jwt/src/SdJwtModule.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 { SdJwtApi } from './SdJwtApi'
import { SdJwtService } from './SdJwtService'
import { SdJwtRepository } from './repository'

/**
* @public
*/
export class SdJwtModule implements Module {
public readonly api = SdJwtApi

/**
* Registers the dependencies of the sd-jwt module on the dependency manager.
*/
public register(dependencyManager: DependencyManager) {
// Warn about experimental module
dependencyManager
.resolve(AgentConfig)
.logger.warn(
"The '@aries-framework/sd-jwt' 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(SdJwtService)

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

export type SdJwtCreateOptions<Payload extends Record<string, unknown> = Record<string, unknown>> = {
karimStekelenburg marked this conversation as resolved.
Show resolved Hide resolved
holderDidUrl: string
issuerDidUrl: string
jsonWebAlgorithm?: JwaSignatureAlgorithm
disclosureFrame?: DisclosureFrame<Payload>
hashingAlgorithm?: HashName
}

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

/**
* `includedDisclosureIndices` is not the best API, but it is the best alternative until something like `PEX` is supported
*/
export type SdJwtPresentOptions = {
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 SdJwtVerifyOptions = {
holderDidUrl: string
verifierDid: string
berendsliedrecht marked this conversation as resolved.
Show resolved Hide resolved
requiredClaimKeys?: Array<string>
}
Loading
Loading