diff --git a/.env.demo b/.env.demo index 798d80260..9f88d9787 100644 --- a/.env.demo +++ b/.env.demo @@ -102,6 +102,7 @@ UTILITIES_NKEY_SEED= CLOUD_WALLET_NKEY_SEED= GEOLOCATION_NKEY_SEED= NOTIFICATION_NKEY_SEED= +X509_NKEY_SEED= KEYCLOAK_DOMAIN=http://localhost:8080/ KEYCLOAK_ADMIN_URL=http://localhost:8080 diff --git a/.env.sample b/.env.sample index 4da82816f..3f9af8f27 100644 --- a/.env.sample +++ b/.env.sample @@ -121,6 +121,7 @@ CREDENTAILDEFINITION_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for SCHEMA_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for schema service UTILITIES_NKEY_SEED= xxxxxxxxxxxxx // Please provide Nkeys secret for utilities service GEOLOCATION_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for geo-location service +X509_NKEY_SEED= xxxxxxxxxxx // Please provide Nkeys secret for x509 service AFJ_AGENT_TOKEN_PATH=/apps/agent-provisioning/AFJ/token/ diff --git a/apps/agent-service/src/agent-service.controller.ts b/apps/agent-service/src/agent-service.controller.ts index 0c4d7c81b..e0f82ce20 100644 --- a/apps/agent-service/src/agent-service.controller.ts +++ b/apps/agent-service/src/agent-service.controller.ts @@ -27,6 +27,11 @@ import { user } from '@prisma/client'; import { InvitationMessage } from '@credebl/common/interfaces/agent-service.interface'; import { AgentSpinUpStatus } from '@credebl/enum/enum'; import { SignDataDto } from '../../api-gateway/src/agent-service/dto/agent-service.dto'; +import { + IX509ImportCertificateOptionsDto, + x509CertificateDecodeDto, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; @Controller() export class AgentServiceController { @@ -380,4 +385,31 @@ export class AgentServiceController { async oidcDeleteCredentialOffer(payload: { url: string; orgId: string }): Promise { return this.agentServiceService.oidcDeleteCredentialOffer(payload.url, payload.orgId); } + + @MessagePattern({ cmd: 'agent-create-x509-certificate' }) + async createX509Certificate(payload: { + options: X509CreateCertificateOptions; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.createX509Certificate(payload.options, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-decode-x509-certificate' }) + async decodeX509Certificate(payload: { + options: x509CertificateDecodeDto; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.decodeX509Certificate(payload.options, payload.url, payload.orgId); + } + + @MessagePattern({ cmd: 'agent-import-x509-certificate' }) + async importX509Certificate(payload: { + options: IX509ImportCertificateOptionsDto; + url: string; + orgId: string; + }): Promise { + return this.agentServiceService.importX509Certificate(payload.options, payload.url, payload.orgId); + } } diff --git a/apps/agent-service/src/agent-service.service.ts b/apps/agent-service/src/agent-service.service.ts index 4f4e877cb..5d20238d4 100644 --- a/apps/agent-service/src/agent-service.service.ts +++ b/apps/agent-service/src/agent-service.service.ts @@ -80,6 +80,11 @@ import { NATSClient } from '@credebl/common/NATSClient'; import { SignDataDto } from '../../api-gateway/src/agent-service/dto/agent-service.dto'; import { IVerificationMethod } from 'apps/organization/interfaces/organization.interface'; import { getAgentUrl } from '@credebl/common/common.utils'; +import { + IX509ImportCertificateOptionsDto, + x509CertificateDecodeDto, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; @Injectable() @WebSocketGateway() export class AgentServiceService { @@ -89,7 +94,6 @@ export class AgentServiceService { private readonly agentServiceRepository: AgentServiceRepository, private readonly prisma: PrismaService, private readonly commonService: CommonService, - private readonly connectionService: ConnectionService, @Inject('NATS_CLIENT') private readonly agentServiceProxy: ClientProxy, @Inject(CACHE_MANAGER) private cacheService: Cache, private readonly userActivityRepository: UserActivityRepository, @@ -2221,4 +2225,49 @@ export class AgentServiceService { throw error; } } + + async createX509Certificate(options: X509CreateCertificateOptions, url: string, orgId: string): Promise { + try { + this.logger.log('Start creating X509 certificate'); + this.logger.debug('Creating X509 certificate with options', options); + const getApiKey = await this.getOrgAgentApiKey(orgId); + const x509Certificate = await this.commonService + .httpPost(url, options, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return x509Certificate; + } catch (error) { + this.logger.error(`Error in creating x509 certificate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async decodeX509Certificate(options: x509CertificateDecodeDto, url: string, orgId: string): Promise { + try { + this.logger.log('Start decoding X509 certificate'); + this.logger.debug('Decoding X509 certificate with options', options); + const getApiKey = await this.getOrgAgentApiKey(orgId); + const x509Certificate = await this.commonService + .httpPost(url, options, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return x509Certificate; + } catch (error) { + this.logger.error(`Error in decoding x509 certificate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } + + async importX509Certificate(options: IX509ImportCertificateOptionsDto, url: string, orgId: string): Promise { + try { + this.logger.log('Start importing X509 certificate'); + this.logger.debug(`Importing X509 certificate with options`, options.certificate); + const getApiKey = await this.getOrgAgentApiKey(orgId); + const x509Certificate = await this.commonService + .httpPost(url, options, { headers: { authorization: getApiKey } }) + .then(async (response) => response); + return x509Certificate; + } catch (error) { + this.logger.error(`Error in creating x509 certificate in agent service : ${JSON.stringify(error)}`); + throw error; + } + } } diff --git a/apps/api-gateway/src/app.module.ts b/apps/api-gateway/src/app.module.ts index 04a4ab856..f586c05e7 100644 --- a/apps/api-gateway/src/app.module.ts +++ b/apps/api-gateway/src/app.module.ts @@ -33,6 +33,7 @@ import { LoggerModule } from '@credebl/logger/logger.module'; import { GlobalConfigModule } from '@credebl/config/global-config.module'; import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; +import { X509Module } from './x509/x509.module'; @Module({ imports: [ @@ -66,7 +67,8 @@ import { Oid4vcIssuanceModule } from './oid4vc-issuance/oid4vc-issuance.module'; CacheModule.register({ store: redisStore, host: process.env.REDIS_HOST, port: process.env.REDIS_PORT }), GeoLocationModule, CloudWalletModule, - Oid4vcIssuanceModule + Oid4vcIssuanceModule, + X509Module ], controllers: [AppController], providers: [ diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts index 4d056d2fc..77c88b075 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/issuer-sessions.dto.ts @@ -22,12 +22,6 @@ import { import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -/* ========= Enums ========= */ -export enum CredentialFormat { - SdJwtVc = 'vc+sd-jwt', - Mdoc = 'mdoc' -} - /* ========= disclosureFrame custom validator ========= */ function isDisclosureFrameValue(v: unknown): boolean { if ('boolean' === typeof v) { diff --git a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts index 57dc99214..b4da516ab 100644 --- a/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts +++ b/apps/api-gateway/src/oid4vc-issuance/oid4vc-issuance.service.ts @@ -7,6 +7,7 @@ import { BaseService } from 'libs/service/base.service'; import { oidc_issuer, user } from '@prisma/client'; import { CreateCredentialTemplateDto, UpdateCredentialTemplateDto } from './dtos/oid4vc-issuer-template.dto'; import { + CreateCredentialOfferD2ADto, CreateOidcCredentialOfferDto, GetAllCredentialOfferDto, UpdateCredentialRequestDto @@ -98,7 +99,10 @@ export class Oid4vcIssuanceService extends BaseService { return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-create-credential-offer', payload); } - async createOidcCredentialOfferD2A(oidcCredentialD2APayload, orgId: string): Promise { + async createOidcCredentialOfferD2A( + oidcCredentialD2APayload: CreateCredentialOfferD2ADto, + orgId: string + ): Promise { const payload = { oidcCredentialD2APayload, orgId }; return this.natsClient.sendNatsMessage(this.issuanceProxy, 'oid4vc-create-credential-offer-D2A', payload); } diff --git a/apps/api-gateway/src/x509/dtos/x509.dto.ts b/apps/api-gateway/src/x509/dtos/x509.dto.ts new file mode 100644 index 000000000..23548ecdb --- /dev/null +++ b/apps/api-gateway/src/x509/dtos/x509.dto.ts @@ -0,0 +1,382 @@ +import { ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; +import { + ArrayNotEmpty, + IsArray, + IsBoolean, + IsDate, + IsEnum, + IsNotEmpty, + IsNotEmptyObject, + IsNumber, + IsOptional, + IsString, + Max, + Min, + ValidateNested +} from 'class-validator'; +import { Transform, Type } from 'class-transformer'; +import { SortFields, X509ExtendedKeyUsage, X509KeyUsage, x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; +import { IX509SearchCriteria } from '@credebl/common/interfaces/x509.interface'; +import { toNumber, trim } from '@credebl/common/cast.helper'; + +export class AuthorityAndSubjectKeyDto { + @ApiProperty({ + enum: x5cKeyType, + //default: x5cKeyType.P256.toString(), + description: 'Type of the key used for signing the X.509 Certificate (default is p256)' + }) + @IsOptional() + @IsEnum(x5cKeyType) + keyType: x5cKeyType = x5cKeyType.P256; +} + +export enum GeneralNameType { + DNS = 'dns', + DN = 'dn', + EMAIL = 'email', + GUID = 'guid', + IP = 'ip', + URL = 'url', + UPN = 'upn', + REGISTERED_ID = 'id' +} + +export class NameDto { + @ApiProperty({ + example: Object.keys(GeneralNameType), + enum: GeneralNameType + }) + @IsNotEmpty() + @IsEnum(GeneralNameType) + type: GeneralNameType; + + @ApiProperty() + @IsNotEmpty() + @IsString() + value: string; +} + +export class X509CertificateIssuerAndSubjectOptionsDto { + @ApiPropertyOptional() @IsOptional() @IsString() countryName?: string; + @ApiPropertyOptional() @IsOptional() @IsString() stateOrProvinceName?: string; + @ApiPropertyOptional() @IsOptional() @IsString() organizationalUnit?: string; + @ApiPropertyOptional() @IsOptional() @IsString() commonName?: string; +} + +class ValidityDto { + @ApiPropertyOptional() @IsOptional() @IsDate() @Type(() => Date) notBefore?: Date; + @ApiPropertyOptional() @IsOptional() @IsDate() @Type(() => Date) notAfter?: Date; +} + +export class KeyUsageDto { + @ApiProperty({ + enum: X509KeyUsage, + isArray: true, + example: Object.keys(X509KeyUsage) + }) + @IsArray() + @IsEnum(X509KeyUsage, { each: true }) + usages: X509KeyUsage[]; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + markAsCritical?: boolean; +} + +export class ExtendedKeyUsageDto { + @ApiProperty({ + enum: X509ExtendedKeyUsage, + isArray: true, + example: Object.keys(X509ExtendedKeyUsage) + }) + @IsArray() + @IsEnum(X509ExtendedKeyUsage, { each: true }) + usages: X509ExtendedKeyUsage[]; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + markAsCritical?: boolean; +} + +export class NameListDto { + @ApiProperty({ type: [NameDto] }) + @ArrayNotEmpty() + @IsArray() + @ValidateNested() + @Type(() => NameDto) + name: NameDto[]; + + @ApiPropertyOptional() + @IsOptional() + @IsBoolean() + markAsCritical?: boolean; +} + +export class AuthorityAndSubjectKeyIdentifierDto { + @ApiPropertyOptional() + @IsBoolean() + include: boolean; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + markAsCritical?: boolean; +} + +export class BasicConstraintsDto { + @ApiProperty() + @IsBoolean() + ca: boolean; + + @ApiPropertyOptional() + @IsNumber() + @IsOptional() + pathLenConstraint?: number; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + markAsCritical?: boolean; +} + +export class CrlDistributionPointsDto { + @ApiProperty({ type: [String] }) + @IsArray() + @IsString({ each: true }) + urls: string[]; + + @ApiPropertyOptional() + @IsBoolean() + @IsOptional() + markAsCritical?: boolean; +} + +export class X509CertificateExtensionsOptionsDto { + @ApiPropertyOptional({ type: KeyUsageDto }) + @IsOptional() + @ValidateNested() + @Type(() => KeyUsageDto) + keyUsage?: KeyUsageDto; + + @ApiPropertyOptional({ type: ExtendedKeyUsageDto }) + @IsOptional() + @ValidateNested() + @Type(() => ExtendedKeyUsageDto) + extendedKeyUsage?: ExtendedKeyUsageDto; + + @ApiPropertyOptional({ type: AuthorityAndSubjectKeyIdentifierDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyIdentifierDto) + authorityKeyIdentifier?: AuthorityAndSubjectKeyIdentifierDto; + + @ApiPropertyOptional({ type: AuthorityAndSubjectKeyIdentifierDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyIdentifierDto) + subjectKeyIdentifier?: AuthorityAndSubjectKeyIdentifierDto; + + @ApiPropertyOptional({ type: NameListDto }) + @IsOptional() + @ValidateNested() + @Type(() => NameListDto) + issuerAlternativeName?: NameListDto; + + @ApiPropertyOptional({ type: NameListDto }) + @IsOptional() + @ValidateNested() + @Type(() => NameListDto) + subjectAlternativeName?: NameListDto; + + @ApiPropertyOptional({ type: BasicConstraintsDto }) + @IsOptional() + @ValidateNested() + @Type(() => BasicConstraintsDto) + basicConstraints?: BasicConstraintsDto; + + @ApiPropertyOptional({ type: CrlDistributionPointsDto }) + @IsOptional() + @ValidateNested() + @Type(() => CrlDistributionPointsDto) + crlDistributionPoints?: CrlDistributionPointsDto; +} + +// Main DTO +//@ApiExtraModels(X509CertificateIssuerAndSubjectOptionsDto) +export class X509CreateCertificateOptionsDto { + @ApiPropertyOptional({ type: () => AuthorityAndSubjectKeyDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyDto) + authorityKey?: AuthorityAndSubjectKeyDto; + + /** + * + * The key that is the subject of the X.509 Certificate + * + * If the `subjectPublicKey` is not included, the `authorityKey` will be used. + * This means that the certificate is self-signed + * + */ + @ApiPropertyOptional({ type: () => AuthorityAndSubjectKeyDto }) + @IsOptional() + @ValidateNested() + @Type(() => AuthorityAndSubjectKeyDto) + subjectPublicKey?: AuthorityAndSubjectKeyDto; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + serialNumber?: string; + + @ApiProperty({ oneOf: [{ $ref: getSchemaPath(X509CertificateIssuerAndSubjectOptionsDto) }, { type: 'string' }] }) + // @ApiProperty({ type: X509CertificateIssuerAndSubjectOptionsDto }) + @ValidateNested() + @Type(() => X509CertificateIssuerAndSubjectOptionsDto) + issuer: X509CertificateIssuerAndSubjectOptionsDto | string; + + @ApiPropertyOptional({ + oneOf: [{ $ref: getSchemaPath(X509CertificateIssuerAndSubjectOptionsDto) }, { type: 'string' }] + }) + @IsOptional() + subject?: X509CertificateIssuerAndSubjectOptionsDto | string; + + @ApiPropertyOptional({ type: () => ValidityDto }) + @IsOptional() + @ValidateNested() + @Type(() => ValidityDto) + validity?: ValidityDto; + + @ApiPropertyOptional({ type: () => X509CertificateExtensionsOptionsDto }) + @IsOptional() + @ValidateNested() + @Type(() => X509CertificateExtensionsOptionsDto) + extensions?: X509CertificateExtensionsOptionsDto; +} + +export class X509ImportCertificateOptionsDto { + @ApiProperty({ + description: 'certificate', + required: true + }) + @IsString() + certificate: string; + + @ApiPropertyOptional({ + description: 'Private key in base64 string format' + }) + @IsOptional() + @IsString() + privateKey?: string; + + @ApiProperty({ + enum: x5cKeyType, + //default: x5cKeyType.P256.toString(), + description: 'Type of the key used for signing the X.509 Certificate (default is p256)' + }) + @IsOptional() + @IsEnum(x5cKeyType) + keyType: x5cKeyType = x5cKeyType.P256; +} + +export class x509Input { + @ApiProperty({ + description: 'certificate', + required: true + }) + @IsString() + certificate: string; +} + +export class X509CertificateSubjectOptionsDto { + @ApiProperty() @IsNotEmpty() @IsString() countryName: string; + // @ApiPropertyOptional() @IsOptional() @IsString() stateOrProvinceName?: string; + // @ApiPropertyOptional() @IsOptional() @IsString() organizationalUnit?: string; + @ApiProperty() @IsNotEmpty() @IsString() commonName: string; +} + +export class BasicX509CreateCertificateConfig { + @ApiProperty({ type: () => X509CertificateSubjectOptionsDto, required: true }) + @IsNotEmptyObject() + @ValidateNested() + @Type(() => X509CertificateSubjectOptionsDto) + subject: X509CertificateSubjectOptionsDto; + + @ApiPropertyOptional({ type: () => AuthorityAndSubjectKeyDto }) + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AuthorityAndSubjectKeyDto) + subjectKey?: AuthorityAndSubjectKeyDto; +} + +export interface X509GenericRecordContent { + dcs?: string | string[]; + root?: string; +} + +export interface X509GenericRecord { + id: string; + content?: X509GenericRecordContent; +} + +export class x509OptionsDto { + @ApiProperty({ example: 'exampleOrg' }) + @IsNotEmpty() + @IsString() + commonName: string; + + @ApiProperty({ example: 'IN' }) + @IsNotEmpty() + @IsString() + countryName: string; +} + +export class X509SearchCriteriaDto implements IX509SearchCriteria { + @ApiProperty({ required: false, example: '1' }) + @Transform(({ value }) => toNumber(value)) + @IsOptional() + pageNumber: number = 1; + + @ApiProperty({ required: false, example: '10' }) + @IsOptional() + @Transform(({ value }) => toNumber(value)) + @Min(1, { message: 'Page size must be greater than 0' }) + @Max(100, { message: 'Page size must be less than 100' }) + pageSize: number = 10; + + @ApiProperty({ required: false }) + @IsOptional() + @Transform(({ value }) => trim(value)) + @Type(() => String) + searchByText: string = ''; + + @ApiProperty({ + required: false + }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsEnum(SortFields) + sortField: string = SortFields.CREATED_DATE_TIME; + + @ApiProperty({ + required: false, + enum: x5cKeyType, + enumName: 'keyType' + }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsEnum(x5cKeyType) + keyType: x5cKeyType; + + @ApiProperty({ + required: false, + enum: x5cRecordStatus, + enumName: 'status' + }) + @Transform(({ value }) => trim(value)) + @IsOptional() + @IsEnum(x5cRecordStatus) + status: x5cRecordStatus; +} diff --git a/apps/api-gateway/src/x509/x509.controller.ts b/apps/api-gateway/src/x509/x509.controller.ts new file mode 100644 index 000000000..e413071fa --- /dev/null +++ b/apps/api-gateway/src/x509/x509.controller.ts @@ -0,0 +1,279 @@ +import { + ApiBearerAuth, + ApiForbiddenResponse, + ApiOperation, + ApiQuery, + ApiResponse, + ApiTags, + ApiUnauthorizedResponse +} from '@nestjs/swagger'; +import { CommonService } from '@credebl/common'; +import { + Controller, + Get, + Put, + Param, + UseGuards, + UseFilters, + Post, + Body, + Res, + HttpStatus, + Query, + ParseUUIDPipe, + BadRequestException +} from '@nestjs/common'; + +import IResponse from '@credebl/common/interfaces/response.interface'; +import { Response } from 'express'; +import { ApiResponseDto } from '../dtos/apiResponse.dto'; +import { UnauthorizedErrorDto } from '../dtos/unauthorized-error.dto'; +import { ForbiddenErrorDto } from '../dtos/forbidden-error.dto'; +import { AuthGuard } from '@nestjs/passport'; +import { User } from '../authz/decorators/user.decorator'; +import { user } from '@prisma/client'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { OrgRolesGuard } from '../authz/guards/org-roles.guard'; +import { Roles } from '../authz/decorators/roles.decorator'; +import { OrgRoles } from 'libs/org-roles/enums'; +import { CustomExceptionFilter } from 'apps/api-gateway/common/exception-handler'; + +import { TrimStringParamPipe } from '@credebl/common/cast.helper'; +import { X509Service } from './x509.service'; +import { + X509CreateCertificateOptionsDto, + X509ImportCertificateOptionsDto, + X509SearchCriteriaDto +} from './dtos/x509.dto'; +import { SortFields, x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; + +@UseFilters(CustomExceptionFilter) +@Controller('x509') +@ApiTags('x509') +@ApiUnauthorizedResponse({ description: 'Unauthorized', type: UnauthorizedErrorDto }) +@ApiForbiddenResponse({ description: 'Forbidden', type: ForbiddenErrorDto }) +export class X509Controller { + constructor( + private readonly x509Service: X509Service, + private readonly commonService: CommonService + ) {} + + /** + * Create a new x509 + * @param createDto The details of the x509 to be created + * @returns Created x509 details + */ + @Post('/:orgId') + @ApiOperation({ + summary: 'Create a new X509', + description: 'Create a new x509 with the provided details.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async createX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Body() createDto: X509CreateCertificateOptionsDto, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.createX509(orgId, createDto, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.x509.success.create, + data: record + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } + + @Put('/:orgId/activate/:id') + @ApiOperation({ + summary: 'Activate X509 certificate', + description: 'Activate X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async activateX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id', ParseUUIDPipe) id: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.activateX509(orgId, id, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.activated, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Put('/:orgId/deactivate/:id') + @ApiOperation({ + summary: 'Deactive X509 certificate', + description: 'Deactive X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async deActivateX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id', ParseUUIDPipe) id: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.deActivateX509(orgId, id, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.deActivated, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/:orgId') + @ApiOperation({ + summary: 'Get all X509 certificate', + description: 'Get all X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + @ApiQuery({ + name: 'keyType', + enum: x5cKeyType, + required: false + }) + @ApiQuery({ + name: 'status', + enum: x5cRecordStatus, + required: false + }) + @ApiQuery({ + name: 'sortField', + enum: SortFields, + required: false + }) + async getAllX509ByOrgId( + @Query() x509SearchCriteriaDto: X509SearchCriteriaDto, + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.getX509CertificatesByOrgId(orgId, x509SearchCriteriaDto, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.fetchAll, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + @Get('/:orgId/:id') + @ApiOperation({ + summary: 'Get X509 certificate', + description: 'Get X509 certificate' + }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async getX509Certificate( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Param('id', ParseUUIDPipe) id: string, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.getX509Certificate(orgId, id, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.OK, + message: ResponseMessages.x509.success.fetch, + data: record + }; + return res.status(HttpStatus.OK).json(finalResponse); + } + + /** + * Import a new x509 + * @param importDto The details of the x509 to be created + * @returns Imported x509 certificate + */ + @Post('/:orgId/import') + @ApiOperation({ + summary: 'Import a new X509', + description: 'Import a new x509 with the provided details.' + }) + @ApiResponse({ status: HttpStatus.CREATED, description: 'Success', type: ApiResponseDto }) + @UseGuards(AuthGuard('jwt'), OrgRolesGuard) + @Roles(OrgRoles.OWNER, OrgRoles.ADMIN) + @ApiBearerAuth() + async importX509( + @Param( + 'orgId', + TrimStringParamPipe, + new ParseUUIDPipe({ + exceptionFactory: (): Error => { + throw new BadRequestException(ResponseMessages.organisation.error.invalidOrgId); + } + }) + ) + orgId: string, + @Body() importDto: X509ImportCertificateOptionsDto, + @Res() res: Response, + @User() reqUser: user + ): Promise { + const record = await this.x509Service.importX509(orgId, importDto, reqUser); + const finalResponse: IResponse = { + statusCode: HttpStatus.CREATED, + message: ResponseMessages.x509.success.import, + data: record + }; + return res.status(HttpStatus.CREATED).json(finalResponse); + } +} diff --git a/apps/api-gateway/src/x509/x509.module.ts b/apps/api-gateway/src/x509/x509.module.ts new file mode 100644 index 000000000..7a4394ca1 --- /dev/null +++ b/apps/api-gateway/src/x509/x509.module.ts @@ -0,0 +1,29 @@ +import { CommonModule, CommonService } from '@credebl/common'; + +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { ConfigModule } from '@nestjs/config'; +import { HttpModule } from '@nestjs/axios'; +import { Module } from '@nestjs/common'; +import { X509Controller } from './x509.controller'; +import { X509Service } from './x509.service'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { AwsService } from '@credebl/aws'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { NATSClient } from '@credebl/common/NATSClient'; +@Module({ + imports: [ + HttpModule, + ConfigModule.forRoot(), + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.API_GATEWAY_NKEY_SEED) + }, + CommonModule + ]) + ], + controllers: [X509Controller], + providers: [X509Service, CommonService, AwsService, NATSClient] +}) +export class X509Module {} diff --git a/apps/api-gateway/src/x509/x509.service.ts b/apps/api-gateway/src/x509/x509.service.ts new file mode 100644 index 000000000..e3d82046d --- /dev/null +++ b/apps/api-gateway/src/x509/x509.service.ts @@ -0,0 +1,88 @@ +import { Inject } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ClientProxy } from '@nestjs/microservices'; +import { BaseService } from 'libs/service/base.service'; + +import { user } from '@prisma/client'; + +import { NATSClient } from '@credebl/common/NATSClient'; +import { + X509CreateCertificateOptionsDto, + X509ImportCertificateOptionsDto, + X509SearchCriteriaDto +} from './dtos/x509.dto'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; + +@Injectable() +export class X509Service extends BaseService { + constructor( + @Inject('NATS_CLIENT') private readonly serviceProxy: ClientProxy, + private readonly natsClient: NATSClient + ) { + super('X509Service'); + } + + /** + * + * @param createDto + * @returns X509 creation Success + */ + async createX509( + orgId: string, + createDto: X509CreateCertificateOptionsDto, + reqUser: user + ): Promise { + this.logger.log(`Start creating x509 certficate`); + this.logger.debug(`payload : `, createDto, reqUser); + const payload = { options: createDto, user: reqUser, orgId }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'create-x509-certificate', payload); + } + + async activateX509(orgId: string, id: string, reqUser: user): Promise { + this.logger.log(`Start activating x509 certficate`); + this.logger.debug(`certificate Id : `, id); + const payload = { orgId, id, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'activate-x509-certificate', payload); + } + + async deActivateX509(orgId: string, id: string, reqUser: user): Promise { + this.logger.log(`Start deactivating x509 certficate`); + this.logger.debug(`certificate Id : `, id); + const payload = { orgId, id, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'deActivate-x509-certificate', payload); + } + + async getX509CertificatesByOrgId( + orgId: string, + x509SearchCriteriaDto: X509SearchCriteriaDto, + reqUser: user + ): Promise { + this.logger.log(`Start getting x509 certficate for org`); + this.logger.debug(`Filters applied : `, x509SearchCriteriaDto); + const payload = { orgId, options: x509SearchCriteriaDto, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-all-certificates', payload); + } + + async getX509Certificate(orgId: string, id: string, reqUser: user): Promise { + this.logger.log(`Start getting x509 certficate by id`); + this.logger.debug(`certificate Id : `, id); + const payload = { id, orgId, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'get-certificate', payload); + } + + /** + * + * @param importDto + * @returns X509 import Success + */ + async importX509( + orgId: string, + importDto: X509ImportCertificateOptionsDto, + reqUser: user + ): Promise { + this.logger.log(`Start importing x509 certficate by id`); + this.logger.debug(`certificate : `, importDto.certificate); + const payload = { orgId, options: importDto, user: reqUser }; + return this.natsClient.sendNatsMessage(this.serviceProxy, 'import-x509-certificate', payload); + } +} diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index b5e353c19..b765bdf5a 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/naming-convention, @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types, camelcase */ import { Prisma, credential_templates } from '@prisma/client'; import { GetAllCredentialOffer, SignerOption } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; +import { CredentialFormat } from '@credebl/enum/enum'; /* ============================================================================ Domain Types ============================================================================ */ @@ -15,11 +16,6 @@ interface TemplateAttribute { } type TemplateAttributes = Record; -export enum CredentialFormat { - SdJwtVc = 'vc+sd-jwt', - Mdoc = 'mso_mdoc' -} - export enum SignerMethodOption { DID = 'did', X5C = 'x5c' diff --git a/apps/oid4vc-issuance/src/main.ts b/apps/oid4vc-issuance/src/main.ts index 3c6933fc9..a76e0de0f 100644 --- a/apps/oid4vc-issuance/src/main.ts +++ b/apps/oid4vc-issuance/src/main.ts @@ -12,7 +12,7 @@ const logger = new Logger(); async function bootstrap(): Promise { const app = await NestFactory.createMicroservice(Oid4vcIssuanceModule, { transport: Transport.NATS, - options: getNatsOptions(CommonConstants.ISSUANCE_SERVICE, process.env.ISSUANCE_NKEY_SEED) + options: getNatsOptions(CommonConstants.OIDC4VC_ISSUANCE_SERVICE, process.env.ISSUANCE_NKEY_SEED) }); app.useLogger(app.get(NestjsLoggerServiceAdapter)); app.useGlobalFilters(new HttpExceptionFilter()); diff --git a/apps/x509/src/interfaces/x509.interface.ts b/apps/x509/src/interfaces/x509.interface.ts new file mode 100644 index 000000000..bfae58ad9 --- /dev/null +++ b/apps/x509/src/interfaces/x509.interface.ts @@ -0,0 +1,53 @@ +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; +import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; + +export interface CreateX509CertificateEntity { + orgId: string; // We'll accept orgId and find orgAgent internally + keyType: x5cKeyType; + status: string; + validFrom: Date; + expiry: Date; + certificateBase64: string; + createdBy: string; + lastChangedBy: string; +} + +export interface UpdateCertificateStatusDto { + status: x5cRecordStatus; + lastChangedBy: string; +} + +export interface CertificateDateCheckDto { + orgId: string; + validFrom: Date; + expiry: Date; + keyType: x5cKeyType; + status: x5cRecordStatus; + excludeCertificateId?: string; +} + +export interface OrgAgent { + id: string; + createDateTime: Date; + createdBy: string; + lastChangedDateTime: Date; + lastChangedBy: string; + orgDid: string; + verkey: string; + agentEndPoint: string; + agentId: string; + isDidPublic: boolean; + ledgerId: string; + orgAgentTypeId: string; + tenantId: string; +} + +export interface IX509ListCount { + total: number; + data: X509CertificateRecord[]; +} + +export interface IX509CollisionResult { + hasCollision: boolean; + collisions: X509CertificateRecord[]; +} diff --git a/apps/x509/src/main.ts b/apps/x509/src/main.ts new file mode 100644 index 000000000..b5a6d01e4 --- /dev/null +++ b/apps/x509/src/main.ts @@ -0,0 +1,21 @@ +import { NestFactory } from '@nestjs/core'; +import { HttpExceptionFilter } from 'libs/http-exception.filter'; +import { Logger } from '@nestjs/common'; +import { MicroserviceOptions, Transport } from '@nestjs/microservices'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import NestjsLoggerServiceAdapter from '@credebl/logger/nestjsLoggerServiceAdapter'; +import { X509Module } from './x509.module'; + +async function bootstrap(): Promise { + const app = await NestFactory.createMicroservice(X509Module, { + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.X509_NKEY_SEED) + }); + app.useLogger(app.get(NestjsLoggerServiceAdapter)); + app.useGlobalFilters(new HttpExceptionFilter()); + + await app.listen(); + Logger.log('X509 Microservice is listening to NATS '); +} +bootstrap(); diff --git a/apps/x509/src/repositories/x509.repository.ts b/apps/x509/src/repositories/x509.repository.ts new file mode 100644 index 000000000..037c2d4f0 --- /dev/null +++ b/apps/x509/src/repositories/x509.repository.ts @@ -0,0 +1,326 @@ +/* eslint-disable camelcase */ +// src/repositories/x509-certificate.repository.ts +import { Injectable, NotFoundException, Logger } from '@nestjs/common'; +import { PrismaService } from '@credebl/prisma-service'; +import { + CertificateDateCheckDto, + CreateX509CertificateEntity, + IX509CollisionResult, + IX509ListCount, + OrgAgent, + UpdateCertificateStatusDto +} from '../interfaces/x509.interface'; +import { x5cRecordStatus } from '@credebl/enum/enum'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { org_agents } from '@prisma/client'; +import { X509CertificateRecord } from '@credebl/common/interfaces/x509.interface'; + +@Injectable() +export class X509CertificateRepository { + constructor( + private readonly prisma: PrismaService, + private readonly logger: Logger + ) {} + + // Helper method to get orgAgent by orgId + private async getOrgAgentByOrgId(orgId: string): Promise { + try { + const orgAgent = await this.prisma.org_agents.findFirst({ + where: { + orgId + } + }); + + if (!orgAgent) { + throw new NotFoundException(`OrgAgent with orgId ${orgId} not found`); + } + + return orgAgent; + } catch (error) { + this.logger.error(`Error in getOrgAgentByOrgId: ${error.message}`); + throw error; + } + } + + // CREATE - Create new certificate using orgId + async create(createDto: CreateX509CertificateEntity): Promise { + try { + // Get orgAgent by orgId + const orgAgent = await this.getOrgAgentByOrgId(createDto.orgId); + + const certificate = await this.prisma.x509_certificates.create({ + data: { + orgAgentId: orgAgent.id, + keyType: createDto.keyType, + status: createDto.status, + validFrom: createDto.validFrom, + expiry: createDto.expiry, + certificateBase64: createDto.certificateBase64, + createdBy: createDto.createdBy, + lastChangedBy: createDto.lastChangedBy + } + }); + + return certificate; + } catch (error) { + this.logger.error(`Error in create certificate: ${error.message}`); + throw error; + } + } + + // READ - Find all certificates with optional filtering and pagination + async findAll(options?: { + orgId: string; + status?: string; + keyType?: string; + page?: number; + limit?: number; + }): Promise { + try { + const { orgId, status, keyType, page = 1, limit = 10 } = options || {}; + + const skip = (page - 1) * limit; + + const where: Parameters[0]['where'] = {}; + + // Build where conditions with joins + if (orgId || status || keyType) { + where.AND = []; + + where.AND.push({ + org_agents: { + orgId + } + }); + + if (status) { + where.AND.push({ status }); + } + + if (keyType) { + where.AND.push({ keyType }); + } + } + + const [data, total] = await Promise.all([ + this.prisma.x509_certificates.findMany({ + where, + skip, + take: limit, + orderBy: { + createdAt: 'desc' + } + }), + this.prisma.x509_certificates.count({ where }) + ]); + + return { data, total }; + } catch (error) { + this.logger.error(`Error in findAll certificates: ${error.message}`); + throw error; + } + } + + // READ - Find certificate by ID + async findById(orgId: string, id: string): Promise { + try { + const certificate = await this.prisma.x509_certificates.findUnique({ + where: { + id, + org_agents: { + orgId + } + } + }); + + if (!certificate) { + throw new NotFoundException(`Certificate with ID ${id} not found`); + } + + return certificate; + } catch (error) { + this.logger.error(`Error in findById: ${error.message}`); + throw error; + } + } + + // READ - Find certificates by organization ID + async findByOrgId(orgId: string): Promise { + try { + const certificates = await this.prisma.x509_certificates.findMany({ + where: { + org_agents: { + orgId + } + }, + orderBy: { + createdAt: 'desc' + } + }); + + return certificates; + } catch (error) { + this.logger.error(`Error in findByOrgId: ${error.message}`); + throw error; + } + } + + // UTILITY - Check date collision without throwing exception + async hasDateCollision(dateCheckDto: CertificateDateCheckDto): Promise { + try { + const { orgId, validFrom, expiry, excludeCertificateId } = dateCheckDto; + + const collisions = await this.prisma.x509_certificates.findMany({ + where: { + status: dateCheckDto.status, + keyType: dateCheckDto.keyType, + org_agents: { + orgId + }, + AND: [ + { + OR: [ + { + AND: [{ validFrom: { lte: expiry } }, { expiry: { gte: validFrom } }] + } + ] + }, + ...(excludeCertificateId ? [{ id: { not: excludeCertificateId } }] : []) + ] + } + }); + + return { + hasCollision: 0 < collisions.length, + collisions + }; + } catch (error) { + this.logger.error(`Error in hasDateCollision: ${error.message}`); + throw error; + } + } + + // UPDATE - Update certificate status + async updateStatus(id: string, statusDto: UpdateCertificateStatusDto): Promise { + try { + const certificate = await this.prisma.x509_certificates.update({ + where: { id }, + data: { + status: statusDto.status, + lastChangedBy: statusDto.lastChangedBy + } + }); + + return certificate; + } catch (error) { + this.logger.error(`Error in updateStatus: ${error.message}`); + throw error; + } + } + + // // DELETE - Delete certificate + // async delete(id: string) { + // try { + // await this.findById(id); // Check if certificate exists + + // await this.prisma.x509_certificates.delete({ + // where: { id } + // }); + // } catch (error) { + // this.logger.error(`Error in delete certificate: ${error.message}`); + // throw error; + // } + // } + + // UTILITY - Check if certificate exists + async exists(id: string): Promise { + try { + const count = await this.prisma.x509_certificates.count({ + where: { id } + }); + return 0 < count; + } catch (error) { + this.logger.error(`Error in exists check: ${error.message}`); + throw error; + } + } + + // UTILITY - Find expiring certificates for an org + async findExpiringCertificatesByOrg(orgId: string, days: number = 30): Promise { + try { + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() + days); + + const certificates = await this.prisma.x509_certificates.findMany({ + where: { + org_agents: { + orgId + }, + expiry: { + lte: expiryDate + }, + status: 'Active' + }, + orderBy: { + expiry: 'asc' + } + }); + + return certificates; + } catch (error) { + this.logger.error(`Error in findExpiringCertificatesByOrg: ${error.message}`); + throw error; + } + } + + async getCurrentActiveCertificate(orgId: string): Promise { + try { + const now = new Date(); + + const certificate = await this.prisma.x509_certificates.findFirst({ + where: { + org_agents: { + orgId + }, + status: x5cRecordStatus.Active, + validFrom: { + lte: now + }, + expiry: { + gte: now + } + }, + orderBy: { + createdAt: 'desc' + } + }); + + return certificate; + } catch (error) { + this.logger.error(`Error in getCurrentActiveCertificate: ${error.message}`); + throw error; + } + } + + async getAgentEndPoint(orgId: string): Promise { + try { + const agentDetails = await this.prisma.org_agents.findFirst({ + where: { + orgId + }, + include: { + organisation: true + } + }); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.x509.error.agentEndPointNotFound); + } + + return agentDetails; + } catch (error) { + this.logger.error(`Error in get getAgentEndPoint: ${error.message} `); + throw error; + } + } +} diff --git a/apps/x509/src/x509.controller.ts b/apps/x509/src/x509.controller.ts new file mode 100644 index 000000000..8227e88e4 --- /dev/null +++ b/apps/x509/src/x509.controller.ts @@ -0,0 +1,58 @@ +import { Controller } from '@nestjs/common'; +import { MessagePattern } from '@nestjs/microservices'; +import { X509CertificateService } from './x509.service'; +import { user } from '@prisma/client'; +import { IUserRequest } from '@credebl/user-request/user-request.interface'; +import { + IX509ImportCertificateOptionsDto, + IX509SearchCriteria, + X509CertificateRecord, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; + +@Controller() +export class X509CertificateController { + constructor(private readonly x509CertificateService: X509CertificateService) {} + + @MessagePattern({ cmd: 'create-x509-certificate' }) + async createCertificate(payload: { + orgId: string; + options: X509CreateCertificateOptions; + user: user; + }): Promise { + return this.x509CertificateService.createCertificate(payload); + } + + @MessagePattern({ cmd: 'activate-x509-certificate' }) + async activateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + return this.x509CertificateService.activateCertificate(payload); + } + + @MessagePattern({ cmd: 'deActivate-x509-certificate' }) + async deActivateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + return this.x509CertificateService.deActivateCertificate(payload); + } + + @MessagePattern({ cmd: 'get-all-certificates' }) + async getCertificateByOrgId(payload: { + orgId: string; + options: IX509SearchCriteria; + user: IUserRequest; + }): Promise<{ data: X509CertificateRecord[]; total: number }> { + return this.x509CertificateService.getCertificateByOrgId(payload.orgId, payload.options); + } + + @MessagePattern({ cmd: 'get-certificate' }) + async getCertificate(payload: { orgId: string; id: string; user: IUserRequest }): Promise { + return this.x509CertificateService.getCertificateById(payload.orgId, payload.id); + } + + @MessagePattern({ cmd: 'import-x509-certificate' }) + async importCertificate(payload: { + orgId: string; + options: IX509ImportCertificateOptionsDto; + user: user; + }): Promise { + return this.x509CertificateService.importCertificate(payload); + } +} diff --git a/apps/x509/src/x509.module.ts b/apps/x509/src/x509.module.ts new file mode 100644 index 000000000..8b2a9f0bb --- /dev/null +++ b/apps/x509/src/x509.module.ts @@ -0,0 +1,31 @@ +import { Logger, Module } from '@nestjs/common'; +import { X509CertificateService } from './x509.service'; +import { PrismaService } from '@credebl/prisma-service'; +import { ClientsModule, Transport } from '@nestjs/microservices'; +import { X509CertificateRepository } from './repositories/x509.repository'; +import { getNatsOptions } from '@credebl/common/nats.config'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ConfigModule as PlatformConfig } from '@credebl/config/config.module'; +import { GlobalConfigModule } from '@credebl/config/global-config.module'; +import { LoggerModule } from '@credebl/logger/logger.module'; +import { ContextInterceptorModule } from '@credebl/context/contextInterceptorModule'; +import { X509CertificateController } from './x509.controller'; + +@Module({ + imports: [ + GlobalConfigModule, + LoggerModule, + PlatformConfig, + ContextInterceptorModule, + ClientsModule.register([ + { + name: 'NATS_CLIENT', + transport: Transport.NATS, + options: getNatsOptions(CommonConstants.X509_SERVICE, process.env.X509_NKEY_SEED) + } + ]) + ], + controllers: [X509CertificateController], + providers: [X509CertificateService, PrismaService, X509CertificateRepository, Logger] +}) +export class X509Module {} diff --git a/apps/x509/src/x509.service.ts b/apps/x509/src/x509.service.ts new file mode 100644 index 000000000..c9c92bdfc --- /dev/null +++ b/apps/x509/src/x509.service.ts @@ -0,0 +1,346 @@ +// src/services/x509-certificate.service.ts +import { + ConflictException, + HttpException, + Inject, + Injectable, + InternalServerErrorException, + NotFoundException, + UnprocessableEntityException +} from '@nestjs/common'; +import { BaseService } from 'libs/service/base.service'; +import { X509CertificateRepository } from './repositories/x509.repository'; +import { user } from '@prisma/client'; +import { ClientProxy, RpcException } from '@nestjs/microservices'; +import { map } from 'rxjs'; +import { + IX509ImportCertificateOptionsDto, + IX509SearchCriteria, + x509CertificateDecodeDto, + X509CertificateRecord, + X509CreateCertificateOptions +} from '@credebl/common/interfaces/x509.interface'; +import { + CertificateDateCheckDto, + CreateX509CertificateEntity, + UpdateCertificateStatusDto +} from './interfaces/x509.interface'; +import { getAgentUrl } from '@credebl/common/common.utils'; +import { CommonConstants } from '@credebl/common/common.constant'; +import { ResponseMessages } from '@credebl/common/response-messages'; +import { x5cKeyType, x5cRecordStatus } from '@credebl/enum/enum'; + +@Injectable() +export class X509CertificateService extends BaseService { + constructor( + private readonly x509CertificateRepository: X509CertificateRepository, + @Inject('NATS_CLIENT') private readonly x509ServiceProxy: ClientProxy + ) { + super('x509Service'); + } + + async createCertificate(payload: { + orgId: string; + options: X509CreateCertificateOptions; + user: user; + }): Promise { + try { + this.logger.log(`Start creating x509 certificate`); + this.logger.debug(`Create x509 certificate with options`, payload); + const { options, user, orgId } = payload; + const url = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.X509_CREATE_CERTIFICATE); + + const certificateDateCheckDto: CertificateDateCheckDto = { + orgId, + validFrom: options.validity.notBefore, + expiry: options.validity.notAfter, + keyType: options.authorityKey.keyType, + status: x5cRecordStatus.Active + }; + const collisionForActiveRecords = await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + let certStatus: x5cRecordStatus; + if (collisionForActiveRecords.hasCollision) { + certificateDateCheckDto.status = x5cRecordStatus.PendingActivation; + const collisionForPendingRecords = + await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + if (collisionForPendingRecords.hasCollision) { + this.logger.log(`Creating x509 certificate has collision`); + this.logger.error(`Collision records`, collisionForActiveRecords); + throw new ConflictException(ResponseMessages.x509.error.collision); + } + + certStatus = x5cRecordStatus.PendingActivation; + } else { + certStatus = x5cRecordStatus.Active; + } + + const certificate = await this._createX509CertificateForOrg(options, url, orgId); + if (!certificate) { + throw new NotFoundException(ResponseMessages.x509.error.errorCreate); + } + + const createDto: CreateX509CertificateEntity = { + orgId, + certificateBase64: certificate.response.publicCertificateBase64, + keyType: options.authorityKey.keyType, + status: certStatus, + validFrom: options.validity.notBefore, + expiry: options.validity.notAfter, + createdBy: user.id, + lastChangedBy: user.id + }; + + return await this.x509CertificateRepository.create(createDto); + } catch (error) { + this.logger.error(`Error in createCertificate: ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async activateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + const { orgId, user, id } = payload; + const certificateRecord = await this.x509CertificateRepository.findById(orgId, id); + if (certificateRecord) { + const certificateDateCheckDto: CertificateDateCheckDto = { + orgId, + validFrom: certificateRecord.validFrom, + expiry: certificateRecord.expiry, + keyType: certificateRecord.keyType as x5cKeyType, + status: x5cRecordStatus.Active, + excludeCertificateId: id + }; + const collisionForActiveRecords = await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + if (collisionForActiveRecords.hasCollision) { + throw new ConflictException( + `${ResponseMessages.x509.error.collisionForActivatingX5c}. Conflict Records:[${collisionForActiveRecords.collisions.map((collision) => collision.id)}]` + ); + } + const statusDto: UpdateCertificateStatusDto = { + status: x5cRecordStatus.Active, + lastChangedBy: user.id + }; + + return this.x509CertificateRepository.updateStatus(id, statusDto); + } + + throw new NotFoundException(ResponseMessages.x509.error.notFound); + } + + async deActivateCertificate(payload: { orgId: string; id: string; user: user }): Promise { + const { orgId, user, id } = payload; + const certificateRecord = await this.x509CertificateRepository.findById(orgId, id); + if (certificateRecord) { + const statusDto: UpdateCertificateStatusDto = { + status: x5cRecordStatus.InActive, + lastChangedBy: user.id + }; + + return this.x509CertificateRepository.updateStatus(id, statusDto); + } + throw new NotFoundException(ResponseMessages.x509.error.notFound); + } + + async importCertificate(payload: { + orgId: string; + options: IX509ImportCertificateOptionsDto; + user: user; + }): Promise { + try { + const { options, user, orgId } = payload; + const url = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.X509_DECODE_CERTIFICATE); + + this.logger.log(`Decoding certificate to import`); + const decodedResult = await this._decodeX509CertificateForOrg({ certificate: options.certificate }, url, orgId); + if (!decodedResult || !decodedResult.response) { + this.logger.error(`Failed to decode certificate`); + throw new NotFoundException(ResponseMessages.x509.error.errorDecode); + } + + this.logger.log(`Decoded certificate`); + this.logger.debug(`certificate data:`, JSON.stringify(decodedResult)); + + const { publicKey } = decodedResult.response; + const decodedCert = decodedResult.response.x509Certificate; + + this.logger.log(`Start validating certificate`); + const isValidKeyType = Object.values(x5cKeyType).includes(publicKey.keyType as x5cKeyType); + + if (!isValidKeyType) { + this.logger.error(`keyType is not valid for importing certificate`); + throw new InternalServerErrorException(ResponseMessages.x509.error.import); + } + + const validFrom = new Date(decodedCert.notBefore); + const expiry = new Date(decodedCert.notAfter); + const certificateDateCheckDto: CertificateDateCheckDto = { + orgId, + validFrom, + expiry, + keyType: publicKey.keyType, + status: x5cRecordStatus.Active + }; + const collisionForActiveRecords = await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + let certStatus: x5cRecordStatus; + if (collisionForActiveRecords.hasCollision) { + certificateDateCheckDto.status = x5cRecordStatus.PendingActivation; + const collisionForPendingRecords = + await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); + + if (collisionForPendingRecords.hasCollision) { + this.logger.log(`Importing x509 certificate has collision`); + this.logger.error(`Collision records`, collisionForPendingRecords); + throw new UnprocessableEntityException(ResponseMessages.x509.error.collision); + } + certStatus = x5cRecordStatus.PendingActivation; + } else { + certStatus = x5cRecordStatus.Active; + } + const importurl = await getAgentUrl(await this.getAgentEndpoint(orgId), CommonConstants.X509_IMPORT_CERTIFICATE); + + this.logger.log(`Certificate validation done`); + const certificate = await this._importX509CertificateForOrg(options, importurl, orgId); + if (!certificate) { + throw new NotFoundException(ResponseMessages.x509.error.errorCreate); + } + this.logger.log(`Successfully imported certificate in wallet `); + const createDto: CreateX509CertificateEntity = { + orgId, + certificateBase64: certificate.response.issuerCertficicate, + keyType: publicKey.keyType, + status: certStatus, + validFrom, + expiry, + createdBy: user.id, + lastChangedBy: user.id + }; + this.logger.log(`Now adding certificate in platform for org : ${orgId} `); + + return await this.x509CertificateRepository.create(createDto); + } catch (error) { + this.logger.error(`Error in importing certificate: ${error}`); + throw new RpcException(error.response ? error.response : error); + } + } + + async getCertificateByOrgId( + orgId: string, + options: IX509SearchCriteria + ): Promise<{ data: X509CertificateRecord[]; total: number }> { + return this.x509CertificateRepository.findAll({ + orgId, + keyType: options.keyType, + status: options.status, + limit: options.pageSize, + page: options.pageNumber + }); + } + + async getCertificateById(orgId: string, id: string): Promise { + return this.x509CertificateRepository.findById(orgId, id); + } + + async _createX509CertificateForOrg( + options: X509CreateCertificateOptions, + url: string, + orgId: string + ): Promise<{ + response; + }> { + try { + const pattern = { cmd: 'agent-create-x509-certificate' }; + const payload = { options, url, orgId }; + this.logger.log(`Requesing agent service for create x509 certificate`); + this.logger.debug(`agent service payload - _createX509CertificateForOrg : `, payload); + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_createX509CertificateForOrg] [NATS call]- : ${JSON.stringify(error)}`); + throw error; + } + } + + async _decodeX509CertificateForOrg( + options: x509CertificateDecodeDto, + url: string, + orgId: string + ): Promise<{ + response; + }> { + try { + const pattern = { cmd: 'agent-decode-x509-certificate' }; + const payload = { options, url, orgId }; + this.logger.log(`Requesing agent service for decode x509 certificate`); + this.logger.debug(`agent service payload - _decodeX509CertificateForOrg : `, payload); + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_decodeX509CertificateForOrg] [NATS call]- : ${JSON.stringify(error)}`); + throw error; + } + } + + async _importX509CertificateForOrg( + options: IX509ImportCertificateOptionsDto, + url: string, + orgId: string + ): Promise<{ + response; + }> { + try { + const pattern = { cmd: 'agent-import-x509-certificate' }; + const payload = { options, url, orgId }; + this.logger.log(`Requesing agent service for importing x509 certificate`); + this.logger.debug(`agent service payload - _importX509CertificateForOrg : `, payload); + return await this.natsCall(pattern, payload); + } catch (error) { + this.logger.error(`[_importX509CertificateForOrg] [NATS call]- : ${JSON.stringify(error)}`); + throw error; + } + } + + async natsCall( + pattern: object, + payload: object + ): Promise<{ + response: string; + }> { + try { + return this.x509ServiceProxy + .send(pattern, payload) + .pipe( + map((response) => ({ + response + })) + ) + .toPromise() + .catch((error) => { + this.logger.error(`catch: ${JSON.stringify(error)}`); + throw new HttpException( + { + status: error.statusCode, + error: error.message + }, + error.error + ); + }); + } catch (error) { + this.logger.error(`[natsCall] - error in nats call : ${JSON.stringify(error)}`); + throw error; + } + } + + async getAgentEndpoint(orgId: string): Promise { + const agentDetails = await this.x509CertificateRepository.getAgentEndPoint(orgId); + + if (!agentDetails) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + if (!agentDetails.agentEndPoint || '' === agentDetails.agentEndPoint.trim()) { + throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); + } + + return agentDetails.agentEndPoint; + } +} diff --git a/apps/x509/tsconfig.app.json b/apps/x509/tsconfig.app.json new file mode 100644 index 000000000..812e0f82c --- /dev/null +++ b/apps/x509/tsconfig.app.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": false, + "outDir": "../../dist/apps/x509" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} diff --git a/libs/common/src/common.constant.ts b/libs/common/src/common.constant.ts index cd28289d3..63ee10043 100644 --- a/libs/common/src/common.constant.ts +++ b/libs/common/src/common.constant.ts @@ -126,6 +126,11 @@ export enum CommonConstants { URL_OIDC_ISSUER_SESSIONS_GET = '/openid4vc/issuance-sessions/#', URL_OIDC_ISSUER_SESSIONS_GET_ALL = '/openid4vc/issuance-sessions', + //X509 agent API URLs + URL_CREATE_X509_CERTIFICATE = '/x509', + URL_IMPORT_X509_CERTIFICATE = '/x509/import', + URL_DECODE_X509_CERTIFICATE = '/x509/decode', + // Nested attribute separator NESTED_ATTRIBUTE_SEPARATOR = '~', @@ -374,6 +379,7 @@ export enum CommonConstants { GEO_LOCATION_SERVICE = 'geo-location', CLOUD_WALLET_SERVICE = 'cloud-wallet', OIDC4VC_ISSUANCE_SERVICE = 'oid4vc-issuance', + X509_SERVICE = 'x509-service', ACCEPT_OFFER = '/didcomm/credentials/accept-offer', SEED_LENGTH = 32, @@ -405,7 +411,12 @@ export enum CommonConstants { OIDC_ISSUER_SESSIONS_UPDATE_OFFER = 'update-oid4vc-credential-offer', OIDC_ISSUER_SESSIONS_BY_ID = 'get-oid4vc-session-by-id', OIDC_ISSUER_SESSIONS = 'get-oid4vc-sessions', - OIDC_DELETE_CREDENTIAL_OFFER = 'delete-oid4vc-credential-offer' + OIDC_DELETE_CREDENTIAL_OFFER = 'delete-oid4vc-credential-offer', + + //X509 + X509_CREATE_CERTIFICATE = 'create-x509-certificate', + X509_IMPORT_CERTIFICATE = 'import-x509-certificate', + X509_DECODE_CERTIFICATE = 'decode-x509-certificate' } export const MICRO_SERVICE_NAME = Symbol('MICRO_SERVICE_NAME'); export const ATTRIBUTE_NAME_REGEX = /\['(.*?)'\]/; diff --git a/libs/common/src/common.utils.ts b/libs/common/src/common.utils.ts index 5091239fc..a41b9b72d 100644 --- a/libs/common/src/common.utils.ts +++ b/libs/common/src/common.utils.ts @@ -71,7 +71,6 @@ export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramI if (!agentEndPoint) { throw new NotFoundException(ResponseMessages.common.error.invalidEndpoint); } - const agentUrlMap: Map = new Map([ [String(CommonConstants.CONNECTION_INVITATION), String(CommonConstants.URL_CONN_INVITE)], [String(CommonConstants.LEGACY_INVITATION), String(CommonConstants.URL_CONN_LEGACY_INVITE)], @@ -106,7 +105,10 @@ export const getAgentUrl = async (agentEndPoint: string, urlFlag: string, paramI [String(CommonConstants.OIDC_ISSUER_SESSIONS_UPDATE_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET)], [String(CommonConstants.OIDC_ISSUER_SESSIONS_BY_ID), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET)], [String(CommonConstants.OIDC_ISSUER_SESSIONS), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], - [String(CommonConstants.OIDC_DELETE_CREDENTIAL_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)] + [String(CommonConstants.OIDC_DELETE_CREDENTIAL_OFFER), String(CommonConstants.URL_OIDC_ISSUER_SESSIONS_GET_ALL)], + [String(CommonConstants.X509_CREATE_CERTIFICATE), String(CommonConstants.URL_CREATE_X509_CERTIFICATE)], + [String(CommonConstants.X509_DECODE_CERTIFICATE), String(CommonConstants.URL_DECODE_X509_CERTIFICATE)], + [String(CommonConstants.X509_IMPORT_CERTIFICATE), String(CommonConstants.URL_IMPORT_X509_CERTIFICATE)] ]); const urlSuffix = agentUrlMap.get(urlFlag); diff --git a/libs/common/src/interfaces/x509.interface.ts b/libs/common/src/interfaces/x509.interface.ts new file mode 100644 index 000000000..17c350c22 --- /dev/null +++ b/libs/common/src/interfaces/x509.interface.ts @@ -0,0 +1,246 @@ +import { X509ExtendedKeyUsage, X509KeyUsage, x5cKeyType } from '@credebl/enum/enum'; + +// Enum remains the same +export enum GeneralNameType { + DNS = 'dns', + DN = 'dn', + EMAIL = 'email', + GUID = 'guid', + IP = 'ip', + URL = 'url', + UPN = 'upn', + REGISTERED_ID = 'id' +} + +export interface AuthorityAndSubjectKey { + // /** + // * @example "my-seed-12345" + // * @description Seed to deterministically derive the key (optional) + // */ + // seed?: string; + + // /** + // * @example "3yPQbnk6WwLgX8K3JZ4t7vBnJ8XqY2mMpRcD9fNvGtHw" + // * @description publicKeyBase58 for using existing key in wallet (optional) + // */ + // publicKeyBase58?: string; + + /** + * @example "p256" + * @description Type of the key used for signing the X.509 Certificate (default is p256) + */ + keyType?: x5cKeyType; +} + +export interface Name { + /** + * @example "dns" + */ + type: GeneralNameType; + + /** + * @example "example.com" + */ + value: string; +} + +export interface X509CertificateIssuerAndSubjectOptions { + /** + * @example "US" + */ + countryName?: string; + + /** + * @example "California" + */ + stateOrProvinceName?: string; + + /** + * @example "IT Department" + */ + organizationalUnit?: string; + + /** + * @example "Example Corporation" + */ + commonName?: string; +} + +export interface Validity { + /** + * @example "2024-01-01T00:00:00.000Z" + */ + notBefore?: Date; + + /** + * @example "2025-01-01T00:00:00.000Z" + */ + notAfter?: Date; +} + +export interface KeyUsage { + /** + * @example ["digitalSignature", "keyEncipherment", "crlSign"] + */ + usages: X509KeyUsage[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface ExtendedKeyUsage { + /** + * @example ["MdlDs", "ServerAuth", "ClientAuth"] + */ + usages: X509ExtendedKeyUsage[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface NameList { + /** + * @example [{ "type": "dns", "value": "example.com" }, { "type": "email", "value": "admin@example.com" }] + */ + name: Name[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface AuthorityAndSubjectKeyIdentifier { + /** + * @example true + */ + include: boolean; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface BasicConstraints { + /** + * @example false + */ + ca: boolean; + + /** + * @example 0 + */ + pathLenConstraint?: number; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface CrlDistributionPoints { + /** + * @example ["http://crl.example.com/ca.crl"] + */ + urls: string[]; + + /** + * @example true + */ + markAsCritical?: boolean; +} + +export interface X509CertificateExtensionsOptions { + keyUsage?: KeyUsage; + extendedKeyUsage?: ExtendedKeyUsage; + authorityKeyIdentifier?: AuthorityAndSubjectKeyIdentifier; + subjectKeyIdentifier?: AuthorityAndSubjectKeyIdentifier; + issuerAlternativeName?: NameList; + subjectAlternativeName?: NameList; + basicConstraints?: BasicConstraints; + crlDistributionPoints?: CrlDistributionPoints; +} + +export interface X509CreateCertificateOptions { + authorityKey?: AuthorityAndSubjectKey; + subjectPublicKey?: AuthorityAndSubjectKey; + + /** + * @example "1234567890" + */ + serialNumber?: string; + + /** + * @example { + * "countryName": "US", + * "stateOrProvinceName": "California", + * "commonName": "Example CA" + * } + * OR + * @example "/C=US/ST=California/O=Example Corporation/CN=Example CA" + */ + issuer: X509CertificateIssuerAndSubjectOptions | string; + + /** + * @example { + * "countryName": "US", + * "commonName": "www.example.com" + * } + * OR + * @example "/C=US/CN=www.example.com" + */ + subject?: X509CertificateIssuerAndSubjectOptions | string; + + validity?: Validity; + extensions?: X509CertificateExtensionsOptions; +} + +export interface X509CertificateRecord { + id: string; + orgAgentId: string; + keyType: string; + status: string; + validFrom: Date; + expiry: Date; + certificateBase64: string; + createdBy: string; + lastChangedBy: string; + createdAt: Date; + lastChangedDateTime: Date; +} + +export interface IX509SearchCriteria extends IPaginationSortingdto { + keyType: string; + status: string; +} + +export interface IPaginationSortingdto { + pageNumber: number; + pageSize: number; + sortField?: string; + sortBy?: string; + searchByText?: string; +} + +export interface IX509ImportCertificateOptionsDto { + /* + X.509 certificate in base64 string format + */ + certificate: string; + + /* + Private key in base64 string format + */ + privateKey?: string; + + keyType: KeyType; +} + +export interface x509CertificateDecodeDto { + certificate: string; +} diff --git a/libs/common/src/response-messages/index.ts b/libs/common/src/response-messages/index.ts index db06e2f41..9eb59b160 100644 --- a/libs/common/src/response-messages/index.ts +++ b/libs/common/src/response-messages/index.ts @@ -553,6 +553,28 @@ export const ResponseMessages = { deleteFailed: 'Failed to delete OID4VC credential offer.' } }, + x509: { + success: { + create: 'x509 certificate created successfully', + activated: 'x509 certificate activated successfully', + deActivated: 'x509 certificate deactivated successfully', + fetch: 'x509 certificate fetched successfully', + fetchAll: 'x509 certificates fetched successfully', + import: 'x509 certificate imported successfully' + }, + error: { + errorCreate: 'Error while creating x509 certificate.', + errorUpdateStatus: 'Error while updating x509 certificate.', + errorActivation: 'Failed to activate x509 certificate..', + agentEndPointNotFound: 'Agent details not found', + collision: 'Certificate date range collides with existing certificates for this organization', + collisionForActivatingX5c: + 'Certificate date range collides with existing certificates for this organization, In order to active this you need to Inactivate the previous one.', + notFound: 'x509 certificate record not found.', + import: 'Failed to import x509 certificate', + errorDecode: 'Error while decoding x509 certificate.' + } + }, nats: { success: {}, error: { diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index 787fc1f92..0a52130df 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -272,7 +272,7 @@ export enum ProviderType { SUPABASE = 'supabase' } -export declare enum OpenId4VcIssuanceSessionState { +export enum OpenId4VcIssuanceSessionState { OfferCreated = 'OfferCreated', OfferUriRetrieved = 'OfferUriRetrieved', AuthorizationInitiated = 'AuthorizationInitiated', @@ -284,3 +284,41 @@ export declare enum OpenId4VcIssuanceSessionState { Completed = 'Completed', Error = 'Error' } + +export enum x5cKeyType { + Ed25519 = 'ed25519', + P256 = 'p256' +} + +export enum x5cRecordStatus { + Active = 'Active', + PendingActivation = 'Pending activation', + InActive = 'In Active' +} + +export enum X509KeyUsage { + DigitalSignature = 1, + NonRepudiation = 2, + KeyEncipherment = 4, + DataEncipherment = 8, + KeyAgreement = 16, + KeyCertSign = 32, + CrlSign = 64, + EncipherOnly = 128, + DecipherOnly = 256 +} + +export enum X509ExtendedKeyUsage { + ServerAuth = '1.3.6.1.5.5.7.3.1', + ClientAuth = '1.3.6.1.5.5.7.3.2', + CodeSigning = '1.3.6.1.5.5.7.3.3', + EmailProtection = '1.3.6.1.5.5.7.3.4', + TimeStamping = '1.3.6.1.5.5.7.3.8', + OcspSigning = '1.3.6.1.5.5.7.3.9', + MdlDs = '1.0.18013.5.1.2' +} + +export enum CredentialFormat { + SdJwtVc = 'vc+sd-jwt', + Mdoc = 'mso_mdoc' +} diff --git a/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql b/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql new file mode 100644 index 000000000..21ce60a0f --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20251013125236_added_x509_certificate_table/migration.sql @@ -0,0 +1,58 @@ +/* + Warnings: + + - You are about to drop the column `supported_protocol` on the `ledgers` table. All the data in the column will be lost. + - You are about to drop the column `supported_protocol` on the `organisation` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "ledgers" DROP COLUMN "supported_protocol"; + +-- AlterTable +ALTER TABLE "organisation" DROP COLUMN "supported_protocol"; + +-- DropEnum +DROP TYPE "CredentialExchangeProtocol"; + +-- CreateTable +CREATE TABLE "oid4vc_credentials" ( + "id" UUID NOT NULL, + "orgId" UUID NOT NULL, + "offerId" TEXT NOT NULL, + "credentialOfferId" TEXT NOT NULL, + "state" TEXT NOT NULL, + "contextCorrelationId" TEXT NOT NULL, + "createdBy" UUID NOT NULL, + "createDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedDateTime" TIMESTAMPTZ(6) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "lastChangedBy" UUID NOT NULL, + + CONSTRAINT "oid4vc_credentials_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "x509_certificates" ( + "id" TEXT NOT NULL, + "orgAgentId" UUID NOT NULL, + "keyType" TEXT NOT NULL, + "status" TEXT NOT NULL, + "validFrom" TIMESTAMP(3) NOT NULL, + "expiry" TIMESTAMP(3) NOT NULL, + "certificateBase64" TEXT NOT NULL, + "isImported" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "createdBy" UUID NOT NULL, + "lastChangedDateTime" TIMESTAMP(3) NOT NULL, + "lastChangedBy" UUID NOT NULL, + + CONSTRAINT "x509_certificates_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "oid4vc_credentials_offerId_key" ON "oid4vc_credentials"("offerId"); + +-- AddForeignKey +ALTER TABLE "oid4vc_credentials" ADD CONSTRAINT "oid4vc_credentials_orgId_fkey" FOREIGN KEY ("orgId") REFERENCES "organisation"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "x509_certificates" ADD CONSTRAINT "x509_certificates_orgAgentId_fkey" FOREIGN KEY ("orgAgentId") REFERENCES "org_agents"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 54f3d7840..495acd350 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -231,6 +231,7 @@ model org_agents { webhookUrl String? @db.VarChar org_dids org_dids[] oidc_issuer oidc_issuer[] + x509_certificates x509_certificates[] } model org_dids { @@ -622,3 +623,24 @@ model credential_templates { signerOption SignerOption } +model x509_certificates { + id String @id @default(uuid()) + + orgAgentId String @db.Uuid + org_agents org_agents @relation(fields: [orgAgentId], references: [id]) + + keyType String // "p256", "ed25519" + status String //e.g "Active", "Pending activation", "InActive" + validFrom DateTime + + expiry DateTime + certificateBase64 String + isImported Boolean @default(false) + + createdAt DateTime @default(now()) + createdBy String @db.Uuid + lastChangedDateTime DateTime @updatedAt + lastChangedBy String @db.Uuid +} + + diff --git a/nest-cli.json b/nest-cli.json index 819afa8b7..82ff28c5d 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -295,6 +295,15 @@ "compilerOptions": { "tsConfigPath": "apps/oid4vc-issuance/tsconfig.app.json" } + }, + "x509": { + "type": "application", + "root": "apps/x509", + "entryFile": "main", + "sourceRoot": "apps/x509/src", + "compilerOptions": { + "tsConfigPath": "apps/x509/tsconfig.app.json" + } } } } \ No newline at end of file