src/verifier/presentations/entities/presentation-config.entity.ts

Extends

TrustedAuthorityQuery

Relationships

Used by

No results matching.

Index

Properties

Properties

type
Type : TrustedAuthorityType.ETSI_TL
Default value : TrustedAuthorityType.ETSI_TL
Decorators :
@ApiProperty({enum: undefined, default: undefined})
@IsString()
@Equals(TrustedAuthorityType.ETSI_TL)
Inherited from TrustedAuthorityQuery
values
Type : TrustListRef[]
Decorators :
@ApiProperty({type: 'array', items: undefined})
@IsArray()
@ValidateNested({each: true})
@Type(undefined)
import {
    ApiExtraModels,
    ApiHideProperty,
    ApiProperty,
    ApiPropertyOptional,
    getSchemaPath,
} from "@nestjs/swagger";
import { Type } from "class-transformer";
import {
    Equals,
    IsDefined,
    IsArray,
    IsBoolean,
    IsEnum,
    IsNotEmpty,
    IsNumber,
    IsObject,
    IsOptional,
    IsString,
    Matches,
    Min,
    ValidateIf,
    Validate,
    ValidateNested,
    ValidationArguments,
    ValidatorConstraint,
    ValidatorConstraintInterface,
} from "class-validator";
import { JWK } from "jose";
import {
    Column,
    CreateDateColumn,
    Entity,
    JoinColumn,
    ManyToOne,
    UpdateDateColumn,
} from "typeorm";
import { TenantEntity } from "../../../auth/tenant/entitites/tenant.entity";
import { WebhookEndpointEntity } from "../../../issuer/configuration/webhook-endpoint/entities/webhook-endpoint.entity";
import { RevocationCheckMode } from "../../../shared/trust/types";
import { RegistrationCertificateRequest } from "../dto/vp-request.dto";
import { IsTransactionData } from "../validators/transaction-data.validator";

export enum TrustedAuthorityType {
    ETSI_TL = "etsi_tl",
    OPENID_FEDERATION = "openid_federation",
}

const DCQL_CREDENTIAL_FORMATS = ["dc+sd-jwt", "mso_mdoc"] as const;

export class TrustListRef {
    @ApiPropertyOptional({
        type: "string",
        description:
            "Managed local trust-list identifier. When provided, verifier material is resolved server-side from the trust list key chain.",
    })
    @IsOptional()
    @IsString()
    trustListId?: string;

    @ApiPropertyOptional({
        type: "string",
        description:
            "Trust-list JWT URL. Required for external trust lists when trustListId is not set.",
    })
    @ValidateIf((o: TrustListRef) => !o.trustListId)
    @IsDefined()
    @IsString()
    url!: string;

    @ApiPropertyOptional({
        type: "object",
        additionalProperties: true,
        description:
            "JWK used to verify trust-list JWT signatures for external trusted authority values.",
    })
    @ValidateIf((o: TrustListRef) => !o.trustListId && !o.verifierX509Der)
    @IsOptional()
    @IsObject()
    verifierKey?: JWK;

    @ApiPropertyOptional({
        type: "string",
        description:
            "Base64 DER-encoded X.509 certificate used to verify trust-list JWT signatures for external trusted authority values.",
    })
    @ValidateIf((o: TrustListRef) => !o.trustListId && !o.verifierKey)
    @IsOptional()
    @IsString()
    verifierX509Der?: string;
}

/**
 * Attached attestations
 */
export class PresentationAttachment {
    @IsString()
    format!: string;

    @IsNotEmpty()
    data!: any;

    @IsOptional()
    @IsString({ each: true })
    credential_ids?: string[];
}

export class ClaimsQuery {
    @IsString()
    @IsOptional()
    id?: string;

    @IsArray()
    path!: string[];

    @IsArray()
    @IsOptional()
    values?: string[];
}

export class MsoMdocClaimsQuery extends ClaimsQuery {
    @ApiPropertyOptional({
        type: "boolean",
        description:
            "Whether the holder should be allowed to retain the claim in an mso_mdoc response.",
    })
    @IsOptional()
    @IsBoolean()
    intent_to_retain?: boolean;
}

