Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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({
Expand Down
3 changes: 0 additions & 3 deletions apps/issuance/constant/issuance.ts
Original file line number Diff line number Diff line change
@@ -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;
5 changes: 4 additions & 1 deletion apps/oid4vc-issuance/constant/issuance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
8 changes: 4 additions & 4 deletions apps/oid4vc-issuance/interfaces/oid4vc-issuance.interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -87,7 +87,7 @@ export interface initialIssuerDetails {
}

export enum AccessTokenSignerKeyType {
ED25519 = 'ed25519'
ED25519 = 'Ed25519'
}

export interface IssuerUpdation {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export interface ISignerOption {
method: SignerMethodOption;
did?: string;
x5c?: string[];
keyId?: string;
}

export enum AuthenticationType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
if ('mso_mdoc' === normalized || 'mso-mdoc' === normalized || 'mdoc' === normalized) {
Expand Down Expand Up @@ -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);
Expand Down
147 changes: 59 additions & 88 deletions apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
Expand Down Expand Up @@ -44,19 +39,25 @@ 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 = {
credentialConfigurationsSupported: Record<string, CredentialConfig>;
};

// ---- 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',
Comment on lines +57 to +60
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

IANA COSE algorithm registry EdDSA RFC 8152 RFC 9052

💡 Result:

  • RFC status / relationships: RFC 8152 (original COSE spec) is obsoleted by RFC 9052 (COSE structures/process) together with RFC 9053 (initial COSE algorithms). RFC 9052’s IANA section describes updating the registries originally defined by RFC 8152 to point to the 9052/9053 document set. [2], [3]
  • Where “EdDSA” is defined now: The algorithm identifier registrations (including EdDSA) are in RFC 9053 (not RFC 9052). [3]
  • IANA “COSE Algorithms” registry (current):
    • EdDSA has Value = -8, and the registry marks it Deprecated (references include RFC 9053 and RFC 9864). [1]
    • A fully-specified replacement exists: Ed25519 has Value = -19, Recommended = Yes, with reference RFC 9864, Section 2.2. [1]

Sources: [1] IANA COSE registries page, [2] RFC 9052, [3] RFC 9053.


🌐 Web query:

COSE algorithm identifier -9 IANA registry value

💡 Result:

In the IANA COSE Algorithms registry, algorithm identifier -9 is:

  • Name: ESP256
  • Description: ECDSA using P-256 curve and SHA-256 [1]

Source: IANA “CBOR Object Signing and Encryption (COSE)” registry (COSE Algorithms table) [1]


🌐 Web query:

COSE algorithm -7 -8 IANA registry complete list

💡 Result:

From the IANA “COSE Algorithms” registry, the complete entries for algorithm values -7 and -8 are: [1]

Name Value Description Capabilities Change Controller Reference Recommended
ES256 -7 ECDSA w/ SHA-256 [kty] IETF RFC 9053, RFC 9864 Deprecated
EdDSA -8 EdDSA [kty] IETF RFC 9053, RFC 9864 Deprecated

Source
[1] IANA — CBOR Object Signing and Encryption (COSE) / COSE Algorithms (registry page; last updated 2026-01-16).


🏁 Script executed:

cat -n apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts | head -80

Repository: credebl/platform

Length of output: 3192


Change COSE algorithm identifier for EdDSA from -9 to -8 in the MDOC list.

According to the IANA COSE Algorithms registry (RFC 9053), EdDSA is assigned the identifier -8, not -9. The current code maps SDJWT's EdDSA to MDOC's -9, but should map it to -8 to match the standard registration. Line 58 should be:

const STATIC_CREDENTIAL_ALGS_FOR_MDOC = [-7, -8] as const;

This ensures the algorithm list correctly pairs ES256 (-7) and EdDSA (-8) for MDOC.

🤖 Prompt for AI Agents
In `@apps/oid4vc-issuance/libs/helpers/issuer.metadata.ts` around lines 57 - 60,
The MDOC COSE algorithm constant STATIC_CREDENTIAL_ALGS_FOR_MDOC currently uses
the wrong EdDSA identifier (-9); update the array to use the IANA-registered
EdDSA value (-8) so it reads [-7, -8] as the paired ES256/EdDSA mapping,
ensuring STATIC_CREDENTIAL_ALGS_FOR_MDOC matches the COSE registry.


// Safe coercion helpers
function coerceJsonObject<T>(v: Prisma.JsonValue): T | null {
Expand Down Expand Up @@ -206,98 +207,60 @@ 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<string, Claim> {
const claims: Record<string, Claim> = {};
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;
}

/**
* Builds claims object for both SD-JWT and MDOC credential templates.
*/
//TODO: Remove any type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function buildClaimsFromTemplate(template: SdJwtTemplate | MdocTemplate): Record<string, any> {
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<string, any> = {};

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
Expand All @@ -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: []
}
}
};
}
Expand All @@ -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]));
// }
Expand All @@ -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: []
}
}
};
}
Expand Down Expand Up @@ -387,7 +360,6 @@ export function buildCredentialConfigurationsSupported(templateRows: any): Recor
templateToBuild,
format === CredentialFormat.Mdoc ? CredentialFormat.Mdoc : CredentialFormat.SdJwtVc
);

const appearanceJson = coerceJsonObject<unknown>(templateRow.appearance);

// Prepare the display configuration
Expand All @@ -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
}
Loading