Skip to content

Commit

Permalink
Merge pull request #13 from cre8/cre8/issue12
Browse files Browse the repository at this point in the history
WIP: Add generic key storage
  • Loading branch information
cre8 authored May 7, 2024
2 parents c49ec4c + 3110ca3 commit ef8ea17
Show file tree
Hide file tree
Showing 23 changed files with 982 additions and 460 deletions.
5 changes: 3 additions & 2 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down
122 changes: 122 additions & 0 deletions apps/backend/src/keys/db-keys.service.ts
Original file line number Diff line number Diff line change
@@ -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<Key>) {
super();
}

async create(createKeyDto: CreateKey, user: string): Promise<KeyResponse> {
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<JoseKeyLike>(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<JoseKeyLike>(key.privateKey, 'ES256');
return new SignJWT({ ...kbJwt.payload, aud })
.setProtectedHeader({ typ: kbJwt.header.typ, alg: 'ES256' })
.sign(jwk);
}
}
1 change: 0 additions & 1 deletion apps/backend/src/keys/dto/key-response.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { JsonWebKey } from 'node:crypto';
export class KeyResponse {
/**
* Id of the key
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/keys/dto/proof-request.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { IsObject, IsOptional, IsString } from 'class-validator';
export class ProofRequest {
@IsString()
@IsOptional()
kid?: string;
kid: string;

@IsObject()
payload: JWTPayload;
Expand Down
136 changes: 0 additions & 136 deletions apps/backend/src/keys/keys.controller.ts

This file was deleted.

60 changes: 50 additions & 10 deletions apps/backend/src/keys/keys.module.ts
Original file line number Diff line number Diff line change
@@ -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: <explanation>
then: Joi.required(),
otherwise: Joi.optional(),
}),
VAULT_TOKEN: Joi.string().when('KM_TYPE', {
is: 'vault',
// biome-ignore lint/suspicious/noThenProperty: <explanation>
then: Joi.required(),
otherwise: Joi.optional(),
}),
};
@Global()
@Module({})
// biome-ignore lint/complexity/noStaticOnlyClass: <explanation>
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<string>('KM_TYPE');
return kmType === 'vault'
? new VaultKeysService(httpService, configService)
: new DbKeysService(dataSource.getRepository(Key));
},
inject: [ConfigService, HttpService, DataSource],
},
],
exports: ['KeyService'],
};
}
}
Loading

0 comments on commit ef8ea17

Please sign in to comment.