export class DcSdJwtCredentialQueryMeta {
    @ApiProperty({
        type: "array",
        items: { type: "string" },
        description: "VCT identifiers accepted for dc+sd-jwt credentials.",
    })
    @IsArray()
    @IsString({ each: true })
    vct_values!: string[];
}

export class MsoMdocCredentialQueryMeta {
    @ApiProperty({
        type: "string",
        description:
            "Document type identifier accepted for mso_mdoc credentials.",
    })
    @IsString()
    doctype_value!: string;
}

// TODO: extend: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-trusted-authorities-query
abstract class TrustedAuthorityQuery {
    declare type: TrustedAuthorityType;
}

export class TrustedAuthorityQueryEtsiTl extends TrustedAuthorityQuery {
    @ApiProperty({
        enum: [TrustedAuthorityType.ETSI_TL],
        default: TrustedAuthorityType.ETSI_TL,
    })
    @IsString()
    @Equals(TrustedAuthorityType.ETSI_TL)
    type: TrustedAuthorityType.ETSI_TL = TrustedAuthorityType.ETSI_TL;

    @ApiProperty({
        type: "array",
        items: {
            $ref: getSchemaPath(TrustListRef),
        },
    })
    @IsArray()
    @ValidateNested({ each: true })
    @Type(() => TrustListRef)
    values!: TrustListRef[];
}

export class TrustedAuthorityQueryOpenIdFederation extends TrustedAuthorityQuery {
    @ApiProperty({
        enum: [TrustedAuthorityType.OPENID_FEDERATION],
        default: TrustedAuthorityType.OPENID_FEDERATION,
    })
    @IsString()
    @Equals(TrustedAuthorityType.OPENID_FEDERATION)
    type: TrustedAuthorityType.OPENID_FEDERATION =
        TrustedAuthorityType.OPENID_FEDERATION;

    @ApiProperty({
        type: "array",
        items: { type: "string" },
    })
    @IsArray()
    @IsString({ each: true })
    values!: string[];
}

export type TrustedAuthorityQueryValue =
    | TrustedAuthorityQueryEtsiTl
    | TrustedAuthorityQueryOpenIdFederation;

export class CredentialSetQuery {
    @ApiProperty({
        type: "array",
        items: { type: "array", items: { type: "string" } },
    })
    @IsArray()
    options!: string[][];

    @IsBoolean()
    @IsOptional()
    required?: boolean;
}

@ValidatorConstraint({ name: "claimSetsConsistency", async: false })
class ClaimSetsConsistencyConstraint implements ValidatorConstraintInterface {
    validate(claimSets: string[][] | undefined, args: ValidationArguments) {
        if (!claimSets || claimSets.length === 0) {
            return true;
        }

        const credentialQuery = args.object as { claims?: ClaimsQuery[] };
        const claims = credentialQuery.claims;
        if (!claims || claims.length === 0) {
            return false;
        }

        const claimIds = claims.map((claim) => claim.id);
        if (claimIds.some((id) => typeof id !== "string" || id.trim() === "")) {
            return false;
        }

        if (new Set(claimIds).size !== claimIds.length) {
            return false;
        }

        const claimIdSet = new Set(claimIds);
        return claimSets.every(
            (claimSet) =>
                Array.isArray(claimSet) &&
                claimSet.length > 0 &&
                new Set(claimSet).size === claimSet.length &&
                claimSet.every(
                    (claimId) =>
                        typeof claimId === "string" && claimIdSet.has(claimId),
                ),
        );
    }

    defaultMessage() {
        return "claim_sets requires claims to be present, each claim to define a unique id, and every claim_set entry to reference ids from claims.";
    }
}

//TODO: extend: https://openid.net/specs/openid-4-verifiable-presentations-1_0.html#name-credential-query
@ApiExtraModels(
    TrustListRef,
    TrustedAuthorityQueryEtsiTl,
    TrustedAuthorityQueryOpenIdFederation,
    DcSdJwtCredentialQueryMeta,
    MsoMdocCredentialQueryMeta,
    MsoMdocClaimsQuery,
)
export abstract class CredentialQuery {
    @IsString()
    @Matches(/^[A-Za-z0-9_-]+$/, {
        message:
            "id must be a non-empty string containing only alphanumeric characters, underscores, or hyphens",
    })
    id!: string;

