Skip to content

Commit

Permalink
Feat/template management (#76)
Browse files Browse the repository at this point in the history
* manage templates for verifier

Signed-off-by: Mirko Mollik <[email protected]>

* add template for issuer

Signed-off-by: Mirko Mollik <[email protected]>

* use only db as the template service implementation

Signed-off-by: Mirko Mollik <[email protected]>

* update metadata and templates

Signed-off-by: Mirko Mollik <[email protected]>

---------

Signed-off-by: Mirko Mollik <[email protected]>
  • Loading branch information
cre8 authored Jul 8, 2024
1 parent d2eaa71 commit 0f6b020
Show file tree
Hide file tree
Showing 41 changed files with 775 additions and 154 deletions.
2 changes: 1 addition & 1 deletion apps/demo/public/assets/issuer-config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"backendUrlPP": "http://localhost:3001",
"backendUrl": "http://localhost:3001",
"credentialId": "Identity",
"oidcUrl": "http://host.docker.internal:8080/realms/wallet",
"oidcClientId": "relying-party",
Expand Down
7 changes: 1 addition & 6 deletions apps/holder-app/src/app/scanner/scanner.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { CameraDevice, Html5Qrcode } from 'html5-qrcode';
import { MatMenuModule } from '@angular/material/menu';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { HttpClient } from '@angular/common/http';
import { CommonModule } from '@angular/common';
import {
VerifyRequestComponent,
Expand Down Expand Up @@ -42,7 +41,7 @@ export class ScannerComponent implements OnInit, OnDestroy {
loading = true;
url?: string;

constructor(private httpClient: HttpClient) {
constructor() {
if (
navigator.clipboard &&
typeof navigator.clipboard.readText !== 'undefined'
Expand Down Expand Up @@ -92,7 +91,6 @@ export class ScannerComponent implements OnInit, OnDestroy {
* Stop the scanner when leaving the page
*/
async ngOnDestroy(): Promise<void> {
console.log('destroying');
await this.stopScanning();
}

Expand Down Expand Up @@ -137,11 +135,8 @@ export class ScannerComponent implements OnInit, OnDestroy {
// handle the scanned code as you like, for example:
if (decodedText.startsWith('openid-credential-offer://')) {
this.showRequest(decodedText, 'receive');
// use a constant for the verification schema
await this.stopScanning();
} else if (decodedText.startsWith('openid://')) {
this.showRequest(decodedText, 'send');
await this.stopScanning();
} else {
alert("Scanned text doesn't match the expected format");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { OIDCClient } from './oidc-client';
import { ConfigService } from '@nestjs/config';
import { OnEvent } from '@nestjs/event-emitter';
import { USER_DELETED_EVENT, UserDeletedEvent } from '../auth.service';
import { UserDeletedEvent } from '../auth.service';
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
import { HttpService } from '@nestjs/axios';
import { firstValueFrom } from 'rxjs';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
BeforeInsert,
BeforeUpdate,
Column,
Entity,
PrimaryColumn,
} from 'typeorm';
import { Column, Entity, PrimaryColumn } from 'typeorm';
import type {
AuthenticatorTransportFuture,
CredentialDeviceType,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import {
ApiCreatedResponse,
ApiOAuth2,
Expand Down
14 changes: 8 additions & 6 deletions apps/holder-backend/src/app/oid4vc/oid4vp/oid4vp.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,14 @@ import {
import { SdJwtDecodedVerifiableCredentialWithKbJwtInput } from '@sphereon/pex';
import { v4 as uuid } from 'uuid';
import { Oid4vpParseRepsonse } from './dto/parse-response.dto';
import {
CredentialSelection,
SubmissionRequest,
} from './dto/submission-request.dto';
import { CredentialSelection } from './dto/submission-request.dto';
import { Oid4vpParseRequest } from './dto/parse-request.dto';
import { Session } from './session';
import { CompactSdJwtVc } from '@sphereon/ssi-types';
import { CredentialsService } from '../../credentials/credentials.service';
import { HistoryService } from '../../history/history.service';
import { KeysService } from '../../keys/keys.service';
import { JWkResolver } from '@credhub/relying-party-shared';

@Injectable()
export class Oid4vpService {
Expand All @@ -49,10 +47,13 @@ export class Oid4vpService {

//parse the uri
const parsedAuthReqURI = await op.parseAuthorizationRequestURI(data.url);
console.log('verify');
const verifiedAuthReqWithJWT: VerifiedAuthorizationRequest =
await op.verifyAuthorizationRequest(
parsedAuthReqURI.requestObjectJwt as string
parsedAuthReqURI.requestObjectJwt as string,
{}
);
console.log('verified');
const issuer =
(
verifiedAuthReqWithJWT.authorizationRequestPayload
Expand Down Expand Up @@ -232,6 +233,7 @@ export class Oid4vpService {
const alg = SigningAlgo.ES256;

const withSuppliedSignature = async (data: string | Uint8Array) => {
console.log('sign');
const signature = await this.keysService.sign(kid, user, {
data: data as string,
});
Expand All @@ -242,7 +244,7 @@ export class Oid4vpService {
.withExpiresIn(1000)
.withHasher(digest)
.withIssuer(ResponseIss.SELF_ISSUED_V2)
.addDidMethod('jwk')
.addResolver('jwk', new JWkResolver())
.withSuppliedSignature(withSuppliedSignature, did, kid, alg)
.withSupportedVersions(SupportedVersion.SIOPv2_D12_OID4VP_D18)
.build();
Expand Down
3 changes: 2 additions & 1 deletion apps/issuer-backend/src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
KeyModule,
OIDC_VALIDATION_SCHEMA,
} from '@credhub/relying-party-shared';
import { DB_VALIDATION_SCHEMA, DbModule } from './db/db.module';
import { DB_VALIDATION_SCHEMA, DbModule } from '@credhub/relying-party-shared';
import { CredentialsModule } from './credentials/credentials.module';
import { StatusModule } from './status/status.module';
import { ScheduleModule } from '@nestjs/schedule';
Expand All @@ -26,6 +26,7 @@ import { IssuerModule } from './issuer/issuer.module';
.valid('development', 'production')
.default('development'),
CREDENTIALS_FOLDER: Joi.string().required(),
//TODO: we only need this, when we configured datbase type, not file type
...DB_VALIDATION_SCHEMA,
...KEY_VALIDATION_SCHEMA,
...OIDC_VALIDATION_SCHEMA,
Expand Down
57 changes: 16 additions & 41 deletions apps/issuer-backend/src/app/issuer/issuer-data.service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { CredentialIssuerMetadataOptsV1_0_13 } from '@sphereon/oid4vci-common';
import { readdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { CredentialSchema } from './types.js';
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { TemplatesService } from '../templates/template.service';
import { MetadataService } from '../templates/metadata.service';

/**
* The issuer class is responsible for managing the credentials and the metadata of the issuer.
Expand All @@ -16,58 +15,34 @@ export class IssuerDataService {
*/
private metadata!: CredentialIssuerMetadataOptsV1_0_13;

/**
* The credentials supported by the issuer.
*/
private credentials: Map<string, CredentialSchema> = new Map();

constructor(private configSerivce: ConfigService) {
constructor(
private configSerivce: ConfigService,
private templatesService: TemplatesService,
private metadataService: MetadataService
) {
this.loadConfig();
}

public loadConfig() {
this.credentials.clear();
const folder = this.configSerivce.get('CREDENTIALS_FOLDER');

public async loadConfig() {
//instead of reading at the beginning, we could implement a read on demand.
this.metadata = JSON.parse(
readFileSync(join(folder, 'metadata.json'), 'utf-8')
) as CredentialIssuerMetadataOptsV1_0_13;
this.metadata.credential_issuer = this.configSerivce.get('ISSUER_BASE_URL');
this.metadata = await this.metadataService.getMetadata();

if (!this.metadata.credential_configurations_supported) {
this.metadata.credential_configurations_supported = {};
}
this.metadata.credential_issuer = this.configSerivce.get('ISSUER_BASE_URL');

const files = readdirSync(join(folder, 'credentials'));
for (const file of files) {
//TODO: we should validate the schema
const content = JSON.parse(
readFileSync(join(folder, 'credentials', file), 'utf-8')
) as CredentialSchema;
//check if an id is already used
if (this.credentials.has(content.schema.id as string)) {
throw new Error(
`The credential with the id ${content.schema.id} is already used.`
);
}
this.credentials.set(content.schema.id as string, content);
this.metadata.credential_configurations_supported[
content.schema.id as string
] = content.schema;
}
this.metadata.credential_configurations_supported =
this.templatesService.getSupported(await this.templatesService.listAll());
}

/**
* Returns the credential with the given id, throws an error if the credential is not supported.
* @param id
* @returns
*/
getCredential(id: string) {
async getCredential(id: string) {
if (this.configSerivce.get('CONFIG_RELOAD')) {
this.loadConfig();
}
const credential = this.credentials.get(id);
const credential = await this.templatesService.getOne(id);
if (!credential) {
throw new Error(`The credential with the id ${id} is not supported.`);
}
Expand All @@ -79,11 +54,11 @@ export class IssuerDataService {
* @param id
* @returns
*/
getDisclosureFrame(id: string) {
async getDisclosureFrame(id: string) {
if (this.configSerivce.get('CONFIG_RELOAD')) {
this.loadConfig();
}
const credential = this.credentials.get(id);
const credential = await this.templatesService.getOne(id);
if (!credential) {
throw new Error(`The credential with the id ${id} is not supported.`);
}
Expand Down
3 changes: 2 additions & 1 deletion apps/issuer-backend/src/app/issuer/issuer.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ import { IssuerController } from './issuer.controller';
import { WellKnownController } from './well-known/well-known.controller';
import { CredentialsModule } from '../credentials/credentials.module';
import { StatusModule } from '../status/status.module';
import { TemplatesModule } from '../templates/templates.module';

@Module({
imports: [CredentialsModule, StatusModule],
imports: [CredentialsModule, StatusModule, TemplatesModule],
controllers: [IssuerController, WellKnownController],
providers: [IssuerService, IssuerDataService],
})
Expand Down
11 changes: 6 additions & 5 deletions apps/issuer-backend/src/app/issuer/issuer.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ export class IssuerService implements OnModuleInit {
const credentialId = values.credentialId;
const sessionId = v4();
try {
const credential = this.issuerDataService.getCredential(credentialId);
const credential = await this.issuerDataService.getCredential(
credentialId
);
let exp: number | undefined;
// we either use the passed exp value or the ttl of the credential. If none is set, the credential will not expire.
if (values.exp) {
Expand Down Expand Up @@ -180,7 +182,7 @@ export class IssuerService implements OnModuleInit {
...(args.credentialDataSupplierInput as CredentialDataSupplierInput)
.credentialSubject,
//TODO: can be removed when correct type is set in PEX
status: status as any,
status: status as unknown as { idx: number; uri: string },
exp: args.credentialDataSupplierInput.exp,
};
return Promise.resolve({
Expand All @@ -192,10 +194,9 @@ export class IssuerService implements OnModuleInit {
/**
* Signer callback for the access token.
* @param jwt header and payload of the jwt
* @param kid key id that should be used for signing
* @returns signed jwt
*/
const signerCallback = async (jwt: Jwt, kid?: string): Promise<string> => {
const signerCallback = async (jwt: Jwt): Promise<string> => {
return this.keyService.signJWT(jwt.payload, {
...jwt.header,
alg: Alg.ES256,
Expand Down Expand Up @@ -237,7 +238,7 @@ export class IssuerService implements OnModuleInit {
> = async (args) => {
const jwt = await sdjwt.issue<SdJwtVcPayload>(
args.credential as unknown as SdJwtVcPayload,
this.issuerDataService.getDisclosureFrame(
await this.issuerDataService.getDisclosureFrame(
args.credential.vct as string
),
{ header: { kid: await this.keyService.getKid() } }
Expand Down
12 changes: 0 additions & 12 deletions apps/issuer-backend/src/app/issuer/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { DisclosureFrame } from '@sd-jwt/types';
import { CredentialConfigurationSupportedV1_0_13 } from '@sphereon/oid4vci-common';
import { JWK } from 'jose';

/**
Expand All @@ -12,13 +10,3 @@ export interface IssuerMetadata {
keys: JWK[];
};
}

/**
* The schema of the credential.
*/
export interface CredentialSchema {
schema: CredentialConfigurationSupportedV1_0_13;
sd: DisclosureFrame<Record<string, unknown | boolean>>;
// time to live in seconds, it will be added on the current time to get the expiration time.
ttl?: number;
}
45 changes: 45 additions & 0 deletions apps/issuer-backend/src/app/templates/dto/metadata.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import {
ImageInfo as IImageInfo,
MetadataDisplay as IMetadataDisplay,
} from '@sphereon/oid4vci-common';
import { Type } from 'class-transformer';
import { IsString, IsOptional, ValidateNested, IsArray } from 'class-validator';

export class ImageInfo implements IImageInfo {
[key: string]: unknown;
@IsString()
@IsOptional()
url?: string;
@IsString()
@IsOptional()
alt_text?: string;
}

class MetadataDisplay implements IMetadataDisplay {
[key: string]: unknown;
@IsString()
name: string;
@IsString()
@IsOptional()
locale?: string;
@ValidateNested()
@Type(() => ImageInfo)
logo?: ImageInfo;
@IsString()
@IsOptional()
description?: string;
@IsOptional()
@IsString()
background_color?: string;
@IsOptional()
@IsString()
text_color?: string;
}

export class Metadata {
@IsOptional()
@ValidateNested({ each: true })
@IsArray()
@Type(() => MetadataDisplay)
display?: MetadataDisplay[];
}
Loading

0 comments on commit 0f6b020

Please sign in to comment.