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: openid4vc and sd-jwt-vc support #1708

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ module.exports = {
},
},
{
files: ['demo/**'],
files: ['demo/**', 'demo-openid/**'],
rules: {
'no-console': 'off',
},
Expand All @@ -112,6 +112,7 @@ module.exports = {
'jest.*.ts',
'samples/**',
'demo/**',
'demo-openid/**',
'scripts/**',
'**/tests/**',
],
Expand Down
89 changes: 89 additions & 0 deletions demo-openid/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<h1 align="center"><b>DEMO</b></h1>

This is the Aries Framework Javascript demo. Walk through the AFJ flow yourself together with agents Alice and Faber.

Alice, a former student of Faber College, connects with the College, is issued a credential about her degree and then is asked by the College for a proof.

## Features

- ✅ Creating a connection
- ✅ Offering a credential
- ✅ Requesting a proof
- ✅ Sending basic messages

## Getting Started

### Platform Specific Setup

In order to use Aries Framework JavaScript some platform specific dependencies and setup is required. See our guides below to quickly set up you project with Aries Framework JavaScript for NodeJS, React Native and Electron.

- [NodeJS](https://aries.js.org/guides/getting-started/installation/nodejs)

### Run the demo

These are the steps for running the AFJ demo:

Clone the AFJ git repository:

```sh
git clone https://github.com/hyperledger/aries-framework-javascript.git
```

Open two different terminals next to each other and in both, go to the demo folder:

```sh
cd aries-framework-javascript/demo
```

Install the project in one of the terminals:

```sh
yarn install
```

In the left terminal run Alice:

```sh
yarn alice
```

In the right terminal run Faber:

```sh
yarn faber
```

### Usage

To set up a connection:

- Select 'receive connection invitation' in Alice and 'create connection invitation' in Faber
- Faber will print a invitation link which you then copy and paste to Alice
- You have now set up a connection!

To offer a credential:

- Select 'offer credential' in Faber
- Faber will start with registering a schema and the credential definition accordingly
- You have now send a credential offer to Alice!
- Go to Alice to accept the incoming credential offer by selecting 'yes'.

To request a proof:

- Select 'request proof' in Faber
- Faber will create a new proof attribute and will then send a proof request to Alice!
- Go to Alice to accept the incoming proof request

To send a basic message:

- Select 'send message' in either one of the Agents
- Type your message and press enter
- Message sent!

Exit:

- Select 'exit' to shutdown the agent.

Restart:

- Select 'restart', to shutdown the current agent and start a new one
36 changes: 36 additions & 0 deletions demo-openid/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
{
"name": "afj-demo-openid",
"version": "1.0.0",
"private": true,
"repository": {
"type": "git",
"url": "https://github.com/hyperledger/aries-framework-javascript",
"directory": "demo-openid/"
},
"license": "Apache-2.0",
"scripts": {
"issuer": "ts-node src/IssuerInquirer.ts",
"holder": "ts-node src/HolderInquirer.ts",
"verifier": "ts-node src/VerifierInquirer.ts",
"refresh": "rm -rf ./node_modules ./yarn.lock && yarn"
},
"dependencies": {
"@aries-framework/openid4vc": "*",
TimoGlastra marked this conversation as resolved.
Show resolved Hide resolved
"@hyperledger/anoncreds-nodejs": "^0.2.0-dev.4",
"@hyperledger/aries-askar-nodejs": "^0.2.0-dev.1",
"@hyperledger/indy-vdr-nodejs": "^0.2.0-dev.5",
"express": "^4.18.1",
"inquirer": "^8.2.5"
},
"devDependencies": {
"@aries-framework/askar": "*",
"@aries-framework/core": "*",
"@aries-framework/node": "*",
"@types/express": "^4.17.13",
"@types/figlet": "^1.5.4",
"@types/inquirer": "^8.2.6",
"clear": "^0.1.0",
"figlet": "^1.5.2",
"ts-node": "^10.4.0"
}
}
61 changes: 61 additions & 0 deletions demo-openid/src/BaseAgent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { InitConfig, KeyDidCreateOptions, ModulesMap, VerificationMethod } from '@aries-framework/core'
import type { Express } from 'express'

import { Agent, DidKey, HttpOutboundTransport, KeyType, TypedArrayEncoder } from '@aries-framework/core'
import { HttpInboundTransport, agentDependencies } from '@aries-framework/node'
import express from 'express'

import { greenText } from './OutputClass'

export class BaseAgent<AgentModules extends ModulesMap> {
public app: Express
public port: number
public name: string
public config: InitConfig
public agent: Agent<AgentModules>
public did!: string
public didKey!: DidKey
public kid!: string
public verificationMethod!: VerificationMethod

public constructor({ port, name, modules }: { port: number; name: string; modules: AgentModules }) {
this.name = name
this.port = port
this.app = express()

const config = {
label: name,
walletConfig: { id: name, key: name },
} satisfies InitConfig

this.config = config

this.agent = new Agent({ config, dependencies: agentDependencies, modules })

const httpInboundTransport = new HttpInboundTransport({ app: this.app, port: this.port })
const httpOutboundTransport = new HttpOutboundTransport()

this.agent.registerInboundTransport(httpInboundTransport)
this.agent.registerOutboundTransport(httpOutboundTransport)
}

public async initializeAgent(secretPrivateKey: string) {
await this.agent.initialize()

const didCreateResult = await this.agent.dids.create<KeyDidCreateOptions>({
method: 'key',
options: { keyType: KeyType.Ed25519 },
secret: { privateKey: TypedArrayEncoder.fromString(secretPrivateKey) },
})

this.did = didCreateResult.didState.did as string
this.didKey = DidKey.fromDid(this.did)
this.kid = `${this.did}#${this.didKey.key.fingerprint}`

const verificationMethod = didCreateResult.didState.didDocument?.dereferenceKey(this.kid, ['authentication'])
if (!verificationMethod) throw new Error('No verification method found')
this.verificationMethod = verificationMethod

console.log(greenText(`\nAgent ${this.name} created!\n`))
}
}
55 changes: 55 additions & 0 deletions demo-openid/src/BaseInquirer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { prompt } from 'inquirer'

import { Title } from './OutputClass'

export enum ConfirmOptions {
Yes = 'yes',
No = 'no',
}

export class BaseInquirer {
public optionsInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] }
public inputInquirer: { type: string; prefix: string; name: string; message: string; choices: string[] }

public constructor() {
this.optionsInquirer = {
type: 'list',
prefix: '',
name: 'options',
message: '',
choices: [],
}

this.inputInquirer = {
type: 'input',
prefix: '',
name: 'input',
message: '',
choices: [],
}
}

public inquireOptions(promptOptions: string[]) {
this.optionsInquirer.message = Title.OptionsTitle
this.optionsInquirer.choices = promptOptions
return this.optionsInquirer
}

public inquireInput(title: string) {
this.inputInquirer.message = title
return this.inputInquirer
}

public inquireConfirmation(title: string) {
this.optionsInquirer.message = title
this.optionsInquirer.choices = [ConfirmOptions.Yes, ConfirmOptions.No]
return this.optionsInquirer
}

public async inquireMessage() {
this.inputInquirer.message = Title.MessageTitle
const message = await prompt([this.inputInquirer])

return message.input[0] === 'q' ? null : message.input
}
}
104 changes: 104 additions & 0 deletions demo-openid/src/Holder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type {
OpenId4VciResolvedCredentialOffer,
OpenId4VcSiopResolvedAuthorizationRequest,
} from '@aries-framework/openid4vc'