    @ApiProperty({
        enum: DCQL_CREDENTIAL_FORMATS,
        description: "Credential format discriminator.",
    })
    @IsString()
    @IsEnum(DCQL_CREDENTIAL_FORMATS)
    format!: string;

    @IsOptional()
    @IsBoolean()
    multiple?: boolean;

    @IsOptional()
    @ValidateNested({ each: true })
    @Type(() => ClaimsQuery)
    claims?: ClaimsQuery[];

    @IsOptional()
    @IsArray()
    @Validate(ClaimSetsConsistencyConstraint)
    @ApiPropertyOptional({
        type: "array",
        items: { type: "array", items: { type: "string" } },
        description:
            "Ordered alternative claim combinations for this credential query.",
    })
    claim_sets?: string[][];

    @IsArray()
    @IsOptional()
    @ValidateNested({ each: true })
    @ApiPropertyOptional({
        type: "array",
        description:
            "Trusted authority constraints (discriminated by type) for this credential query.",
        items: {
            oneOf: [
                { $ref: getSchemaPath(TrustedAuthorityQueryEtsiTl) },
                {
                    $ref: getSchemaPath(TrustedAuthorityQueryOpenIdFederation),
                },
            ],
            discriminator: {
                propertyName: "type",
                mapping: {
                    [TrustedAuthorityType.ETSI_TL]: getSchemaPath(
                        TrustedAuthorityQueryEtsiTl,
                    ),
                    [TrustedAuthorityType.OPENID_FEDERATION]: getSchemaPath(
                        TrustedAuthorityQueryOpenIdFederation,
                    ),
                },
            },
        },
    })
    @Type(() => TrustedAuthorityQuery, {
        discriminator: {
            property: "type",
            subTypes: [
                {
                    value: TrustedAuthorityQueryEtsiTl,
                    name: TrustedAuthorityType.ETSI_TL,
                },
                {
                    value: TrustedAuthorityQueryOpenIdFederation,
                    name: TrustedAuthorityType.OPENID_FEDERATION,
                },
            ],
        },
        keepDiscriminatorProperty: true,
    })
    trusted_authorities?: TrustedAuthorityQueryValue[];
}

export class CredentialQueryDcSdJwt extends CredentialQuery {
    @ApiProperty({
        enum: ["dc+sd-jwt"],
        default: "dc+sd-jwt",
    })
    @IsString()
    @Equals("dc+sd-jwt")
    format = "dc+sd-jwt" as const;

    @ApiProperty({
        type: DcSdJwtCredentialQueryMeta,
        description: "dc+sd-jwt schema metadata for the requested credential.",
    })
    @IsDefined()
    @ValidateNested()
    @Type(() => DcSdJwtCredentialQueryMeta)
    meta!: DcSdJwtCredentialQueryMeta;

    @IsOptional()
    @ValidateNested({ each: true })
    @Type(() => ClaimsQuery)
    declare claims?: ClaimsQuery[];
}

export class CredentialQueryMsoMdoc extends CredentialQuery {
    @ApiProperty({
        enum: ["mso_mdoc"],
        default: "mso_mdoc",
    })
    @IsString()
    @Equals("mso_mdoc")
    format = "mso_mdoc" as const;

    @ApiProperty({
        type: MsoMdocCredentialQueryMeta,
        description:
            "mso_mdoc document type metadata for the requested credential.",
    })
    @IsDefined()
    @ValidateNested()
    @Type(() => MsoMdocCredentialQueryMeta)
    meta!: MsoMdocCredentialQueryMeta;

    @IsOptional()
    @ValidateNested({ each: true })
    @Type(() => MsoMdocClaimsQuery)
    declare claims?: MsoMdocClaimsQuery[];
}

export type CredentialQueryValue =
    | CredentialQueryDcSdJwt
    | CredentialQueryMsoMdoc;

