diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts index d3231f10f..1bbb6de32 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer-template.dto.ts @@ -171,8 +171,8 @@ export class MdocNamespaceDto { } export class MdocTemplateDto { @ApiProperty({ - description: 'Document type (required when format is "mso_mdoc"; must NOT be provided when format is "vc+sd-jwt")', - example: 'org.iso.23220.photoID.1' + description: 'Document type (required when format is "mso_mdoc"; must NOT be provided when format is "dc+sd-jwt")', + example: 'org.iso.18013.5.1.mDL' }) //@ValidateIf((o: CreateCredentialTemplateDto) => 'mso_mdoc' === o.format) @IsString() @@ -189,7 +189,7 @@ export class MdocTemplateDto { export class SdJwtTemplateDto { @ApiProperty({ description: - 'Verifiable Credential Type (required when format is "vc+sd-jwt"; must NOT be provided when format is "mso_mdoc")', + 'Verifiable Credential Type (required when format is "dc+sd-jwt"; must NOT be provided when format is "mso_mdoc")', example: 'BirthCertificateCredential-sdjwt' }) // @ValidateIf((o: CreateCredentialTemplateDto) => 'vc+sd-jwt' === o.format) @@ -230,7 +230,7 @@ export class CreateCredentialTemplateDto { format: CredentialFormat; @ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.SdJwtVc === o.format) - @IsEmpty({ message: 'doctype must not be provided when format is "vc+sd-jwt"' }) + @IsEmpty({ message: 'doctype must not be provided when format is "dc+sd-jwt"' }) readonly _doctypeAbsentGuard?: unknown; @ValidateIf((o: CreateCredentialTemplateDto) => CredentialFormat.Mdoc === o.format) diff --git a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts index 2675d7514..74eafc674 100644 --- a/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts +++ b/apps/api-gateway/src/oid4vc-issuance/dtos/oid4vc-issuer.dto.ts @@ -1,6 +1,6 @@ /* eslint-disable camelcase */ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsString, IsOptional, IsArray, ValidateNested, IsUrl, IsInt } from 'class-validator'; +import { IsString, IsOptional, IsArray, ValidateNested, IsUrl, IsInt, Min } from 'class-validator'; import { Type } from 'class-transformer'; export class LogoDto { @@ -96,6 +96,7 @@ export class IssuerCreationDto { }) @IsOptional() @IsInt({ message: 'batchCredentialIssuanceSize must be an integer' }) + @Min(1, { message: 'batchCredentialIssuanceSize must be greater than 0' }) batchCredentialIssuanceSize?: number; @ApiProperty({ diff --git a/apps/issuance/constant/issuance.ts b/apps/issuance/constant/issuance.ts index d32498997..fac68e2ce 100644 --- a/apps/issuance/constant/issuance.ts +++ b/apps/issuance/constant/issuance.ts @@ -1,6 +1,3 @@ -import { AccessTokenSignerKeyType } from '../interfaces/oid4vc-issuance.interfaces'; - export const dpopSigningAlgValuesSupported = ['RS256', 'ES256', 'EdDSA']; export const credentialConfigurationsSupported = {}; -export const accessTokenSignerKeyType = 'ed25519' as AccessTokenSignerKeyType; export const batchCredentialIssuanceDefault = 0; diff --git a/apps/oid4vc-issuance/constant/issuance.ts b/apps/oid4vc-issuance/constant/issuance.ts index d32498997..c1adf5aa2 100644 --- a/apps/oid4vc-issuance/constant/issuance.ts +++ b/apps/oid4vc-issuance/constant/issuance.ts @@ -2,5 +2,8 @@ import { AccessTokenSignerKeyType } from '../interfaces/oid4vc-issuance.interfac export const dpopSigningAlgValuesSupported = ['RS256', 'ES256', 'EdDSA']; export const credentialConfigurationsSupported = {}; -export const accessTokenSignerKeyType = 'ed25519' as AccessTokenSignerKeyType; +export const accessTokenSignerKeyType = { kty: 'OKP', crv: 'Ed25519' } as { + kty: string; + crv: AccessTokenSignerKeyType; +}; export const batchCredentialIssuanceDefault = 0; diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts index e3ba6413e..be9613ed6 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts @@ -36,7 +36,7 @@ export interface CredentialConfiguration { doctype?: string; scope: string; claims: Claim[]; - credential_signing_alg_values_supported: string[]; + credential_signing_alg_values_supported: string[] | number[]; cryptographic_binding_methods_supported: string[]; display: Display[]; } @@ -54,7 +54,7 @@ export interface AuthorizationServerConfig { export interface IssuerCreation { authorizationServerUrl: string; issuerId: string; - accessTokenSignerKeyType?: AccessTokenSignerKeyType; + accessTokenSignerKeyType?: { kty: string; crv: string }; display: Display[]; dpopSigningAlgValuesSupported?: string[]; authorizationServerConfigs: AuthorizationServerConfig; @@ -67,7 +67,7 @@ export interface IssuerInitialConfig { display: Display[] | {}; // eslint-disable-next-line @typescript-eslint/ban-types authorizationServerConfigs: AuthorizationServerConfig | {}; - accessTokenSignerKeyType: AccessTokenSignerKeyType; + accessTokenSignerKeyType: { kty: string; crv: string }; dpopSigningAlgValuesSupported: string[]; batchCredentialIssuance?: object; credentialConfigurationsSupported: object; @@ -87,7 +87,7 @@ export interface initialIssuerDetails { } export enum AccessTokenSignerKeyType { - ED25519 = 'ed25519' + ED25519 = 'Ed25519' } export interface IssuerUpdation { diff --git a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts index cfc6f3a3d..e463eda2b 100644 --- a/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts +++ b/apps/oid4vc-issuance/interfaces/oid4vc-issuer-sessions.interfaces.ts @@ -17,6 +17,7 @@ export interface ISignerOption { method: SignerMethodOption; did?: string; x5c?: string[]; + keyId?: string; } export enum AuthenticationType { diff --git a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts index dac66df5e..a2b8b208d 100644 --- a/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts +++ b/apps/oid4vc-issuance/libs/helpers/credential-sessions.builder.ts @@ -135,7 +135,7 @@ export const DEFAULT_TXCODE = { /** Map DB format string -> API enum */ function mapDbFormatToApiFormat(dbFormat: string): CredentialFormat { const normalized = (dbFormat ?? '').toLowerCase(); - if (['sd-jwt', 'vc+sd-jwt', 'sdjwt', 'sd+jwt-vc'].includes(normalized)) { + if (['sd-jwt', 'dc+sd-jwt', 'vc+sd-jwt', 'sdjwt', 'sd+jwt-vc'].includes(normalized)) { return CredentialFormat.SdJwtVc; } if ('mso_mdoc' === normalized || 'mso-mdoc' === normalized || 'mdoc' === normalized) { @@ -459,7 +459,7 @@ export function buildCredentialOfferPayload( throw new UnprocessableEntityException(`${validationError.errors.join(', ')}`); } - const templateFormat = (templateRecord as any).format ?? 'vc+sd-jwt'; + const templateFormat = (templateRecord as any).format ?? 'dc+sd-jwt'; const apiFormat = mapDbFormatToApiFormat(templateFormat); if (apiFormat === CredentialFormat.SdJwtVc) { return buildSdJwtCredential(credentialRequest, templateRecord, signerOptions, activeCertificateDetails); diff --git a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts index 96f5282c0..f5a3426ad 100644 --- a/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts +++ b/apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts @@ -3,12 +3,7 @@ import { oidc_issuer, Prisma } from '@prisma/client'; import { batchCredentialIssuanceDefault } from '../../constant/issuance'; import { CreateOidcCredentialOffer } from '../../interfaces/oid4vc-issuer-sessions.interfaces'; import { IssuerResponse } from 'apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces'; -import { - Claim, - CredentialAttribute, - MdocTemplate, - SdJwtTemplate -} from 'apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces'; +import { Claim, MdocTemplate, SdJwtTemplate } from 'apps/oid4vc-issuance/interfaces/oid4vc-template.interfaces'; import { CredentialFormat } from '@credebl/enum/enum'; type AttributeDisplay = { name: string; locale: string }; @@ -44,10 +39,14 @@ type CredentialConfig = { vct?: string; scope: string; doctype?: string; - claims: Claim[]; - credential_signing_alg_values_supported: string[]; + + credential_signing_alg_values_supported: string[] | number[]; cryptographic_binding_methods_supported: string[]; - display: { name: string; description?: string; locale?: string }[]; + + credential_metadata: { + claims: Claim[]; + display: CredentialDisplayItem[]; + }; }; type CredentialConfigurationsSupported = { @@ -55,8 +54,10 @@ type CredentialConfigurationsSupported = { }; // ---- Static Lists (as requested) ---- -const STATIC_CREDENTIAL_ALGS = ['ES256', 'EdDSA'] as const; -const STATIC_BINDING_METHODS = ['did:key'] as const; +const STATIC_CREDENTIAL_ALGS_FOR_SDJWT = ['ES256', 'EdDSA'] as const; +const STATIC_CREDENTIAL_ALGS_FOR_MDOC = [-7, -9] as const; +const STATIC_BINDING_METHODS_FOR_SDJWT = ['did:key', 'did:web', 'did:jwk', 'jwk'] as const; +const STATIC_BINDING_METHODS_FOR_MDOC = ['cose_key'] as const; // We need to test 'did:key', 'did:web', 'did:jwk', 'jwk', // Safe coercion helpers function coerceJsonObject(v: Prisma.JsonValue): T | null { @@ -206,72 +207,35 @@ export function encodeIssuerPublicId(publicIssuerId: string): string { return encodeURIComponent(publicIssuerId.trim()); } -///--------------------------------------------------------- - -// function buildClaimsFromAttributesWithPath(attributes: CredentialAttribute[], parentPath: string[] = []): Claim[] { -// const claims: Claim[] = []; - -// for (const attr of attributes) { -// const currentPath = [...parentPath, attr.key]; - -// // 1️⃣ Add the parent attribute itself if it has display or mandatory metadata -// if ((attr.display && 0 < attr.display.length) || attr.mandatory) { -// const parentClaim: Claim = { path: currentPath }; - -// if (attr.display?.length) { -// parentClaim.display = attr.display.map((d) => ({ -// name: d.name, -// locale: d.locale -// })); -// } - -// if (attr.mandatory) { -// parentClaim.mandatory = true; -// } +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function generateClaims(attributes: any[], namespace?: string, parentPath: string[] = []): Claim[] { + const result: Claim[] = []; -// claims.push(parentClaim); -// } + for (const attr of attributes) { + const currentPath = [...parentPath, attr.key]; -// // 2️⃣ If this attribute has nested children, recurse into them -// if (attr.children && 0 < attr.children.length) { -// claims.push(...buildClaimsFromAttributes(attr.children, currentPath)); -// } -// } -// return claims; -// } + const path = namespace ? [namespace, ...currentPath] : currentPath; -/** - * Recursively builds a nested claims object from a list of attributes. - */ -function buildNestedClaims(attributes: CredentialAttribute[]): Record { - const claims: Record = {}; + const claim: Claim = { path }; - for (const attr of attributes) { - const node: Claim = {}; - - // ✅ include display info - if (attr.display?.length) { - node.display = attr.display.map((d) => ({ - name: d.name, - locale: d.locale - })); + if (attr.display) { + claim.display = attr.display; } - // ✅ include mandatory flag - if (attr.mandatory) { - node.mandatory = true; + if (true === attr.mandatory) { + claim.mandatory = true; } - // ✅ handle nested children recursively + // Always push the claim (even if it only has path) + result.push(claim); + + // Handle nested children if (attr.children?.length) { - const childClaims = buildNestedClaims(attr.children); - Object.assign(node, childClaims); // merge children into current node + result.push(...generateClaims(attr.children, namespace, currentPath)); } - - claims[attr.key] = node; } - return claims; + return result; } /** @@ -279,25 +243,24 @@ function buildNestedClaims(attributes: CredentialAttribute[]): Record { +function buildClaimsFromTemplate(template: SdJwtTemplate | MdocTemplate): Claim[] { // ✅ MDOC case — handle namespaces if ((template as MdocTemplate).namespaces) { const mdocTemplate = template as MdocTemplate; - //TODO: Remove any type // eslint-disable-next-line @typescript-eslint/no-explicit-any - const claims: Record = {}; - + const claims: Claim[] = []; for (const ns of mdocTemplate.namespaces) { - claims[ns.namespace] = buildNestedClaims(ns.attributes); - } + const generated = generateClaims(ns.attributes, ns.namespace); + claims.push(...generated); + } return claims; } - // ✅ SD-JWT case — flat attributes const sdjwtTemplate = template as SdJwtTemplate; - return buildNestedClaims(sdjwtTemplate.attributes); + const claims = generateClaims(sdjwtTemplate.attributes); + return claims; } //TODO: Fix this eslint issue @@ -316,14 +279,17 @@ export function buildSdJwtCredentialConfig(name: string, template: SdJwtTemplate format: CredentialFormat.SdJwtVc, scope: credentialScope, vct: template.vct, - credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], - cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], - // proof_types_supported: { - // jwt: { - // proof_signing_alg_values_supported: ['ES256'] - // } - // }, - claims + credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS_FOR_SDJWT], + cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS_FOR_SDJWT], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: ['ES256', 'EdDSA'] + } + }, + credential_metadata: { + claims, + display: [] + } } }; } @@ -339,7 +305,6 @@ export function buildMdocCredentialConfig(name: string, template: MdocTemplate) const credentialScope = `openid4vc:${template.doctype}-${formatSuffix}`; const claims = buildClaimsFromTemplate(template); - // for (const ns of template.namespaces) { // claims.push(...buildClaimsFromAttributes(ns.attributes, [ns.namespace])); // } @@ -349,9 +314,17 @@ export function buildMdocCredentialConfig(name: string, template: MdocTemplate) format: CredentialFormat.Mdoc, scope: credentialScope, doctype: template.doctype, - credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS], - cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS], - claims + credential_signing_alg_values_supported: [...STATIC_CREDENTIAL_ALGS_FOR_MDOC], + cryptographic_binding_methods_supported: [...STATIC_BINDING_METHODS_FOR_MDOC], + proof_types_supported: { + jwt: { + proof_signing_alg_values_supported: ['ES256', 'EdDSA'] + } + }, + credential_metadata: { + claims, + display: [] + } } }; } @@ -387,7 +360,6 @@ export function buildCredentialConfigurationsSupported(templateRows: any): Recor templateToBuild, format === CredentialFormat.Mdoc ? CredentialFormat.Mdoc : CredentialFormat.SdJwtVc ); - const appearanceJson = coerceJsonObject(templateRow.appearance); // Prepare the display configuration @@ -406,12 +378,11 @@ export function buildCredentialConfigurationsSupported(templateRows: any): Recor // eslint-disable-next-line prefer-destructuring const dynamicKey = Object.keys(credentialConfig)[0]; - Object.assign(credentialConfig[dynamicKey], { + Object.assign(credentialConfig[dynamicKey].credential_metadata, { display: displayConfigurations }); Object.assign(credentialConfigMap, credentialConfig); } - return credentialConfigMap; // ✅ Return flat map, not nested object } diff --git a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts index bc78d9620..9f4427fce 100644 --- a/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts +++ b/apps/oid4vc-issuance/src/oid4vc-issuance.service.ts @@ -546,9 +546,15 @@ export class Oid4vcIssuanceService { if (!activeCertificate) { throw new NotFoundException('No active certificate(p256) found for issuer'); } + if (!activeCertificate.keyId) { + throw new BadRequestException( + 'Active certificate is missing keyId; re-import or re-issue the certificate.' + ); + } signerOptions.push({ method: SignerMethodOption.X5C, - x5c: [activeCertificate.certificateBase64] + x5c: [activeCertificate.certificateBase64], + keyId: activeCertificate.keyId }); activeCertificateDetails.push(activeCertificate); } @@ -564,7 +570,8 @@ export class Oid4vcIssuanceService { } signerOptions.push({ method: SignerMethodOption.X5C, - x5c: [activeCertificate.certificateBase64] + x5c: [activeCertificate.certificateBase64], + keyId: activeCertificate.keyId }); activeCertificateDetails.push(activeCertificate); } @@ -754,7 +761,6 @@ export class Oid4vcIssuanceService { const templates = await this.oid4vcIssuanceRepository.getTemplatesByIssuerId(issuerId); const credentialConfigurationsSupported = buildCredentialConfigurationsSupported(templates); - return buildIssuerPayload({ credentialConfigurationsSupported }, issuerDetails); } catch (error) { this.logger.error(`[buildOidcIssuerPayload] - error: ${JSON.stringify(error)}`); diff --git a/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts b/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts index b6db519a3..381c96470 100644 --- a/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts +++ b/apps/oid4vc-verification/interfaces/oid4vp-verification-sessions.interfaces.ts @@ -18,6 +18,7 @@ export interface DidSigner { export interface X5cSigner { method: SignerMethodOption.X5C; x5c: string[]; + keyId: string; } export type RequestSigner = DidSigner | X5cSigner; diff --git a/apps/oid4vc-verification/src/oid4vc-verification.service.ts b/apps/oid4vc-verification/src/oid4vc-verification.service.ts index 87f8ca802..ff6a83df4 100644 --- a/apps/oid4vc-verification/src/oid4vc-verification.service.ts +++ b/apps/oid4vc-verification/src/oid4vc-verification.service.ts @@ -213,7 +213,6 @@ export class Oid4vpVerificationService extends BaseService { `[oid4vpCreateVerificationSession] called for orgId=${orgId}, verifierId=${verifierId}, user=${userDetails?.id ?? 'unknown'}` ); try { - const activeCertificateDetails: X509CertificateRecord[] = []; const agentDetails = await this.oid4vpRepository.getAgentEndPoint(orgId); if (!agentDetails) { throw new NotFoundException(ResponseMessages.issuance.error.agentEndPointNotFound); @@ -234,16 +233,28 @@ export class Oid4vpVerificationService extends BaseService { method: SignerMethodOption.DID, didUrl: orgDid }; - } else if ( - sessionRequest.requestSigner.method === SignerOption.X509_P256 || - sessionRequest.requestSigner.method === SignerOption.X509_ED25519 - ) { + } else if (sessionRequest.requestSigner.method === SignerOption.X509_P256) { this.logger.debug('X5C based request signer method selected'); - const activeCertificate = await this.oid4vpRepository.getCurrentActiveCertificate( - orgId, - sessionRequest.requestSigner.method - ); + const activeCertificate = await this.oid4vpRepository.getCurrentActiveCertificate(orgId, x5cKeyType.P256); + this.logger.debug(`activeCertificate=${JSON.stringify(activeCertificate)}`); + + if (!activeCertificate) { + throw new NotFoundException(`No active certificate(${sessionRequest.requestSigner.method}) found for issuer`); + } + if (!activeCertificate.keyId) { + throw new NotFoundException(`Active certificate keyId missing for ${sessionRequest.requestSigner.method}`); + } + + requestSigner = { + method: SignerMethodOption.X5C, // "x5c" + x5c: [activeCertificate.certificateBase64], // array with PEM/DER base64 + keyId: activeCertificate.keyId + }; + } else if (sessionRequest.requestSigner.method === SignerOption.X509_ED25519) { + this.logger.debug('X5C based request signer method selected'); + + const activeCertificate = await this.oid4vpRepository.getCurrentActiveCertificate(orgId, x5cKeyType.Ed25519); this.logger.debug(`activeCertificate=${JSON.stringify(activeCertificate)}`); if (!activeCertificate) { @@ -251,17 +262,17 @@ export class Oid4vpVerificationService extends BaseService { `No active certificate(${sessionRequest.requestSigner.method}}) found for issuer` ); } - + if (!activeCertificate.keyId) { + throw new NotFoundException(`Active certificate keyId missing for ${sessionRequest.requestSigner.method}`); + } requestSigner = { method: SignerMethodOption.X5C, // "x5c" - x5c: [activeCertificate.certificateBase64] // array with PEM/DER base64 + x5c: [activeCertificate.certificateBase64], // array with PEM/DER base64 + keyId: activeCertificate.keyId }; - - activeCertificateDetails.push(activeCertificate); } else { throw new BadRequestException(`Unsupported requestSigner method: ${sessionRequest.requestSigner.method}`); } - // assign the single object (not an array) sessionRequest.requestSigner = requestSigner; @@ -354,17 +365,20 @@ export class Oid4vpVerificationService extends BaseService { if (!activeCertificate) { throw new NotFoundException(`No active certificate(${signerOption}) found for organization`); } + if (!activeCertificate.keyId) { + throw new NotFoundException(`Active certificate keyId missing for ${signerOption}`); + } resolvedSigner = { method: SignerMethodOption.X5C, - x5c: [activeCertificate.certificateBase64] + x5c: [activeCertificate.certificateBase64], + keyId: activeCertificate.keyId }; } else { throw new BadRequestException(`Unsupported requestSigner method: ${signerOption}`); } sessionRequest.requestSigner = resolvedSigner; - const url = getAgentUrl(agentEndPoint, CommonConstants.OID4VP_VERIFICATION_SESSION); this.logger.debug(`[createIntentBasedVerificationPresentation] calling agent URL=${url}`); diff --git a/apps/x509/src/interfaces/x509.interface.ts b/apps/x509/src/interfaces/x509.interface.ts index bfae58ad9..cb7c444fd 100644 --- a/apps/x509/src/interfaces/x509.interface.ts +++ b/apps/x509/src/interfaces/x509.interface.ts @@ -10,6 +10,7 @@ export interface CreateX509CertificateEntity { certificateBase64: string; createdBy: string; lastChangedBy: string; + keyId?: string; } export interface UpdateCertificateStatusDto { diff --git a/apps/x509/src/repositories/x509.repository.ts b/apps/x509/src/repositories/x509.repository.ts index 037c2d4f0..78226a263 100644 --- a/apps/x509/src/repositories/x509.repository.ts +++ b/apps/x509/src/repositories/x509.repository.ts @@ -57,7 +57,8 @@ export class X509CertificateRepository { expiry: createDto.expiry, certificateBase64: createDto.certificateBase64, createdBy: createDto.createdBy, - lastChangedBy: createDto.lastChangedBy + lastChangedBy: createDto.lastChangedBy, + keyId: createDto.keyId } }); diff --git a/apps/x509/src/x509.service.ts b/apps/x509/src/x509.service.ts index 0c3f1bd52..81c1dec06 100644 --- a/apps/x509/src/x509.service.ts +++ b/apps/x509/src/x509.service.ts @@ -89,7 +89,8 @@ export class X509CertificateService extends BaseService { validFrom: options.validity.notBefore, expiry: options.validity.notAfter, createdBy: user.id, - lastChangedBy: user.id + lastChangedBy: user.id, + keyId: certificate.response.keyId }; return await this.x509CertificateRepository.create(createDto); @@ -142,6 +143,29 @@ export class X509CertificateService extends BaseService { throw new NotFoundException(ResponseMessages.x509.error.notFound); } + private parseAsn1Date( + value: { utcTime?: string; generalTime?: string } | undefined, + fieldName: 'notBefore' | 'notAfter' + ): Date { + if (!value) { + throw new InternalServerErrorException(`Certificate validity.${fieldName} is missing`); + } + + const dateValue = value.utcTime ?? value.generalTime; + + if (!dateValue) { + throw new InternalServerErrorException(`Certificate validity.${fieldName} has neither utcTime nor generalTime`); + } + + const date = new Date(dateValue); + + if (!isFinite(date.getTime())) { + throw new InternalServerErrorException(`Invalid ${fieldName} date in certificate: ${dateValue}`); + } + + return date; + } + async importCertificate(payload: { orgId: string; options: IX509ImportCertificateOptionsDto; @@ -161,24 +185,26 @@ export class X509CertificateService extends BaseService { this.logger.log(`Decoded certificate`); this.logger.debug(`certificate data:`, JSON.stringify(decodedResult)); - const { publicKey } = decodedResult.response; + const { publicJwk } = decodedResult.response; const decodedCert = decodedResult.response.x509Certificate; this.logger.log(`Start validating certificate`); - const isValidKeyType = Object.values(x5cKeyType).includes(publicKey.keyType as x5cKeyType); - + const crv = publicJwk?.jwk?.jwk?.crv; + const isValidKeyType = Object.values(x5cKeyType).includes(crv 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 validity = decodedCert?.asn?.tbsCertificate?.validity; + + const validFrom = this.parseAsn1Date(validity?.notBefore, 'notBefore'); + const expiry = this.parseAsn1Date(validity?.notAfter, 'notAfter'); const certificateDateCheckDto: CertificateDateCheckDto = { orgId, validFrom, expiry, - keyType: publicKey.keyType, + keyType: crv, status: x5cRecordStatus.Active }; const collisionForActiveRecords = await this.x509CertificateRepository.hasDateCollision(certificateDateCheckDto); @@ -208,13 +234,14 @@ export class X509CertificateService extends BaseService { this.logger.log(`Successfully imported certificate in wallet `); const createDto: CreateX509CertificateEntity = { orgId, - certificateBase64: certificate.response.issuerCertficicate, - keyType: publicKey.keyType, + certificateBase64: certificate.response.issuerCertificate, + keyType: crv, status: certStatus, validFrom, expiry, createdBy: user.id, - lastChangedBy: user.id + lastChangedBy: user.id, + keyId: certificate.response.keyId }; this.logger.log(`Now adding certificate in platform for org : ${orgId} `); diff --git a/libs/common/src/interfaces/x509.interface.ts b/libs/common/src/interfaces/x509.interface.ts index 01e9cd2f7..df2015a88 100644 --- a/libs/common/src/interfaces/x509.interface.ts +++ b/libs/common/src/interfaces/x509.interface.ts @@ -213,6 +213,7 @@ export interface X509CertificateRecord { lastChangedBy: string; createdAt: Date; lastChangedDateTime: Date; + keyId?: string; } export interface IX509SearchCriteria extends IPaginationSortingDto { diff --git a/libs/enum/src/enum.ts b/libs/enum/src/enum.ts index f05d64e0e..329a360e3 100644 --- a/libs/enum/src/enum.ts +++ b/libs/enum/src/enum.ts @@ -315,8 +315,8 @@ export enum OpenId4VcIssuanceSessionState { } export enum x5cKeyType { - Ed25519 = 'ed25519', - P256 = 'p256' + Ed25519 = 'Ed25519', + P256 = 'P-256' } export enum x5cRecordStatus { @@ -348,7 +348,7 @@ export enum X509ExtendedKeyUsage { } export enum CredentialFormat { - SdJwtVc = 'vc+sd-jwt', + SdJwtVc = 'dc+sd-jwt', Mdoc = 'mso_mdoc' } diff --git a/libs/prisma-service/prisma/migrations/20260119085820_added_key_id_in_x509_certificates_table/migration.sql b/libs/prisma-service/prisma/migrations/20260119085820_added_key_id_in_x509_certificates_table/migration.sql new file mode 100644 index 000000000..c6bb4f500 --- /dev/null +++ b/libs/prisma-service/prisma/migrations/20260119085820_added_key_id_in_x509_certificates_table/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "x509_certificates" ADD COLUMN "keyId" TEXT; diff --git a/libs/prisma-service/prisma/schema.prisma b/libs/prisma-service/prisma/schema.prisma index 28f1054e5..334a2774e 100644 --- a/libs/prisma-service/prisma/schema.prisma +++ b/libs/prisma-service/prisma/schema.prisma @@ -666,6 +666,7 @@ model x509_certificates { expiry DateTime certificateBase64 String isImported Boolean @default(false) + keyId String? createdAt DateTime @default(now()) createdBy String @db.Uuid