From e5666457acd97317feda6bd7d0e2190a0150396c Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Wed, 1 May 2024 11:54:44 +0200 Subject: [PATCH 1/6] Add generic key storage Fixes #12 Signed-off-by: Mirko Mollik --- apps/backend/src/app.module.ts | 5 +- apps/backend/src/keys/db-keys.service.ts | 122 ++++++++ apps/backend/src/keys/dto/key-response.dto.ts | 1 - .../backend/src/keys/dto/proof-request.dto.ts | 2 +- apps/backend/src/keys/keys.controller.ts | 136 --------- apps/backend/src/keys/keys.module.ts | 60 +++- apps/backend/src/keys/keys.service.ts | 182 +----------- apps/backend/src/keys/vault-keys.service.ts | 268 ++++++++++++++++++ apps/backend/src/oid4vc/oid4vc.module.ts | 3 +- .../src/oid4vc/oid4vci/oid4vci.service.ts | 25 +- .../src/oid4vc/oid4vp/oid4vp.service.ts | 6 +- docker-compose.yml | 19 +- 12 files changed, 484 insertions(+), 345 deletions(-) create mode 100644 apps/backend/src/keys/db-keys.service.ts delete mode 100644 apps/backend/src/keys/keys.controller.ts create mode 100644 apps/backend/src/keys/vault-keys.service.ts diff --git a/apps/backend/src/app.module.ts b/apps/backend/src/app.module.ts index 7d7ac502..7dcd8836 100644 --- a/apps/backend/src/app.module.ts +++ b/apps/backend/src/app.module.ts @@ -4,7 +4,7 @@ import * as Joi from 'joi'; import { AuthModule, KEYCLOAK_VALIDATION_SCHEMA } from './auth/auth.module'; import { CredentialsModule } from './credentials/credentials.module'; import { DB_VALIDATION_SCHEMA, DbModule } from './db/db.module'; -import { KeysModule } from './keys/keys.module'; +import { KEY_VALIDATION_SCHEMA, KeysModule } from './keys/keys.module'; import { Oid4vcModule } from './oid4vc/oid4vc.module'; import { HistoryModule } from './history/history.module'; import { AppController } from './app.controller'; @@ -15,12 +15,13 @@ import { SettingsModule } from './settings/settings.module'; ConfigModule.forRoot({ validationSchema: Joi.object({ ...KEYCLOAK_VALIDATION_SCHEMA, + ...KEY_VALIDATION_SCHEMA, ...DB_VALIDATION_SCHEMA, }), }), AuthModule, DbModule, - KeysModule, + KeysModule.forRootSync(), CredentialsModule, Oid4vcModule, HistoryModule, diff --git a/apps/backend/src/keys/db-keys.service.ts b/apps/backend/src/keys/db-keys.service.ts new file mode 100644 index 00000000..72f889c7 --- /dev/null +++ b/apps/backend/src/keys/db-keys.service.ts @@ -0,0 +1,122 @@ +import { + type ECKeyPairOptions, + type JsonWebKey, + generateKeyPairSync, +} from 'node:crypto'; +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import { SdJwtKbJwtInput } from '@sphereon/pex/dist/main/lib'; +import { + type JWTHeaderParameters, + type JWTPayload, + type KeyLike as JoseKeyLike, + SignJWT, + importJWK, +} from 'jose'; +import { Repository } from 'typeorm'; +import { CreateKey } from './dto/create-key.dto'; +import { KeyResponse } from './dto/key-response.dto'; +import { ProofRequest } from './dto/proof-request.dto'; +import { SignRequest } from './dto/sign-request.dto'; +import { Key } from './entities/key.entity'; +import { KeysService } from './keys.service'; + +@Injectable() +export class DbKeysService extends KeysService { + constructor(@InjectRepository(Key) private keyRepository: Repository) { + super(); + } + + async create(createKeyDto: CreateKey, user: string): Promise { + const curves = { + ES256: 'P-256', + ES384: 'P-384', + ES512: 'P-521', + }; + const { privateKey, publicKey } = generateKeyPairSync('ec', { + namedCurve: curves[createKeyDto.type], + publicKeyEncoding: { + type: 'spki', + format: 'jwk', + }, + privateKeyEncoding: { + type: 'pkcs8', + format: 'jwk', + }, + } as ECKeyPairOptions<'jwk', 'jwk'>); + + const key = new Key(); + key.user = user; + key.privateKey = privateKey as unknown as JsonWebKey; + key.publicKey = publicKey as unknown as JsonWebKey; + const entity = await this.keyRepository.save(key); + return { + id: entity.id, + publicKey: publicKey as unknown as JsonWebKey, + }; + } + + firstOrCreate(user: string) { + return this.keyRepository.findOne({ where: { user } }).then((key) => { + if (!key) { + return this.create({ type: 'ES256' }, user); + } + return key; + }); + } + + sign(id: string, user: string, value: SignRequest) { + return this.keyRepository + .findOneOrFail({ where: { id, user } }) + .then(async (key) => { + const jwk = await importJWK(key.privateKey, 'ES256'); + const header = JSON.parse( + this.decodeBase64Url(value.data.split('.')[0]) + ) as JWTHeaderParameters; + const payload = JSON.parse( + this.decodeBase64Url(value.data.split('.')[1]) + ) as JWTPayload; + const jwt = await new SignJWT(payload) + .setProtectedHeader(header) + .sign(jwk); + return jwt.split('.')[2]; + }); + } + + async proof(user: string, value: ProofRequest) { + return this.keyRepository + .findOneOrFail({ + where: { id: value.kid, user }, + }) + .then(async (key) => { + //TODO: add the key id when the key is interset into the database. For this the primary get has to be generated first + key.publicKey.kid = key.id; + + const jwk = await importJWK(key.privateKey, 'ES256'); + return new SignJWT({ ...value.payload }) + .setProtectedHeader({ + alg: 'ES256', + typ: 'openid4vci-proof+jwt', + jwk: key.publicKey, + }) + .setIssuedAt() + .setExpirationTime('2h') + .sign(jwk); + }); + } + + public async signkbJwt( + user: string, + kid: string, + kbJwt: SdJwtKbJwtInput, + aud: string + ) { + const key = await this.keyRepository.findOneOrFail({ + where: { id: kid, user }, + }); + const jwk = await importJWK(key.privateKey, 'ES256'); + return new SignJWT({ ...kbJwt.payload, aud }) + .setProtectedHeader({ typ: kbJwt.header.typ, alg: 'ES256' }) + .sign(jwk); + } +} diff --git a/apps/backend/src/keys/dto/key-response.dto.ts b/apps/backend/src/keys/dto/key-response.dto.ts index 281666b2..75bd11c8 100644 --- a/apps/backend/src/keys/dto/key-response.dto.ts +++ b/apps/backend/src/keys/dto/key-response.dto.ts @@ -1,4 +1,3 @@ -import { JsonWebKey } from 'node:crypto'; export class KeyResponse { /** * Id of the key diff --git a/apps/backend/src/keys/dto/proof-request.dto.ts b/apps/backend/src/keys/dto/proof-request.dto.ts index 5a8cade4..600002f1 100644 --- a/apps/backend/src/keys/dto/proof-request.dto.ts +++ b/apps/backend/src/keys/dto/proof-request.dto.ts @@ -4,7 +4,7 @@ import { IsObject, IsOptional, IsString } from 'class-validator'; export class ProofRequest { @IsString() @IsOptional() - kid?: string; + kid: string; @IsObject() payload: JWTPayload; diff --git a/apps/backend/src/keys/keys.controller.ts b/apps/backend/src/keys/keys.controller.ts deleted file mode 100644 index b22c757f..00000000 --- a/apps/backend/src/keys/keys.controller.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { - Body, - ConflictException, - Controller, - Delete, - Get, - Param, - Post, - UseGuards, -} from '@nestjs/common'; -import { - ApiBody, - ApiCreatedResponse, - ApiOAuth2, - ApiOperation, - ApiTags, -} from '@nestjs/swagger'; -import { AuthGuard, AuthenticatedUser } from 'nest-keycloak-connect'; -import { KeycloakUser } from 'src/auth/user'; -import { CreateKey } from './dto/create-key.dto'; -import { KeyResponse } from './dto/key-response.dto'; -import { ProofRequest } from './dto/proof-request.dto'; -import { SignRequest } from './dto/sign-request.dto'; -import { VerifyRequest } from './dto/verify-request.dto'; -import { KeysService } from './keys.service'; - -@UseGuards(AuthGuard) -@ApiOAuth2([]) -@ApiTags('keys') -@Controller('keys') -export class KeysController { - constructor(private keysService: KeysService) {} - - /** - * Create a new key - */ - @ApiOperation({ summary: 'create a new key' }) - @ApiBody({ type: CreateKey }) - @ApiCreatedResponse({ description: 'Key created successfully' }) - @Post() - create( - @Body() createKeyDto: CreateKey, - @AuthenticatedUser() user: KeycloakUser - ) { - return this.keysService.create(createKeyDto, user.sub); - } - - /** - * Get all keys - * @param user - * @returns - */ - @ApiOperation({ summary: 'get all keys' }) - @Get() - findAll(@AuthenticatedUser() user: KeycloakUser): Promise { - return this.keysService.findAll(user.sub); - } - - @ApiOperation({ - summary: 'proof a message', - }) - @Post('proof') - proof(@Body() value: ProofRequest, @AuthenticatedUser() user: KeycloakUser) { - return this.keysService.proof(user.sub, value); - } - - /** - * - * @param id - * @param user - * @returns - */ - @ApiOperation({ summary: 'get a key' }) - @Get(':id') - findOne( - @Param('id') id: string, - @AuthenticatedUser() user: KeycloakUser - ): Promise { - return this.keysService.findOne(id, user.sub).catch(() => { - throw new ConflictException('Key not found'); - }); - } - - /** - * Signs a message with the given key. - * @param id - * @param value - * @param user - * @returns - */ - @ApiOperation({ - summary: 'sign a message', - description: 'Sign a message with the given key reference.', - }) - @Post(':id/sign') - sign( - @Param('id') id: string, - @Body() value: SignRequest, - @AuthenticatedUser() user: KeycloakUser - ) { - return this.keysService.sign(id, user.sub, value); - } - - /** - * Verifies the signature of a message with the given key. - * @param id - * @param value - * @param user - * @returns - */ - @ApiOperation({ - summary: 'verify a message', - description: - 'Verifies the signature of a message with the given key reference.', - }) - @Post(':id/verify') - verify( - @Param('id') id: string, - @Body() value: VerifyRequest, - @AuthenticatedUser() user: KeycloakUser - ) { - return this.keysService.verify(id, user.sub, value); - } - - /** - * - * @param id - * @param user - * @returns - */ - @ApiOperation({ summary: 'delete a key' }) - @Delete(':id') - remove(@Param('id') id: string, @AuthenticatedUser() user: KeycloakUser) { - return this.keysService.remove(id, user.sub).then(() => ({ id })); - } -} diff --git a/apps/backend/src/keys/keys.module.ts b/apps/backend/src/keys/keys.module.ts index a4ed8e5e..9d7ce81b 100644 --- a/apps/backend/src/keys/keys.module.ts +++ b/apps/backend/src/keys/keys.module.ts @@ -1,13 +1,53 @@ -import { Module } from '@nestjs/common'; +import { DynamicModule, Global, Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Key } from './entities/key.entity'; -import { KeysController } from './keys.controller'; -import { KeysService } from './keys.service'; +import { DbKeysService } from './db-keys.service'; +import { VaultKeysService } from './vault-keys.service'; +import { HttpModule, HttpService } from '@nestjs/axios'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import * as Joi from 'joi'; +import { DataSource } from 'typeorm'; -@Module({ - imports: [TypeOrmModule.forFeature([Key])], - controllers: [KeysController], - providers: [KeysService], - exports: [KeysService], -}) -export class KeysModule {} +export const KEY_VALIDATION_SCHEMA = { + KM_TYPE: Joi.string().valid('db', 'vault').default('db'), + VAULT_URL: Joi.string().when('KM_TYPE', { + is: 'vault', + // biome-ignore lint/suspicious/noThenProperty: + then: Joi.required(), + otherwise: Joi.optional(), + }), + VAULT_TOKEN: Joi.string().when('KM_TYPE', { + is: 'vault', + // biome-ignore lint/suspicious/noThenProperty: + then: Joi.required(), + otherwise: Joi.optional(), + }), +}; +@Global() +@Module({}) +// biome-ignore lint/complexity/noStaticOnlyClass: +export class KeysModule { + static forRootSync(): DynamicModule { + return { + module: KeysModule, + imports: [TypeOrmModule.forFeature([Key]), HttpModule, ConfigModule], + providers: [ + { + provide: 'KeyService', + useFactory: ( + configService: ConfigService, + httpService: HttpService, + dataSource: DataSource + ) => { + const kmType = configService.get('KM_TYPE'); + return kmType === 'vault' + ? new VaultKeysService(httpService, configService) + : new DbKeysService(dataSource.getRepository(Key)); + }, + inject: [ConfigService, HttpService, DataSource], + }, + ], + exports: ['KeyService'], + }; + } +} diff --git a/apps/backend/src/keys/keys.service.ts b/apps/backend/src/keys/keys.service.ts index 6d3de2aa..f7a03cdd 100644 --- a/apps/backend/src/keys/keys.service.ts +++ b/apps/backend/src/keys/keys.service.ts @@ -1,162 +1,24 @@ -import { - type ECKeyPairOptions, - type JsonWebKey, - type KeyLike, - createVerify, - generateKeyPairSync, -} from 'node:crypto'; -import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; import { SdJwtKbJwtInput } from '@sphereon/pex/dist/main/lib'; -import { - type JWTHeaderParameters, - type JWTPayload, - type KeyLike as JoseKeyLike, - SignJWT, - importJWK, -} from 'jose'; -import { Repository } from 'typeorm'; import { CreateKey } from './dto/create-key.dto'; import { KeyResponse } from './dto/key-response.dto'; import { ProofRequest } from './dto/proof-request.dto'; import { SignRequest } from './dto/sign-request.dto'; -import { VerifyRequest } from './dto/verify-request.dto'; -import { VerifyResponse } from './dto/verify-response.dto'; -import { Key } from './entities/key.entity'; -@Injectable() -export class KeysService { - constructor(@InjectRepository(Key) private keyRepository: Repository) {} +export abstract class KeysService { + abstract create(createKeyDto: CreateKey, user: string): Promise; - async create(createKeyDto: CreateKey, user: string): Promise { - const curves = { - ES256: 'P-256', - ES384: 'P-384', - ES512: 'P-521', - }; - const { privateKey, publicKey } = generateKeyPairSync('ec', { - namedCurve: curves[createKeyDto.type], - publicKeyEncoding: { - type: 'spki', - format: 'jwk', - }, - privateKeyEncoding: { - type: 'pkcs8', - format: 'jwk', - }, - } as ECKeyPairOptions<'jwk', 'jwk'>); + abstract firstOrCreate(user: string): Promise; - const key = new Key(); - key.user = user; - key.privateKey = privateKey as unknown as JsonWebKey; - key.publicKey = publicKey as unknown as JsonWebKey; - const entity = await this.keyRepository.save(key); - return { - id: entity.id, - publicKey: publicKey as unknown as JsonWebKey, - }; - } - - findAll(user: string) { - return this.keyRepository.find({ where: { user } }).then((keys) => { - return keys.map((key) => { - return { - id: key.id, - publicKey: key.publicKey, - }; - }); - }); - } - - firstOrCreate(user: string) { - return this.keyRepository.findOne({ where: { user } }).then((key) => { - if (!key) { - return this.create({ type: 'ES256' }, user); - } - return key; - }); - } - - findOne(id: string, user: string) { - return this.keyRepository - .findOneOrFail({ where: { id, user } }) - .then((key) => ({ - id: key.id, - publicKey: key.publicKey, - })); - } - - remove(id: string, user: string) { - return this.keyRepository.delete({ id, user }); - } - - sign(id: string, user: string, value: SignRequest) { - return this.keyRepository - .findOneOrFail({ where: { id, user } }) - .then(async (key) => { - const jwk = await importJWK(key.privateKey, 'ES256'); - const header = JSON.parse( - this.decodeBase64Url(value.data.split('.')[0]) - ) as JWTHeaderParameters; - const payload = JSON.parse( - this.decodeBase64Url(value.data.split('.')[1]) - ) as JWTPayload; - const jwt = await new SignJWT(payload) - .setProtectedHeader(header) - .sign(jwk); - return jwt.split('.')[2]; - }); - } + abstract sign(id: string, user: string, value: SignRequest): Promise; - decodeBase64Url(data: string) { - return Buffer.from(data, 'base64url').toString(); - } - - async proof(user: string, value: ProofRequest) { - let key: Key; - if (value.kid) { - key = await this.keyRepository.findOneOrFail({ - where: { id: value.kid, user }, - }); - } else { - const newKey = await this.create({ type: 'ES256' }, user); - key = await this.keyRepository.findOneOrFail({ - where: { id: newKey.id }, - }); - } - //TODO: add the key id when the key is interset into the database. For this the primary get has to be generated first - key.publicKey.kid = key.id; + abstract proof(user: string, value: ProofRequest): Promise; - const jwk = await importJWK(key.privateKey, 'ES256'); - return { - jwt: await new SignJWT({ ...value.payload }) - .setProtectedHeader({ - alg: 'ES256', - typ: 'openid4vci-proof+jwt', - jwk: key.publicKey, - }) - .setIssuedAt() - .setExpirationTime('2h') - .sign(jwk), - }; - } - - public async signkbJwt( + abstract signkbJwt( user: string, kid: string, kbJwt: SdJwtKbJwtInput, aud: string - ) { - const key = await this.keyRepository.findOneOrFail({ - where: { id: kid, user }, - }); - const jwk = await importJWK(key.privateKey, 'ES256'); - const jwt = await new SignJWT({ ...kbJwt.payload, aud }) - .setProtectedHeader({ typ: kbJwt.header.typ, alg: 'ES256' }) - .sign(jwk); - return jwt; - } - + ): Promise; /** * Encodes a public key as a DID JWK. * @param key @@ -168,33 +30,11 @@ export class KeysService { } /** - * Decodes a DID JWK to a public key. - * @param did + * Decodes a base64url to a string. + * @param data * @returns */ - decodeDidJWK(did: string) { - return JSON.parse( - Buffer.from(did.split('#')[0].split(':')[2], 'base64url').toString() - ) as JsonWebKey; - } - - verify( - id: string, - user: string, - value: VerifyRequest - ): Promise { - return this.keyRepository - .findOneOrFail({ where: { id, user } }) - .then(async (key) => { - const jwk: KeyLike = (await importJWK( - key.publicKey, - 'ES256' - )) as KeyLike; - const verify = createVerify(value.hashAlgorithm || 'sha256'); - verify.update(value.data); - return { - valid: verify.verify(jwk, value.signature, 'base64url'), - }; - }); + protected decodeBase64Url(data: string) { + return Buffer.from(data, 'base64url').toString(); } } diff --git a/apps/backend/src/keys/vault-keys.service.ts b/apps/backend/src/keys/vault-keys.service.ts new file mode 100644 index 00000000..7f11c36f --- /dev/null +++ b/apps/backend/src/keys/vault-keys.service.ts @@ -0,0 +1,268 @@ +import { Injectable } from '@nestjs/common'; +import { KeysService } from './keys.service'; +import { SdJwtKbJwtInput } from '@sphereon/pex/dist/main/lib'; +import { CreateKey } from './dto/create-key.dto'; +import { KeyResponse } from './dto/key-response.dto'; +import { ProofRequest } from './dto/proof-request.dto'; +import { SignRequest } from './dto/sign-request.dto'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; +import { + importSPKI, + KeyLike, + exportJWK, + jwtVerify, + importJWK, + JWK, +} from 'jose'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class VaultKeysService extends KeysService { + // url to the vault instance + private vaultUrl: string; + // headers for the vault api + private headers: { headers: { 'X-Vault-Token': string } }; + + constructor( + private httpService: HttpService, + private configService: ConfigService + ) { + super(); + this.vaultUrl = this.configService.get('VAULT_URL'); + this.headers = { + headers: { + 'X-Vault-Token': this.configService.get('VAULT_TOKEN'), + }, + }; + } + + /** + * Creates a new keypair in the vault. + * @param createKeyDto + * @param user + * @returns + */ + async create(createKeyDto: CreateKey, user: string): Promise { + if (createKeyDto.type !== 'ES256') { + throw new Error('Only ES256 is supported'); + } + //TODO: the key id should not be the user id since a user can have multiple keys. Therefore we need a one to many mapping. Also configure that an existing key can not be overwritten. + const res = await firstValueFrom( + this.httpService.post( + `${this.vaultUrl}/keys/${user}`, + { + exportable: false, + type: 'ecdsa-p256', + }, + this.headers + ) + ); + const jwk = await this.getPublicKeyAsJwk(user); + return { + id: res.data.id, + publicKey: jwk, + }; + } + + /** + * Deletes a key in the vault. + * @param id + * @returns + */ + private deleteKey(id: string) { + // first set the deletion_allowed to true + return firstValueFrom( + this.httpService.post( + `${this.vaultUrl}/keys/${id}/config`, + { + deletion_allowed: true, + }, + this.headers + ) + ).then(() => + firstValueFrom( + this.httpService.delete(`${this.vaultUrl}/keys/${id}`, this.headers) + ) + ); + } + + /** + * Gets the public key and converts it to a KeyLike object. + * @param id + * @returns + */ + private async getPublicKey(id: string): Promise { + return firstValueFrom( + this.httpService.get(`${this.vaultUrl}/keys/${id}`, this.headers) + ).then((res) => importSPKI(res.data.data.keys['1'].public_key, 'ES256')); + } + + getPublicKeyAsJwk(id: string): Promise { + return this.getPublicKey(id) + .then((key) => exportJWK(key)) + .then((jwk) => { + jwk.kid = id; + return jwk; + }); + } + + /** + * Gets the public key of a user or creates a new keypair if it does not exist. + * @param user + * @returns + */ + firstOrCreate(user: string): Promise { + return this.getPublicKeyAsJwk(user).then( + (jwk) => ({ + id: user, + publicKey: jwk, + }), + () => this.create({ type: 'ES256' }, user) + ); + } + + /** + * Signs a value with a key in the vault. + * @param id + * @param user + * @param value + * @returns + */ + sign(id: string, user: string, value: SignRequest): Promise { + return firstValueFrom( + this.httpService.post(`${this.vaultUrl}/sign/${id}`, { + algorithm: value.hashAlgorithm, + input: Buffer.from(value.data).toString('base64'), + }) + ).then((res) => res.data.data.signature.split('vault:v1:')[1]); + } + + /** + * Creates a proof of possession jwt. + * @param user + * @param value + */ + async proof(user: string, value: ProofRequest): Promise { + const keyId = user; + const jwk = await this.getPublicKeyAsJwk(keyId); + + // JWT header + const header = { + //alg has to be changed when we will support other algorithms + alg: 'ES256', + typ: 'openid4vci-proof+jwt', + jwk, + }; + + // JWT payload + const payload = { + ...value.payload, + iat: Math.floor(Date.now() / 1000), // Issued at time + exp: Math.floor(Date.now() / 1000) + 7200, // Expiration time set to 2 hours + }; + + // Convert header and payload to Base64 to prepare for Vault + const encodedHeader = Buffer.from(JSON.stringify(header)).toString( + 'base64url' + ); + const encodedPayload = Buffer.from(JSON.stringify(payload)).toString( + 'base64url' + ); + const signingInput = `${encodedHeader}.${encodedPayload}`; + + // Request to Vault for signing + try { + const response = await firstValueFrom( + this.httpService.post( + `${this.vaultUrl}/sign/${keyId}/sha2-256`, + { + input: Buffer.from(signingInput).toString('base64'), + }, + this.headers + ) + ); + + // Extract the signature from response and construct the full JWT + const signature = this.base64ToBase64Url( + response.data.data.signature.split(':')[2] + ); + const jwt = `${encodedHeader}.${encodedPayload}.${signature}`; + console.log(jwt); + //verigy the jwt before continuing + await jwtVerify(jwt, await importJWK(jwk, 'ES256')); + return `${encodedHeader}.${encodedPayload}.${signature}`; + } catch (error) { + console.error('Error signing JWT with Vault:', error); + throw error; + } + } + + private base64ToBase64Url(str: string) { + return str.replace('+', '-').replace('/', '_').replace(/=+$/, ''); + } + + /** + * Signs a kb jwt. + * @param user + * @param kid + * @param kbJwt + * @param aud + */ + async signkbJwt( + user: string, + kid: string, + kbJwt: SdJwtKbJwtInput, + aud: string + ): Promise { + const keyId = user; + const jwk = await this.getPublicKeyAsJwk(keyId); + // JWT header + const header = { + //alg has to be changed when we will support other algorithms + alg: 'ES256', + typ: kbJwt.header.typ, + // maybe reference the keyid. In case of did:jwk this does not make sense right now. + }; + + // JWT payload + const payload = { + ...kbJwt.payload, + aud, + iat: Math.floor(Date.now() / 1000), // Issued at time + exp: Math.floor(Date.now() / 1000) + 7200, // Expiration time set to 2 hours + }; + + // Convert header and payload to Base64 to prepare for Vault + const encodedHeader = Buffer.from(JSON.stringify(header)).toString( + 'base64url' + ); + const encodedPayload = Buffer.from(JSON.stringify(payload)).toString( + 'base64url' + ); + const signingInput = `${encodedHeader}.${encodedPayload}`; + + // Request to Vault for signing + try { + const response = await firstValueFrom( + this.httpService.post( + `${this.vaultUrl}/sign/${keyId}`, + { + input: Buffer.from(signingInput).toString('base64'), + }, + this.headers + ) + ); + + // Extract the signature from response and construct the full JWT + const signature = this.base64ToBase64Url( + response.data.data.signature.split(':')[2] + ); + console.log(`${encodedHeader}.${encodedPayload}.${signature}`); + return `${encodedHeader}.${encodedPayload}.${signature}`; + } catch (error) { + console.error('Error signing JWT with Vault:', error); + throw error; + } + } +} diff --git a/apps/backend/src/oid4vc/oid4vc.module.ts b/apps/backend/src/oid4vc/oid4vc.module.ts index 0a75d483..efc99130 100644 --- a/apps/backend/src/oid4vc/oid4vc.module.ts +++ b/apps/backend/src/oid4vc/oid4vc.module.ts @@ -1,7 +1,6 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { CredentialsModule } from 'src/credentials/credentials.module'; -import { KeysModule } from 'src/keys/keys.module'; import { Oid4vciController } from './oid4vci/oid4vci.controller'; import { Oid4vciService } from './oid4vci/oid4vci.service'; import { Oid4vpController } from './oid4vp/oid4vp.controller'; @@ -9,7 +8,7 @@ import { Oid4vpService } from './oid4vp/oid4vp.service'; import { HistoryModule } from 'src/history/history.module'; @Module({ - imports: [HttpModule, KeysModule, CredentialsModule, HistoryModule], + imports: [HttpModule, CredentialsModule, HistoryModule], controllers: [Oid4vciController, Oid4vpController], providers: [Oid4vciService, Oid4vpService], }) diff --git a/apps/backend/src/oid4vc/oid4vci/oid4vci.service.ts b/apps/backend/src/oid4vc/oid4vci/oid4vci.service.ts index 93fcad6a..dcad831e 100644 --- a/apps/backend/src/oid4vc/oid4vci/oid4vci.service.ts +++ b/apps/backend/src/oid4vc/oid4vci/oid4vci.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { digest } from '@sd-jwt/crypto-nodejs'; import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc'; import { OpenID4VCIClient } from '@sphereon/oid4vci-client'; @@ -13,12 +13,11 @@ import { } from '@sphereon/oid4vci-common'; import { DIDDocument } from 'did-resolver'; import { CredentialsService } from 'src/credentials/credentials.service'; -import { KeyResponse } from 'src/keys/dto/key-response.dto'; -import { KeysService } from 'src/keys/keys.service'; import { v4 as uuid } from 'uuid'; import { Oid4vciParseRepsonse } from './dto/parse-response.dto'; import { Oid4vciParseRequest } from './dto/parse-request.dto'; import { decodeJwt } from 'jose'; +import { KeysService } from 'src/keys/keys.service'; type Session = { //instead of storing the client, we could also generate it on demand. In this case we need to store the uri @@ -37,7 +36,7 @@ export class Oid4vciService { constructor( private credentialsService: CredentialsService, - private keysService: KeysService + @Inject('KeyService') private keysService: KeysService ) { this.sdjwt = new SDJwtVcInstance({ hasher: digest }); } @@ -86,13 +85,7 @@ export class Oid4vciService { } //use the first key, can be changed to use a specific or unique key - const keys = await this.keysService.findAll(user); - let key: KeyResponse; - if (keys.length === 0) { - key = await this.keysService.create({ type: 'ES256' }, user); - } else { - key = keys[0]; - } + const key = await this.keysService.firstOrCreate(user); const proofCallbacks: ProofOfPossessionCallbacks = { verifyCallback: async (args: { jwt: string; @@ -104,12 +97,10 @@ export class Oid4vciService { jwk: key.publicKey, }), signCallback: async (args: Jwt): Promise => - this.keysService - .proof(user, { - payload: args.payload, - kid: key.id, - }) - .then((response) => response.jwt), + this.keysService.proof(user, { + payload: args.payload, + kid: key.id, + }), }; await data.client.acquireAccessToken(); for (const credential of data.credentials) { diff --git a/apps/backend/src/oid4vc/oid4vp/oid4vp.service.ts b/apps/backend/src/oid4vc/oid4vp/oid4vp.service.ts index 46529cf7..ba1b2b16 100644 --- a/apps/backend/src/oid4vc/oid4vp/oid4vp.service.ts +++ b/apps/backend/src/oid4vc/oid4vp/oid4vp.service.ts @@ -1,4 +1,4 @@ -import { ConflictException, Injectable } from '@nestjs/common'; +import { ConflictException, Inject, Injectable } from '@nestjs/common'; import { digest } from '@sd-jwt/crypto-nodejs'; import { SDJwtVcInstance } from '@sd-jwt/sd-jwt-vc'; import { @@ -15,7 +15,6 @@ import { } from '@sphereon/did-auth-siop'; import { SdJwtDecodedVerifiableCredentialWithKbJwtInput } from '@sphereon/pex'; import { CredentialsService } from 'src/credentials/credentials.service'; -import { KeysService } from 'src/keys/keys.service'; import { v4 as uuid } from 'uuid'; import { Oid4vpParseRepsonse } from './dto/parse-response.dto'; import { SubmissionRequest } from './dto/submission-request.dto'; @@ -23,6 +22,7 @@ import { HistoryService } from 'src/history/history.service'; import { Oid4vpParseRequest } from './dto/parse-request.dto'; import { Session } from './session'; import { CompactSdJwtVc } from '@sphereon/ssi-types'; +import { KeysService } from 'src/keys/keys.service'; @Injectable() export class Oid4vpService { @@ -30,8 +30,8 @@ export class Oid4vpService { sdjwt: SDJwtVcInstance; constructor( + @Inject('KeyService') private keysService: KeysService, private credentialsService: CredentialsService, - private keysService: KeysService, private historyService: HistoryService ) { this.sdjwt = new SDJwtVcInstance({ hasher: digest }); diff --git a/docker-compose.yml b/docker-compose.yml index 198da0bc..987c7da4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.7' - services: postgres-keycloak: restart: unless-stopped @@ -61,6 +59,22 @@ services: retries: 3 start_period: 2m + vault: + image: 'hashicorp/vault:1.16' + restart: unless-stopped + healthcheck: + test: ['CMD', 'vault', 'status'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 2m + volumes: + - vault:/data + environment: + VAULT_DEV_ROOT_TOKEN_ID: $VAULT_DEV_ROOT_TOKEN_ID + ports: + - '8200:8200' + db: image: 'postgres:14.4' restart: unless-stopped @@ -164,3 +178,4 @@ volumes: nestjs-db: issuer_tmp: verifier_tmp: + vault: From 143c5b827e4d1c1a0962e436dd03a12c064fda49 Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Wed, 1 May 2024 12:25:33 +0200 Subject: [PATCH 2/6] add some useful debug information Signed-off-by: Mirko Mollik --- apps/backend/src/keys/vault-keys.service.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/keys/vault-keys.service.ts b/apps/backend/src/keys/vault-keys.service.ts index 7f11c36f..fd29e0e9 100644 --- a/apps/backend/src/keys/vault-keys.service.ts +++ b/apps/backend/src/keys/vault-keys.service.ts @@ -178,17 +178,30 @@ export class VaultKeysService extends KeysService { `${this.vaultUrl}/sign/${keyId}/sha2-256`, { input: Buffer.from(signingInput).toString('base64'), + key_version: 1, }, this.headers ) ); + const verify = await firstValueFrom( + this.httpService.post( + `${this.vaultUrl}/verify/${keyId}/sha2-256`, + { + input: Buffer.from(signingInput).toString('base64'), + signature: response.data.data.signature, + }, + this.headers + ) + ); + //beeing true means the signature is valid by hashicorp vault. So it seems it's an encoding issue + console.log(verify.data.data.valid); + // Extract the signature from response and construct the full JWT const signature = this.base64ToBase64Url( response.data.data.signature.split(':')[2] ); const jwt = `${encodedHeader}.${encodedPayload}.${signature}`; - console.log(jwt); //verigy the jwt before continuing await jwtVerify(jwt, await importJWK(jwk, 'ES256')); return `${encodedHeader}.${encodedPayload}.${signature}`; From 2284a52821f61591528d11ea0ffa1fea25b32cd3 Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Thu, 2 May 2024 15:59:58 +0200 Subject: [PATCH 3/6] Add generic key storage Fixes #12 Signed-off-by: Mirko Mollik --- apps/backend/src/keys/vault-keys.service.ts | 119 ++++++++++++++++---- docker-compose.yml | 3 - 2 files changed, 96 insertions(+), 26 deletions(-) diff --git a/apps/backend/src/keys/vault-keys.service.ts b/apps/backend/src/keys/vault-keys.service.ts index fd29e0e9..79481686 100644 --- a/apps/backend/src/keys/vault-keys.service.ts +++ b/apps/backend/src/keys/vault-keys.service.ts @@ -16,6 +16,11 @@ import { JWK, } from 'jose'; import { ConfigService } from '@nestjs/config'; +import { + createSign, + KeyLike as CryptoKeyLike, + generateKeyPairSync, +} from 'node:crypto'; @Injectable() export class VaultKeysService extends KeysService { @@ -130,12 +135,19 @@ export class VaultKeysService extends KeysService { * @returns */ sign(id: string, user: string, value: SignRequest): Promise { + const keyId = user; return firstValueFrom( - this.httpService.post(`${this.vaultUrl}/sign/${id}`, { - algorithm: value.hashAlgorithm, - input: Buffer.from(value.data).toString('base64'), - }) - ).then((res) => res.data.data.signature.split('vault:v1:')[1]); + this.httpService.post( + `${this.vaultUrl}/sign/${keyId}`, + { + algorithm: value.hashAlgorithm, + input: Buffer.from(value.data).toString('base64'), + }, + this.headers + ) + ).then((res) => + this.derToJwtSignature(res.data.data.signature.split(':')[2]) + ); } /** @@ -178,27 +190,12 @@ export class VaultKeysService extends KeysService { `${this.vaultUrl}/sign/${keyId}/sha2-256`, { input: Buffer.from(signingInput).toString('base64'), - key_version: 1, - }, - this.headers - ) - ); - - const verify = await firstValueFrom( - this.httpService.post( - `${this.vaultUrl}/verify/${keyId}/sha2-256`, - { - input: Buffer.from(signingInput).toString('base64'), - signature: response.data.data.signature, }, this.headers ) ); - //beeing true means the signature is valid by hashicorp vault. So it seems it's an encoding issue - console.log(verify.data.data.valid); - // Extract the signature from response and construct the full JWT - const signature = this.base64ToBase64Url( + const signature = this.derToJwtSignature( response.data.data.signature.split(':')[2] ); const jwt = `${encodedHeader}.${encodedPayload}.${signature}`; @@ -259,7 +256,7 @@ export class VaultKeysService extends KeysService { try { const response = await firstValueFrom( this.httpService.post( - `${this.vaultUrl}/sign/${keyId}`, + `${this.vaultUrl}/sign/${keyId}/sha2-256`, { input: Buffer.from(signingInput).toString('base64'), }, @@ -271,11 +268,87 @@ export class VaultKeysService extends KeysService { const signature = this.base64ToBase64Url( response.data.data.signature.split(':')[2] ); - console.log(`${encodedHeader}.${encodedPayload}.${signature}`); return `${encodedHeader}.${encodedPayload}.${signature}`; } catch (error) { console.error('Error signing JWT with Vault:', error); throw error; } } + + async test(type: 'RS256' | 'ES256') { + let privateKey: CryptoKeyLike; + let publicKey: CryptoKeyLike; + + if (type === 'RS256') { + const keys = generateKeyPairSync('rsa', { + modulusLength: 2048, + }); + privateKey = keys.privateKey; + publicKey = keys.publicKey; + } else { + const keys = generateKeyPairSync('ec', { + namedCurve: 'prime256v1', + }); + privateKey = keys.privateKey; + publicKey = keys.publicKey; + } + + const payload = { iss: 'foo' }; + const header = { alg: type }; + const h = Buffer.from(JSON.stringify(header)).toString('base64url'); + const p = Buffer.from(JSON.stringify(payload)).toString('base64url'); + const sign = createSign('SHA256'); + sign.update(`${h}.${p}`); + sign.end(); + const finalSign = sign.sign(privateKey, 'base64'); + const jwt1 = `${h}.${p}.${type === 'RS256' ? this.base64ToBase64Url(finalSign) : this.derToJwtSignature(finalSign)}`; + await jwtVerify(jwt1, publicKey); + } + + /** + * Converts a DER signature to a JWT signature. + * @param derSignature + * @returns + */ + derToJwtSignature(derSignature: string) { + // Step 1: Extract r and s from DER signature + const der = Buffer.from(derSignature, 'base64'); + const sequence = der.slice(2); // Skip the sequence tag and length + const rLength = sequence[1]; + const r = sequence.slice(2, 2 + rLength); + const s = sequence.slice(2 + rLength + 2); // Skip r, its tag and length byte, and s's tag and length byte + + // Step 2: Ensure r and s are 32 bytes each (pad with zeros if necessary) + // Ensure r and s are 32 bytes each + let rPadded: Buffer; + let sPadded: Buffer; + if (r.length > 32) { + if (r.length === 33 && r[0] === 0x00) { + rPadded = r.slice(1); + } else { + throw new Error('Invalid r length in DER signature'); + } + } else { + rPadded = Buffer.concat([Buffer.alloc(32 - r.length), r]); + } + if (s.length > 32) { + if (s.length === 33 && s[0] === 0x00) { + sPadded = s.slice(1); + } else { + throw new Error('Invalid s length in DER signature'); + } + } else { + sPadded = Buffer.concat([Buffer.alloc(32 - s.length), s]); + } + + // Step 3: Concatenate r and s to form the raw signature + const rawSignature = Buffer.concat([rPadded, sPadded]); + + // Step 4: Base64url encode the raw signature + return rawSignature + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + } } diff --git a/docker-compose.yml b/docker-compose.yml index 987c7da4..88bb146a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -68,8 +68,6 @@ services: timeout: 10s retries: 3 start_period: 2m - volumes: - - vault:/data environment: VAULT_DEV_ROOT_TOKEN_ID: $VAULT_DEV_ROOT_TOKEN_ID ports: @@ -178,4 +176,3 @@ volumes: nestjs-db: issuer_tmp: verifier_tmp: - vault: From 0bb7baaf01d8f2f1dd16f1d39ab5ff44f8a387a8 Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Mon, 6 May 2024 22:28:18 +0200 Subject: [PATCH 4/6] Add generic key storage Fixes #12 Signed-off-by: Mirko Mollik --- config/vault/config.hcl | 12 +++++++++++ docs/running-docker.md | 48 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 config/vault/config.hcl diff --git a/config/vault/config.hcl b/config/vault/config.hcl new file mode 100644 index 00000000..61d57adb --- /dev/null +++ b/config/vault/config.hcl @@ -0,0 +1,12 @@ +storage "file" { + path = "./vault/data" +} + +listener "tcp" { + address = "0.0.0.0:8200" + tls_disable = "true" +} + +api_addr = "http://127.0.0.1:8200" +cluster_addr = "https://127.0.0.1:8201" +ui = true diff --git a/docs/running-docker.md b/docs/running-docker.md index ff002269..3287822f 100644 --- a/docs/running-docker.md +++ b/docs/running-docker.md @@ -9,3 +9,51 @@ The configuration of the pwa client is mounted from the `config/holder/config.js ## Known limitations right now running it locally via docker can cause some problems since `localhost` is used to interact with some services. + +## Vault +To secure your keys, you are able to use [vault by hashicorp](https://developer.hashicorp.com/vault), otherwise the keys are either stored in the filesystem for the issuer and verifier or in the unencrypted database for the wallet. + +You are able to run vault via docker with the following command: +```bash +docker compose up -d vault +``` +This will spin up a vault instance in dev mode and will not persist the keys after a restart. In the `.env` in the root folder, you can set a token you need for authentication. + +### Using in the cloud wallet + +Configure the environment variables in the `.env` to tell the service to use vault: +```env +KM_TYPE=vault +VAULT_URL=http://localhost:8200/v1/transit +VAULT_TOKEN=root +``` +The server does not support multiple key management systems in parallel and also no import or export feature. So decide at the beginning which type of key management you want to use. + +TODO: we also need key management for the accounts to support multiple keys, because right now we use the user-id for the key reference, so each user is only able to store one key. We need a mapping table for the keys and the user-id. + +### Using in the issuer and verifier + +TODO: not implemented yet. + +### Production use +Update the docker container like this: +```yaml + vault: + image: 'hashicorp/vault:1.16' + restart: unless-stopped + healthcheck: + test: ['CMD', 'vault', 'status'] + interval: 30s + timeout: 10s + retries: 3 + start_period: 2m + volumes: + - vault-storage:/vault/file:rw + - ./config/vault:/vault/config:rw + ports: + - '8200:8200' + environment: + VAULT_ADDR: http://127.0.0.1:8200 + entrypoint: vault server -config=/vault/config/config.hcl +``` +Get familiar with the [vault deployment guide](https://developer.hashicorp.com/vault/tutorials/getting-started/getting-started-deploy). This current documentation is not fully covered to run vault in production! \ No newline at end of file From a9c79522f0b0302520048f4a9d8fbc3e3f72a593 Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Mon, 6 May 2024 23:06:14 +0200 Subject: [PATCH 5/6] add vault to issuer backend (but not completed!) Signed-off-by: Mirko Mollik --- apps/backend/src/keys/vault-keys.service.ts | 38 ---- apps/issuer-backend/package.json | 1 + apps/issuer-backend/src/app.module.ts | 4 +- .../src/issuer/issuer.service.ts | 60 +++--- .../src/key/filesystem-key.service.ts | 100 ++++++++++ apps/issuer-backend/src/key/key.module.ts | 55 +++++- apps/issuer-backend/src/key/key.service.ts | 87 ++------- .../src/key/vault-key.service.ts | 183 ++++++++++++++++++ pnpm-lock.yaml | 3 + 9 files changed, 389 insertions(+), 142 deletions(-) create mode 100644 apps/issuer-backend/src/key/filesystem-key.service.ts create mode 100644 apps/issuer-backend/src/key/vault-key.service.ts diff --git a/apps/backend/src/keys/vault-keys.service.ts b/apps/backend/src/keys/vault-keys.service.ts index 79481686..72760f00 100644 --- a/apps/backend/src/keys/vault-keys.service.ts +++ b/apps/backend/src/keys/vault-keys.service.ts @@ -16,11 +16,6 @@ import { JWK, } from 'jose'; import { ConfigService } from '@nestjs/config'; -import { - createSign, - KeyLike as CryptoKeyLike, - generateKeyPairSync, -} from 'node:crypto'; @Injectable() export class VaultKeysService extends KeysService { @@ -198,9 +193,6 @@ export class VaultKeysService extends KeysService { const signature = this.derToJwtSignature( response.data.data.signature.split(':')[2] ); - const jwt = `${encodedHeader}.${encodedPayload}.${signature}`; - //verigy the jwt before continuing - await jwtVerify(jwt, await importJWK(jwk, 'ES256')); return `${encodedHeader}.${encodedPayload}.${signature}`; } catch (error) { console.error('Error signing JWT with Vault:', error); @@ -275,36 +267,6 @@ export class VaultKeysService extends KeysService { } } - async test(type: 'RS256' | 'ES256') { - let privateKey: CryptoKeyLike; - let publicKey: CryptoKeyLike; - - if (type === 'RS256') { - const keys = generateKeyPairSync('rsa', { - modulusLength: 2048, - }); - privateKey = keys.privateKey; - publicKey = keys.publicKey; - } else { - const keys = generateKeyPairSync('ec', { - namedCurve: 'prime256v1', - }); - privateKey = keys.privateKey; - publicKey = keys.publicKey; - } - - const payload = { iss: 'foo' }; - const header = { alg: type }; - const h = Buffer.from(JSON.stringify(header)).toString('base64url'); - const p = Buffer.from(JSON.stringify(payload)).toString('base64url'); - const sign = createSign('SHA256'); - sign.update(`${h}.${p}`); - sign.end(); - const finalSign = sign.sign(privateKey, 'base64'); - const jwt1 = `${h}.${p}.${type === 'RS256' ? this.base64ToBase64Url(finalSign) : this.derToJwtSignature(finalSign)}`; - await jwtVerify(jwt1, publicKey); - } - /** * Converts a DER signature to a JWT signature. * @param derSignature diff --git a/apps/issuer-backend/package.json b/apps/issuer-backend/package.json index 6f98017d..b9c1234e 100644 --- a/apps/issuer-backend/package.json +++ b/apps/issuer-backend/package.json @@ -23,6 +23,7 @@ "publish": "pnpm run build && cd ../../ && docker compose build issuer-backend && docker compose push issuer-backend" }, "dependencies": { + "@nestjs/axios": "^3.0.2", "@nestjs/common": "^10.0.0", "@nestjs/config": "^3.2.2", "@nestjs/core": "^10.0.0", diff --git a/apps/issuer-backend/src/app.module.ts b/apps/issuer-backend/src/app.module.ts index 8fc3ab22..b5e980c0 100644 --- a/apps/issuer-backend/src/app.module.ts +++ b/apps/issuer-backend/src/app.module.ts @@ -22,12 +22,12 @@ import { CredentialsModule } from './credentials/credentials.module'; ...DB_VALIDATION_SCHEMA, }), }), - KeyModule, + KeyModule.forRootSync(), IssuerModule, AuthModule, DbModule, CredentialsModule, ], - controllers: [AppController], + controllers: [AppController], }) export class AppModule {} diff --git a/apps/issuer-backend/src/issuer/issuer.service.ts b/apps/issuer-backend/src/issuer/issuer.service.ts index ef3e9533..2c520cee 100644 --- a/apps/issuer-backend/src/issuer/issuer.service.ts +++ b/apps/issuer-backend/src/issuer/issuer.service.ts @@ -1,4 +1,9 @@ -import { ConflictException, Injectable, OnModuleInit } from '@nestjs/common'; +import { + ConflictException, + Inject, + Injectable, + OnModuleInit, +} from '@nestjs/common'; import { HttpAdapterHost } from '@nestjs/core'; import { ExpressAdapter } from '@nestjs/platform-express'; import { ES256, digest, generateSalt } from '@sd-jwt/crypto-nodejs'; @@ -35,10 +40,10 @@ import { ExpressCorsConfigurer, ExpressSupport, } from '@sphereon/ssi-express-support'; -import { KeyService } from 'src/key/key.service'; import { IssuerDataService } from './issuer-data.service'; import { SessionRequestDto } from './dto/session-request.dto'; import { CredentialsService } from 'src/credentials/credentials.service'; +import { KeyService } from 'src/key/key.service'; @Injectable() export class IssuerService implements OnModuleInit { @@ -46,11 +51,11 @@ export class IssuerService implements OnModuleInit { vcIssuer: VcIssuer; constructor( private adapterHost: HttpAdapterHost, - private keyService: KeyService, + @Inject('KeyService') private keyService: KeyService, private issuerDataService: IssuerDataService, - private credentialsService: CredentialsService, + private credentialsService: CredentialsService ) { - this.express = this.getExpressInstance(); + this.express = this.getExpressInstance(); } async onModuleInit() { await this.init(); @@ -60,11 +65,11 @@ export class IssuerService implements OnModuleInit { * Returns the issuer metadata. * @returns */ - getIssuerMetadata(): IssuerMetadata { + async getIssuerMetadata(): Promise { return { issuer: process.env.ISSUER_BASE_URL as string, jwks: { - keys: [this.keyService.getPublicKey() as JWK], + keys: [await this.keyService.getPublicKey()], }, }; } @@ -113,15 +118,13 @@ export class IssuerService implements OnModuleInit { async init() { // import the private key. - const privateKeyLike = await importJWK(this.keyService.getPrivateKey()); - // get the signer and verifier. Only ES256 is supported for now. - const signer = await ES256.getSigner(this.keyService.getPrivateKey()); + // get verifier. Only ES256 is supported for now. const verifier = await ES256.getVerifier(this.keyService.getPublicKey()); // crearre the sd-jwt instance with the required parameters. const sdjwt = new SDJwtVcInstance({ - signer, + signer: this.keyService.sign, verifier, signAlg: 'ES256', hasher: digest, @@ -134,7 +137,7 @@ export class IssuerService implements OnModuleInit { * @param args * @returns */ - const credentialDataSupplier: CredentialDataSupplier = async (args) => { + const credentialDataSupplier: CredentialDataSupplier = async (args) => { const credential: SdJwtDecodedVerifiableCredentialPayload = { iat: new Date().getTime(), iss: args.credentialOffer.credential_offer.credential_issuer, @@ -155,14 +158,11 @@ export class IssuerService implements OnModuleInit { * @returns signed jwt */ const signerCallback = async (jwt: Jwt, kid?: string): Promise => { - //TODO: use the kid to select the correct key - return new SignJWT({ ...jwt.payload }) - .setProtectedHeader({ - ...jwt.header, - alg: Alg.ES256, - kid: this.keyService.getKid(), - }) - .sign(privateKeyLike); + return this.keyService.signJWT(jwt.payload, { + ...jwt.header, + alg: Alg.ES256, + kid: await this.keyService.getKid(), + }); }; /** @@ -196,16 +196,20 @@ export class IssuerService implements OnModuleInit { */ const credentialSignerCallback: CredentialSignerCallback< DIDDocument - > = async (args) => { + > = async (args) => { const jwt = await sdjwt.issue<{ iss: string; vct: string }>( args.credential as SdJwtDecodedVerifiableCredentialPayload, this.issuerDataService.getDisclosureFrame( - args.credential.vct as string, + args.credential.vct as string ), - { header: { kid: this.keyService.getKid() } }); - await this.credentialsService.create({value: jwt, id: args.credential.jti as string}) - return jwt; - } + { header: { kid: this.keyService.getKid() } } + ); + await this.credentialsService.create({ + value: jwt, + id: args.credential.jti as string, + }); + return jwt; + }; //create the issuer instance this.vcIssuer = new VcIssuer( @@ -218,8 +222,8 @@ export class IssuerService implements OnModuleInit { uris: new MemoryStates(), jwtVerifyCallback, credentialDataSupplier, - credentialSignerCallback, - }, + credentialSignerCallback, + } ); /** diff --git a/apps/issuer-backend/src/key/filesystem-key.service.ts b/apps/issuer-backend/src/key/filesystem-key.service.ts new file mode 100644 index 00000000..9245de55 --- /dev/null +++ b/apps/issuer-backend/src/key/filesystem-key.service.ts @@ -0,0 +1,100 @@ +import { ES256 } from '@sd-jwt/crypto-nodejs'; +import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'; +import { + JWK, + JWTHeaderParameters, + JWTPayload, + KeyLike, + SignJWT, + importJWK, +} from 'jose'; +import { v4 } from 'uuid'; +import { KeyService } from './key.service'; +import { Injectable } from '@nestjs/common'; + +//TODO: implement a vault integration like in the backend +/** + * The key service is responsible for managing the keys of the issuer. + */ +@Injectable() +export class FileSystemKeyService implements KeyService { + private privateKey: JWK; + private publicKey: JWK; + private privateKeyInstance: KeyLike; + signer: (data: string) => Promise; + + async onModuleInit(): Promise { + await this.init(); + this.signer = await ES256.getSigner(this.privateKey); + this.privateKeyInstance = (await importJWK(this.privateKey)) as KeyLike; + } + async init() { + // get the keys + const { privateKey, publicKey } = await this.getKeys(); + this.privateKey = privateKey; + this.publicKey = publicKey; + } + + /** + * Get the keys from the file system or generate them if they do not exist + * @returns + */ + private async getKeys() { + let privateKey: JWK; + let publicKey: JWK; + const folder = './tmp'; + if (!existsSync(folder)) { + mkdirSync(folder); + } + if ( + !existsSync(`${folder}/private.json`) && + !existsSync(`${folder}/public.json`) + ) { + const keys = await ES256.generateKeyPair(); + privateKey = keys.privateKey as JWK; + publicKey = keys.publicKey as JWK; + //add a random key id for reference + publicKey.kid = v4(); + privateKey.kid = publicKey.kid; + writeFileSync(`${folder}/private.json`, JSON.stringify(privateKey)); + writeFileSync(`${folder}/public.json`, JSON.stringify(publicKey)); + } else { + privateKey = JSON.parse(readFileSync(`${folder}/private.json`, 'utf-8')); + publicKey = JSON.parse(readFileSync(`${folder}/public.json`, 'utf-8')); + } + return { privateKey, publicKey }; + } + + /** + * Get the key id + * @returns + */ + getKid() { + return Promise.resolve(this.publicKey.kid); + } + + /** + * Get the public key + * @returns + */ + getPublicKey() { + const copy = { ...this.publicKey }; + copy.key_ops = undefined; + copy.ext = undefined; + return Promise.resolve(copy); + } + + /** + * Returns the signature of the given value + * @param value + */ + sign(value: string) { + return this.signer(value); + } + + async signJWT(payload: JWTPayload, header: JWTHeaderParameters) { + return new SignJWT(payload) + .setProtectedHeader(header) + .sign(this.privateKeyInstance); + } +} diff --git a/apps/issuer-backend/src/key/key.module.ts b/apps/issuer-backend/src/key/key.module.ts index ea7bf23c..4f6cc419 100644 --- a/apps/issuer-backend/src/key/key.module.ts +++ b/apps/issuer-backend/src/key/key.module.ts @@ -1,8 +1,49 @@ -import { Module } from '@nestjs/common'; -import { KeyService } from './key.service'; +import { DynamicModule, Global, Module } from '@nestjs/common'; +import { FileSystemKeyService } from './filesystem-key.service'; +import Joi from 'joi'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { HttpModule, HttpService } from '@nestjs/axios'; +import { VaultKeyService } from './vault-key.service'; -@Module({ - providers: [KeyService], - exports: [KeyService], -}) -export class KeyModule {} +export const KEY_VALIDATION_SCHEMA = { + KM_TYPE: Joi.string().valid('db', 'vault').default('db'), + VAULT_URL: Joi.string().when('KM_TYPE', { + is: 'vault', + // biome-ignore lint/suspicious/noThenProperty: + then: Joi.required(), + otherwise: Joi.optional(), + }), + VAULT_TOKEN: Joi.string().when('KM_TYPE', { + is: 'vault', + // biome-ignore lint/suspicious/noThenProperty: + then: Joi.required(), + otherwise: Joi.optional(), + }), +}; +@Global() +@Module({}) +// biome-ignore lint/complexity/noStaticOnlyClass: +export class KeyModule { + static forRootSync(): DynamicModule { + return { + module: KeyModule, + imports: [HttpModule, ConfigModule], + providers: [ + { + provide: 'KeyService', + useFactory: ( + configService: ConfigService, + httpService: HttpService + ) => { + const kmType = configService.get('KM_TYPE'); + return kmType === 'vault' + ? new VaultKeyService(httpService, configService) + : new FileSystemKeyService(); + }, + inject: [ConfigService, HttpService], + }, + ], + exports: ['KeyService'], + }; + } +} diff --git a/apps/issuer-backend/src/key/key.service.ts b/apps/issuer-backend/src/key/key.service.ts index a34ba4b2..eef1fdb7 100644 --- a/apps/issuer-backend/src/key/key.service.ts +++ b/apps/issuer-backend/src/key/key.service.ts @@ -1,86 +1,39 @@ -import { Injectable, OnModuleInit } from '@nestjs/common'; -import { ES256 } from '@sd-jwt/crypto-nodejs'; -import { existsSync, mkdirSync, writeFileSync, readFileSync } from 'node:fs'; -import { JWK } from 'jose'; -import { v4 } from 'uuid'; +import { OnModuleInit } from '@nestjs/common'; +import { JWK, JWTHeaderParameters, JWTPayload } from 'jose'; -//TODO: implement a vault integration like in the backend /** - * The key service is responsible for managing the keys of the issuer. + * Generic interface for a key service */ -@Injectable() -export class KeyService implements OnModuleInit { - private privateKey: JWK; - private publicKey: JWK; - constructor() { - this.init(); - } - - async onModuleInit() { - await this.init(); - } - - async init() { - // get the keys - const { privateKey, publicKey } = await this.getKeys(); - this.privateKey = privateKey; - this.publicKey = publicKey; +export abstract class KeyService implements OnModuleInit { + async onModuleInit() { + await this.init(); } /** - * Get the keys from the file system or generate them if they do not exist - * @returns + * Initialize the key service */ - private async getKeys() { - let privateKey: JWK; - let publicKey: JWK; - const folder = './tmp'; - if (!existsSync(folder)) { - mkdirSync(folder); - } - if ( - !existsSync(`${folder}/private.json`) && - !existsSync(`${folder}/public.json`) - ) { - const keys = await ES256.generateKeyPair(); - privateKey = keys.privateKey as JWK; - publicKey = keys.publicKey as JWK; - //add a random key id for reference - publicKey.kid = v4(); - privateKey.kid = publicKey.kid; - writeFileSync(`${folder}/private.json`, JSON.stringify(privateKey)); - writeFileSync(`${folder}/public.json`, JSON.stringify(publicKey)); - } else { - privateKey = JSON.parse(readFileSync(`${folder}/private.json`, 'utf-8')); - publicKey = JSON.parse(readFileSync(`${folder}/public.json`, 'utf-8')); - } - return { privateKey, publicKey }; - } + abstract init(): Promise; /** * Get the key id - * @returns + * @returns */ - getKid(): string { - return this.publicKey.kid; - } + abstract getKid(): Promise; /** * Get the public key - * @returns + * @returns */ - getPublicKey(): JWK { - const copy = { ...this.publicKey }; - copy.key_ops = undefined; - copy.ext = undefined; - return copy; - } + abstract getPublicKey(): Promise; /** - * Get the private key - * @returns + * Returns the signature of the given value + * @param value */ - getPrivateKey(): JWK { - return this.privateKey; - } + abstract sign(value: string): Promise; + + abstract signJWT( + payload: JWTPayload, + header: JWTHeaderParameters + ): Promise; } diff --git a/apps/issuer-backend/src/key/vault-key.service.ts b/apps/issuer-backend/src/key/vault-key.service.ts new file mode 100644 index 00000000..2010e9be --- /dev/null +++ b/apps/issuer-backend/src/key/vault-key.service.ts @@ -0,0 +1,183 @@ +import { Injectable } from '@nestjs/common'; +import { KeyService } from './key.service'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; +import { importSPKI, exportJWK, JWTHeaderParameters } from 'jose'; +import { ConfigService } from '@nestjs/config'; +import { v4 } from 'uuid'; +import { JwtPayload } from '@sd-jwt/types'; + +@Injectable() +export class VaultKeyService extends KeyService { + private keyId = 'keyID'; + + // url to the vault instance + private vaultUrl: string; + // headers for the vault api + private headers: { headers: { 'X-Vault-Token': string } }; + + constructor( + private httpService: HttpService, + private configService: ConfigService + ) { + super(); + this.vaultUrl = this.configService.get('VAULT_URL'); + this.headers = { + headers: { + 'X-Vault-Token': this.configService.get('VAULT_TOKEN'), + }, + }; + } + + /** + * Check if the vault has a key with the given id + */ + async init() { + await this.getPublicKey().catch(() => { + this.keyId = v4(); + //TODO: we need to persist this key id + this.create(); + }); + } + + /** + * Creates a new keypair in the vault. + * @param createKeyDto + * @param user + * @returns + */ + async create() { + const res = await firstValueFrom( + this.httpService.post( + `${this.vaultUrl}/keys/${this.keyId}`, + { + exportable: false, + type: 'ecdsa-p256', + }, + this.headers + ) + ); + const jwk = await this.getPublicKey(); + return { + id: res.data.id, + publicKey: jwk, + }; + } + + getKid(): Promise { + return Promise.resolve(this.keyId); + } + + /** + * Gets the public key and converts it to a KeyLike object. + * @param id + * @returns + */ + async getPublicKey() { + return firstValueFrom( + this.httpService.get(`${this.vaultUrl}/keys/${this.keyId}`, this.headers) + ) + .then((res) => importSPKI(res.data.data.keys['1'].public_key, 'ES256')) + .then((key) => exportJWK(key)) + .then((jwk) => { + jwk.kid = this.keyId; + return jwk; + }); + } + + /** + * Signs a value with a key in the vault. + * @param id + * @param user + * @param value + * @returns + */ + sign(value: string): Promise { + return firstValueFrom( + this.httpService.post( + `${this.vaultUrl}/sign/${this.keyId}`, + { + input: Buffer.from(value).toString('base64'), + }, + this.headers + ) + ).then((res) => + this.derToJwtSignature(res.data.data.signature.split(':')[2]) + ); + } + + /** + * Creates a proof of possession jwt. + * @param user + * @param value + */ + async signJWT( + payload: JwtPayload, + header: JWTHeaderParameters + ): Promise { + // Convert header and payload to Base64 to prepare for Vault + const encodedHeader = Buffer.from(JSON.stringify(header)).toString( + 'base64url' + ); + const encodedPayload = Buffer.from(JSON.stringify(payload)).toString( + 'base64url' + ); + const signingInput = `${encodedHeader}.${encodedPayload}`; + + // Request to Vault for signing + try { + const signature = this.sign(signingInput); + return `${encodedHeader}.${encodedPayload}.${signature}`; + } catch (error) { + console.error('Error signing JWT with Vault:', error); + throw error; + } + } + + /** + * Converts a DER signature to a JWT signature. + * @param derSignature + * @returns + */ + derToJwtSignature(derSignature: string) { + // Step 1: Extract r and s from DER signature + const der = Buffer.from(derSignature, 'base64'); + const sequence = der.slice(2); // Skip the sequence tag and length + const rLength = sequence[1]; + const r = sequence.slice(2, 2 + rLength); + const s = sequence.slice(2 + rLength + 2); // Skip r, its tag and length byte, and s's tag and length byte + + // Step 2: Ensure r and s are 32 bytes each (pad with zeros if necessary) + // Ensure r and s are 32 bytes each + let rPadded: Buffer; + let sPadded: Buffer; + if (r.length > 32) { + if (r.length === 33 && r[0] === 0x00) { + rPadded = r.slice(1); + } else { + throw new Error('Invalid r length in DER signature'); + } + } else { + rPadded = Buffer.concat([Buffer.alloc(32 - r.length), r]); + } + if (s.length > 32) { + if (s.length === 33 && s[0] === 0x00) { + sPadded = s.slice(1); + } else { + throw new Error('Invalid s length in DER signature'); + } + } else { + sPadded = Buffer.concat([Buffer.alloc(32 - s.length), s]); + } + + // Step 3: Concatenate r and s to form the raw signature + const rawSignature = Buffer.concat([rPadded, sPadded]); + + // Step 4: Base64url encode the raw signature + return rawSignature + .toString('base64') + .replace(/\+/g, '-') + .replace(/\//g, '_') + .replace(/=/g, ''); + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 029cb74a..8754fd41 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -315,6 +315,9 @@ importers: apps/issuer-backend: dependencies: + '@nestjs/axios': + specifier: ^3.0.2 + version: 3.0.2(@nestjs/common@10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1))(axios@1.6.8)(rxjs@7.8.1) '@nestjs/common': specifier: ^10.0.0 version: 10.3.8(class-transformer@0.5.1)(class-validator@0.14.1)(reflect-metadata@0.2.2)(rxjs@7.8.1) From 3110ca384c1f0117ba4443e6f3f2afd1db8edec2 Mon Sep 17 00:00:00 2001 From: Mirko Mollik Date: Tue, 7 May 2024 22:31:03 +0200 Subject: [PATCH 6/6] Add key storage to issuer backend Fixes #12 Signed-off-by: Mirko Mollik --- apps/backend/src/keys/vault-keys.service.ts | 17 +++++++---------- apps/issuer-backend/src/issuer/issuer.module.ts | 3 +-- .../issuer-backend/src/issuer/issuer.service.ts | 16 +++++----------- .../src/key/filesystem-key.service.ts | 11 ++--------- apps/issuer-backend/src/key/key.module.ts | 8 +++++++- apps/issuer-backend/src/key/key.service.ts | 5 ++++- .../issuer-backend/src/key/vault-key.service.ts | 9 +++++---- 7 files changed, 31 insertions(+), 38 deletions(-) diff --git a/apps/backend/src/keys/vault-keys.service.ts b/apps/backend/src/keys/vault-keys.service.ts index 72760f00..aebd2f0d 100644 --- a/apps/backend/src/keys/vault-keys.service.ts +++ b/apps/backend/src/keys/vault-keys.service.ts @@ -7,14 +7,7 @@ import { ProofRequest } from './dto/proof-request.dto'; import { SignRequest } from './dto/sign-request.dto'; import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; -import { - importSPKI, - KeyLike, - exportJWK, - jwtVerify, - importJWK, - JWK, -} from 'jose'; +import { importSPKI, KeyLike, exportJWK, JWK } from 'jose'; import { ConfigService } from '@nestjs/config'; @Injectable() @@ -57,7 +50,10 @@ export class VaultKeysService extends KeysService { }, this.headers ) - ); + ).catch((error) => { + console.error(error); + throw error; + }); const jwk = await this.getPublicKeyAsJwk(user); return { id: res.data.id, @@ -193,7 +189,8 @@ export class VaultKeysService extends KeysService { const signature = this.derToJwtSignature( response.data.data.signature.split(':')[2] ); - return `${encodedHeader}.${encodedPayload}.${signature}`; + const jwt = `${encodedHeader}.${encodedPayload}.${signature}`; + return jwt; } catch (error) { console.error('Error signing JWT with Vault:', error); throw error; diff --git a/apps/issuer-backend/src/issuer/issuer.module.ts b/apps/issuer-backend/src/issuer/issuer.module.ts index 4f4e017b..b07bfd67 100644 --- a/apps/issuer-backend/src/issuer/issuer.module.ts +++ b/apps/issuer-backend/src/issuer/issuer.module.ts @@ -2,12 +2,11 @@ import { Module } from '@nestjs/common'; import { IssuerDataService } from './issuer-data.service'; import { IssuerService } from './issuer.service'; import { IssuerController } from './issuer.controller'; -import { KeyModule } from 'src/key/key.module'; import { CredentialsModule } from 'src/credentials/credentials.module'; import { WellKnownController } from './well-known/well-known.controller'; @Module({ - imports: [KeyModule, CredentialsModule], + imports: [CredentialsModule], controllers: [IssuerController, WellKnownController], providers: [IssuerService, IssuerDataService], }) diff --git a/apps/issuer-backend/src/issuer/issuer.service.ts b/apps/issuer-backend/src/issuer/issuer.service.ts index 2c520cee..2095fe2a 100644 --- a/apps/issuer-backend/src/issuer/issuer.service.ts +++ b/apps/issuer-backend/src/issuer/issuer.service.ts @@ -26,13 +26,7 @@ import { import { OID4VCIServer } from '@sphereon/oid4vci-issuer-server'; import { SdJwtDecodedVerifiableCredentialPayload } from '@sphereon/ssi-types'; import { DIDDocument } from 'did-resolver'; -import { - importJWK, - SignJWT, - decodeProtectedHeader, - JWK, - jwtVerify, -} from 'jose'; +import { importJWK, decodeProtectedHeader, JWK, jwtVerify } from 'jose'; import { IssuerMetadata } from 'src/issuer/types'; import { v4 } from 'uuid'; import { @@ -117,14 +111,14 @@ export class IssuerService implements OnModuleInit { } async init() { - // import the private key. - // get verifier. Only ES256 is supported for now. - const verifier = await ES256.getVerifier(this.keyService.getPublicKey()); + const verifier = await ES256.getVerifier( + await this.keyService.getPublicKey() + ); // crearre the sd-jwt instance with the required parameters. const sdjwt = new SDJwtVcInstance({ - signer: this.keyService.sign, + signer: this.keyService.signer, verifier, signAlg: 'ES256', hasher: digest, diff --git a/apps/issuer-backend/src/key/filesystem-key.service.ts b/apps/issuer-backend/src/key/filesystem-key.service.ts index 9245de55..d8c8bbe6 100644 --- a/apps/issuer-backend/src/key/filesystem-key.service.ts +++ b/apps/issuer-backend/src/key/filesystem-key.service.ts @@ -11,6 +11,7 @@ import { import { v4 } from 'uuid'; import { KeyService } from './key.service'; import { Injectable } from '@nestjs/common'; +import { Signer } from '@sd-jwt/types'; //TODO: implement a vault integration like in the backend /** @@ -18,10 +19,10 @@ import { Injectable } from '@nestjs/common'; */ @Injectable() export class FileSystemKeyService implements KeyService { + public signer: Signer; private privateKey: JWK; private publicKey: JWK; private privateKeyInstance: KeyLike; - signer: (data: string) => Promise; async onModuleInit(): Promise { await this.init(); @@ -84,14 +85,6 @@ export class FileSystemKeyService implements KeyService { return Promise.resolve(copy); } - /** - * Returns the signature of the given value - * @param value - */ - sign(value: string) { - return this.signer(value); - } - async signJWT(payload: JWTPayload, header: JWTHeaderParameters) { return new SignJWT(payload) .setProtectedHeader(header) diff --git a/apps/issuer-backend/src/key/key.module.ts b/apps/issuer-backend/src/key/key.module.ts index 4f6cc419..58d30501 100644 --- a/apps/issuer-backend/src/key/key.module.ts +++ b/apps/issuer-backend/src/key/key.module.ts @@ -1,6 +1,6 @@ import { DynamicModule, Global, Module } from '@nestjs/common'; import { FileSystemKeyService } from './filesystem-key.service'; -import Joi from 'joi'; +import * as Joi from 'joi'; import { ConfigModule, ConfigService } from '@nestjs/config'; import { HttpModule, HttpService } from '@nestjs/axios'; import { VaultKeyService } from './vault-key.service'; @@ -19,6 +19,12 @@ export const KEY_VALIDATION_SCHEMA = { then: Joi.required(), otherwise: Joi.optional(), }), + VAULT_KEY_ID: Joi.string().when('KM_TYPE', { + is: 'vault', + // biome-ignore lint/suspicious/noThenProperty: + then: Joi.required(), + otherwise: Joi.optional(), + }), }; @Global() @Module({}) diff --git a/apps/issuer-backend/src/key/key.service.ts b/apps/issuer-backend/src/key/key.service.ts index eef1fdb7..66b53bfd 100644 --- a/apps/issuer-backend/src/key/key.service.ts +++ b/apps/issuer-backend/src/key/key.service.ts @@ -1,10 +1,13 @@ import { OnModuleInit } from '@nestjs/common'; +import { Signer } from '@sd-jwt/types'; import { JWK, JWTHeaderParameters, JWTPayload } from 'jose'; /** * Generic interface for a key service */ export abstract class KeyService implements OnModuleInit { + public abstract signer: Signer; + async onModuleInit() { await this.init(); } @@ -30,7 +33,7 @@ export abstract class KeyService implements OnModuleInit { * Returns the signature of the given value * @param value */ - abstract sign(value: string): Promise; + // abstract sign(value: string): Promise; abstract signJWT( payload: JWTPayload, diff --git a/apps/issuer-backend/src/key/vault-key.service.ts b/apps/issuer-backend/src/key/vault-key.service.ts index 2010e9be..a6b1ce16 100644 --- a/apps/issuer-backend/src/key/vault-key.service.ts +++ b/apps/issuer-backend/src/key/vault-key.service.ts @@ -4,12 +4,12 @@ import { HttpService } from '@nestjs/axios'; import { firstValueFrom } from 'rxjs'; import { importSPKI, exportJWK, JWTHeaderParameters } from 'jose'; import { ConfigService } from '@nestjs/config'; -import { v4 } from 'uuid'; -import { JwtPayload } from '@sd-jwt/types'; +import { JwtPayload, Signer } from '@sd-jwt/types'; @Injectable() export class VaultKeyService extends KeyService { - private keyId = 'keyID'; + public signer: Signer; + private keyId: string; // url to the vault instance private vaultUrl: string; @@ -27,6 +27,7 @@ export class VaultKeyService extends KeyService { 'X-Vault-Token': this.configService.get('VAULT_TOKEN'), }, }; + this.keyId = this.configService.get('VAULT_KEY_ID'); } /** @@ -34,10 +35,10 @@ export class VaultKeyService extends KeyService { */ async init() { await this.getPublicKey().catch(() => { - this.keyId = v4(); //TODO: we need to persist this key id this.create(); }); + this.signer = async (input: string) => this.sign(input); } /**