@ApiExtraModels(CredentialQueryDcSdJwt, CredentialQueryMsoMdoc)
export class DCQL {
    @IsArray()
    @ValidateNested({ each: true })
    @Type(() => CredentialQuery, {
        discriminator: {
            property: "format",
            subTypes: [
                {
                    value: CredentialQueryDcSdJwt,
                    name: "dc+sd-jwt",
                },
                {
                    value: CredentialQueryMsoMdoc,
                    name: "mso_mdoc",
                },
            ],
        },
        keepDiscriminatorProperty: true,
    })
    @ApiPropertyOptional({
        type: "array",
        description: "Format-discriminated credential queries.",
        items: {
            oneOf: [
                { $ref: getSchemaPath(CredentialQueryDcSdJwt) },
                { $ref: getSchemaPath(CredentialQueryMsoMdoc) },
            ],
            discriminator: {
                propertyName: "format",
                mapping: {
                    "dc+sd-jwt": getSchemaPath(CredentialQueryDcSdJwt),
                    mso_mdoc: getSchemaPath(CredentialQueryMsoMdoc),
                },
            },
        },
    })
    credentials!: CredentialQueryValue[];

    @IsArray()
    @IsOptional()
    @ValidateNested({ each: true })
    @Type(() => CredentialSetQuery)
    credential_sets?: CredentialSetQuery[];
}

export class TransactionData {
    @IsString()
    type!: string;
    @IsArray()
    @IsString({ each: true })
    credential_ids!: string[];
    [key: string]: any;
}

/**
 * Cached/materialized registration certificate state for a presentation config.
 *
 * Server-managed; recomputed when {@link PresentationConfig.registration_cert} or
 * {@link PresentationConfig.dcql_query} change, or when the JWT expires.
 */
export interface RegistrationCertCache {
    /** The issued/imported registration certificate JWT. */
    jwt: string;
    /** Canonical-JSON hash of the cert's authorized `credentials` claim. */
    fingerprint: string;
    /** Canonical-JSON hash of the presentation's `dcql_query.credentials` at cache time. */
    dcqlFingerprint: string;
    /** Canonical-JSON hash of the {@link PresentationConfig.registration_cert} spec at cache time. */
    specFingerprint: string;
    /** JWT `iat` (seconds since epoch). */
    issuedAt?: number;
    /** JWT `exp` (seconds since epoch). */
    expiresAt?: number;
    /** Origin of the cached JWT. */
    source: "imported" | "registrar";
}

/**
 * Entity representing a configuration for a Verifiable Presentation (VP) request.
 */
@Entity()
export class PresentationConfig {
    /**
     * Unique identifier for the VP request.
     */
    @Column("varchar", { primary: true })
    @IsString()
    id!: string;

    /**
     * The tenant ID for which the VP request is made.
     */
    @ApiHideProperty()
    @Column("varchar", { primary: true })
    tenantId!: string;

    /**
     * The tenant that owns this object.
     */
    @ManyToOne(() => TenantEntity, { cascade: true, onDelete: "CASCADE" })
    tenant!: TenantEntity;

    /**
     * Description of the presentation configuration.
     */
    @Column("varchar", { nullable: true })
    @IsOptional()
    @IsString()
    description?: string | null;

    /**
     * Lifetime how long the presentation request is valid after creation, in seconds.
     */
    @IsNumber()
    @IsOptional()
    @Column("int", { default: 300 })
    lifeTime?: number;

    /**
     * Clock skew tolerance for credential JWT time validation, in seconds.
     */
    @ApiPropertyOptional({
        description:
            "Clock skew tolerance for credential JWT time validation, in seconds.",
        default: 60,
    })
    @IsNumber()
    @Min(0)
    @IsOptional()
    @Column("int", { default: 60 })
    skewSeconds?: number;

    /**
     * Controls how credential status lists (revocation/suspension) are handled during verification.
     */
    @ApiPropertyOptional({
        description:
            "Status list verification mode for presentations: strict (default), best_effort, or disabled.",
        enum: RevocationCheckMode,
        default: RevocationCheckMode.Strict,
    })
    @IsEnum(RevocationCheckMode)
    @IsOptional()
    @Column("varchar", { default: RevocationCheckMode.Strict })
    statusCheckMode?: RevocationCheckMode;

    /**
     * The DCQL query to be used for the VP request.
     */
    @Column("json")
    @ValidateNested()
    @Type(() => DCQL)
    dcql_query!: DCQL;