import { AskarModule } from '@aries-framework/askar'
import {
W3cJwtVerifiableCredential,
W3cJsonLdVerifiableCredential,
DifPresentationExchangeService,
} from '@aries-framework/core'
import { OpenId4VcHolderModule } from '@aries-framework/openid4vc'
import { ariesAskar } from '@hyperledger/aries-askar-nodejs'

import { BaseAgent } from './BaseAgent'
import { Output } from './OutputClass'

function getOpenIdHolderModules() {
return {
askar: new AskarModule({ ariesAskar }),
openId4VcHolder: new OpenId4VcHolderModule(),
} as const
}

export class Holder extends BaseAgent<ReturnType<typeof getOpenIdHolderModules>> {
public constructor(port: number, name: string) {
super({ port, name, modules: getOpenIdHolderModules() })
}

public static async build(): Promise<Holder> {
const holder = new Holder(3000, 'OpenId4VcHolder ' + Math.random().toString())
await holder.initializeAgent('96213c3d7fc8d4d6754c7a0fd969598e')

return holder
}

public async resolveCredentialOffer(credentialOffer: string) {
return await this.agent.modules.openId4VcHolder.resolveCredentialOffer(credentialOffer)
}

public async requestAndStoreCredentials(
resolvedCredentialOffer: OpenId4VciResolvedCredentialOffer,
credentialsToRequest: string[]
) {
const credentials = await this.agent.modules.openId4VcHolder.acceptCredentialOfferUsingPreAuthorizedCode(
resolvedCredentialOffer,
{
credentialsToRequest,
// TODO: add jwk support for holder binding
credentialBindingResolver: async () => ({
method: 'did',
didUrl: this.verificationMethod.id,
}),
}
)

const storedCredentials = await Promise.all(
credentials.map((credential) => {
if (credential instanceof W3cJwtVerifiableCredential || credential instanceof W3cJsonLdVerifiableCredential) {
return this.agent.w3cCredentials.storeCredential({ credential })
} else {
return this.agent.sdJwtVc.store(credential.compact)
}
})
)

return storedCredentials
}

public async resolveProofRequest(proofRequest: string) {
const resolvedProofRequest = await this.agent.modules.openId4VcHolder.resolveSiopAuthorizationRequest(proofRequest)

return resolvedProofRequest
}

public async acceptPresentationRequest(resolvedPresentationRequest: OpenId4VcSiopResolvedAuthorizationRequest) {
const presentationExchangeService = this.agent.dependencyManager.resolve(DifPresentationExchangeService)

if (!resolvedPresentationRequest.presentationExchange) {
throw new Error('Missing presentation exchange on resolved authorization request')
}

const submissionResult = await this.agent.modules.openId4VcHolder.acceptSiopAuthorizationRequest({
authorizationRequest: resolvedPresentationRequest.authorizationRequest,
presentationExchange: {
credentials: presentationExchangeService.selectCredentialsForRequest(
resolvedPresentationRequest.presentationExchange.credentialsForRequest
),
},
})

return submissionResult.serverResponse
}

public async exit() {
console.log(Output.Exit)
await this.agent.shutdown()
process.exit(0)
}

public async restart() {
await this.agent.shutdown()
}
}
Loading
Loading