    /**
     *
     */
    @Column("json", { nullable: true })
    @IsOptional()
    @IsArray()
    @IsTransactionData()
    @Type(() => TransactionData)
    transaction_data?: TransactionData[];

    /**
     * The registration certificate request containing the necessary details.
     */
    @IsOptional()
    @ValidateNested()
    @Type(() => RegistrationCertificateRequest)
    @Column("json", { nullable: true })
    registration_cert?: RegistrationCertificateRequest | null;

    /**
     * Cached/materialized registration certificate derived from {@link registration_cert}.
     *
     * This is a server-managed field (not user-editable). It stores the JWT that
     * was actually issued (or imported) together with fingerprints used to detect
     * configuration drift. The cache is invalidated automatically when either the
     * `registrationCert` spec or the `dcql_query` of this presentation config
     * changes, ensuring no stale/over-broad authorizations leak into VP requests.
     *
     * @example
     * {
     *   "jwt": "eyJ...",
     *   "fingerprint": "<canonical hash of authorized credentials[]>",
     *   "dcqlFingerprint": "<canonical hash of dcql_query.credentials>",
     *   "issuedAt": 1714050000,
     *   "expiresAt": 1714650000,
     *   "source": "registrar"
     * }
     */
    @ApiPropertyOptional({
        description:
            "Server-managed cache of the materialized registration certificate. Read-only; values supplied by clients are ignored.",
        readOnly: true,
        type: "object",
        additionalProperties: true,
        nullable: true,
    })
    @IsOptional()
    @IsObject()
    @Column("json", { nullable: true })
    registrationCertCache?: RegistrationCertCache | null;

    /**
     * Reference to the webhook endpoint used for notifications.
     * Optional: if set, notifications will be sent to this endpoint.
     */
    @IsOptional()
    @IsString()
    @Column("varchar", { nullable: true })
    webhookEndpointId?: string | null;

    @ApiHideProperty()
    @ManyToOne(() => WebhookEndpointEntity, {
        createForeignKeyConstraints: false,
    })
    @JoinColumn([
        { name: "webhookEndpointId", referencedColumnName: "id" },
        { name: "tenantId", referencedColumnName: "tenantId" },
    ])
    webhookEndpoint?: WebhookEndpointEntity;

    /**
     * The timestamp when the VP request was created.
     */
    @CreateDateColumn()
    createdAt!: Date;

    /**
     * The timestamp when the VP request was last updated.
     */
    @UpdateDateColumn()
    updatedAt!: Date;

    /**
     * Attestation that should be attached
     */
    @IsOptional()
    @IsArray()
    @ValidateNested()
    @Type(() => PresentationAttachment)
    @Column("json", { nullable: true })
    attached?: PresentationAttachment[] | null;

    /**
     * Redirect URI to which the user-agent should be redirected after the presentation is completed.
     * You can use the `{sessionId}` placeholder in the URI, which will be replaced with the actual session ID.
     * @example "https://example.com/callback?session={sessionId}"
     */
    @IsOptional()
    @IsString()
    @Column("varchar", { nullable: true })
    redirectUri?: string | null;

    /**
     * Optional ID of the access certificate to use for signing the presentation request.
     * If not provided, the default access certificate for the tenant will be used.
     *
     * Note: This is intentionally NOT a TypeORM relationship because CertEntity uses
     * a composite primary key (id + tenantId), and SQLite cannot create foreign keys
     * that reference only part of a composite primary key. The relationship is handled
     * at the application level in the service layer.
     */
    @IsOptional()
    @IsString()
    @Column("varchar", { nullable: true })
    accessKeyChainId?: string | null;

    /**
     * Enable reader authentication for the ISO 18013-7 Annex C (DC API) flow.
     *
     * When `true`, the DeviceRequest embeds a detached `readerAuth` COSE_Sign1
     * signed with the tenant's Access key chain (selected by
     * {@link accessKeyChainId}), letting the wallet cryptographically
     * authenticate the verifier — the mDOC equivalent of the signed request
     * object used in the OID4VP flow. Defaults to disabled (null/false).
     *
     * Only affects `response_type: "iso-18013-7"` offers.
     */
    @IsOptional()
    @IsBoolean()
    @Column("boolean", { nullable: true })
    readerAuth?: boolean | null;
}

results matching ""

    No results matching ""