File

src/crypto/key/key-chain.service.ts

Description

KeyChainService manages the unified key chain model.

Acts as a facade: all key generation, import and signing is delegated to a KmsAdapter resolved via KmsProviderRegistry. Certificate construction is delegated to CertificateBuilderService.

Index

Properties
Methods

Constructor

constructor(keyChainRepository: Repository<KeyChainEntity>, tenantRepository: Repository<TenantEntity>, configService: ConfigService, kmsRegistry: KmsProviderRegistry, certBuilder: CertificateBuilderService, signingService: KeyChainSigningService, importService: KeyChainImportService)
Parameters :
Name Type Optional
keyChainRepository Repository<KeyChainEntity> No
tenantRepository Repository<TenantEntity> No
configService ConfigService No
kmsRegistry KmsProviderRegistry No
certBuilder CertificateBuilderService No
signingService KeyChainSigningService No
importService KeyChainImportService No

Methods

Private calculateNextRotation
calculateNextRotation(keyChain: KeyChainEntity)
Parameters :
Name Type Optional
keyChain KeyChainEntity No
Returns : Date | undefined
Async create
create(tenantId: string, dto: KeyChainCreateDto)
Parameters :
Name Type Optional
tenantId string No
dto KeyChainCreateDto No
Returns : Promise<string>
Private Async createInternalChain
createInternalChain(id: string, adapter: KmsAdapter, subjectCN: string, hostname: string, notBefore: Date, notAfter: Date)
Parameters :
Name Type Optional
id string No
adapter KmsAdapter No
subjectCN string No
hostname string No
notBefore Date No
notAfter Date No
Async createStandalone
createStandalone(options: literal type)
Parameters :
Name Type Optional
options literal type No
Returns : Promise<string>
Private Async createStandaloneKey
createStandaloneKey(id: string, adapter: KmsAdapter, subjectCN: string, hostname: string, notBefore: Date, notAfter: Date)
Parameters :
Name Type Optional
id string No
adapter KmsAdapter No
subjectCN string No
hostname string No
notBefore Date No
notAfter Date No
Async delete
delete(tenantId: string, id: string)
Parameters :
Name Type Optional
tenantId string No
id string No
Returns : Promise<void>
Async export
export(tenantId: string, id: string)
Parameters :
Name Type Optional
tenantId string No
id string No
Async findByUsageType
findByUsageType(tenantId: string, usageType: KeyUsageType, keyId?: string, fallbackUsageType?: KeyUsageType)
Parameters :
Name Type Optional
tenantId string No
usageType KeyUsageType No
keyId string Yes
fallbackUsageType KeyUsageType Yes
Async getActiveCertificate
getActiveCertificate(tenantId: string, id: string)
Parameters :
Name Type Optional
tenantId string No
id string No
Returns : Promise<string>
Async getActiveKey
getActiveKey(tenantId: string, id: string)
Parameters :
Name Type Optional
tenantId string No
id string No
Returns : Promise<JWK>
Async getAll
getAll(tenantId: string, usageType?: KeyUsageType)
Parameters :
Name Type Optional
tenantId string No
usageType KeyUsageType Yes
Async getById
getById(tenantId: string, id: string)
Parameters :
Name Type Optional
tenantId string No
id string No
Async getEntity
getEntity(tenantId: string, id: string)
Parameters :
Name Type Optional
tenantId string No
id string No
Private getHostname
getHostname()
Returns : string
Async getKid
getKid(tenantId: string)
Parameters :
Name Type Optional
tenantId string No
Returns : Promise<string>
getProviderHealth
getProviderHealth()

Run a health probe for every registered KMS provider.

Returns : Promise<Array<literal type>>
getProviders
getProviders()

Return registered KMS providers (delegated to the registry).

Private getPublicJwk
getPublicJwk(jwk: JWK)
Parameters :
Name Type Optional
jwk JWK No
Returns : JWK
getPublicKey
getPublicKey(type: unknown, tenantId: string, keyId?: string)
Parameters :
Name Type Optional
type unknown No
tenantId string No
keyId string Yes
Returns : Promise<JWK>
getPublicKey
getPublicKey(type: unknown, tenantId: string, keyId?: string)
Parameters :
Name Type Optional
type unknown No
tenantId string No
keyId string Yes
Returns : Promise<string>
getPublicKey
getPublicKey(type: "pem" | "jwk", tenantId: string, keyId?: string)
Parameters :
Name Type Optional
type "pem" | "jwk" No
tenantId string No
keyId string Yes
Returns : Promise<JWK | string>
Async getPublicKeys
getPublicKeys(tenantId: string, id: string)
Parameters :
Name Type Optional
tenantId string No
id string No
Returns : Promise<JWK[]>
Async importKeyChain
importKeyChain(tenantId: string, dto: KeyChainImportDto)
Parameters :
Name Type Optional
tenantId string No
dto KeyChainImportDto No
Returns : Promise<string>
Private refForStoredKey
refForStoredKey(adapter: KmsAdapter, storedJwk: JWK, externalKeyId?: string)

Build a KmsKeyRef for any stored JWK (active or root).

  • db provider: the stored JWK is the private JWK; derive the public JWK from it.
  • external providers: the stored JWK is the public JWK; the private key lives in the backend, identified by externalKeyId (falling back to storedJwk.kid which is what adapters use as the external key identifier).
Parameters :
Name Type Optional
adapter KmsAdapter No
storedJwk JWK No
externalKeyId string Yes
Returns : KmsKeyRef
Private refFromEntity
refFromEntity(keyChain: KeyChainEntity)

Reconstruct a KmsKeyRef from a persisted entity so the adapter can sign / delete.

Parameters :
Name Type Optional
keyChain KeyChainEntity No
Returns : KmsKeyRef
Async rotate
rotate(tenantId: string, id: string)
Parameters :
Name Type Optional
tenantId string No
id string No
Returns : Promise<void>
Async signer
signer(tenantId: string, keyId?: string)
Parameters :
Name Type Optional
tenantId string No
keyId string Yes
Returns : Promise<Signer>
Async signJWT
signJWT(payload: JWTPayload, header: JWSHeaderParameters, tenantId: string, keyId?: string)
Parameters :
Name Type Optional
payload JWTPayload No
header JWSHeaderParameters No
tenantId string No
keyId string Yes
Returns : Promise<string>
Private storedKeyForEntity
storedKeyForEntity(adapter: KmsAdapter, ref: KmsKeyRef)

What gets persisted into the entity's activeJwk / rootJwk JWK column. For the db provider we keep the full private JWK (so signing can re-import it). For external providers we keep only the public JWK — the private key lives in the KMS backend.

Parameters :
Name Type Optional
adapter KmsAdapter No
ref KmsKeyRef No
Returns : JWK
Private toCertificateInfo
toCertificateInfo(pem: string)
Parameters :
Name Type Optional
pem string No
Returns : CertificateInfoDto
Private toPublicKeyInfo
toPublicKeyInfo(jwk: JWK)
Parameters :
Name Type Optional
jwk JWK No
Returns : PublicKeyInfoDto
Private toResponseDto
toResponseDto(keyChain: KeyChainEntity)
Parameters :
Name Type Optional
keyChain KeyChainEntity No
Async update
update(tenantId: string, id: string, dto: KeyChainUpdateDto)
Parameters :
Name Type Optional
tenantId string No
id string No
dto KeyChainUpdateDto No
Returns : Promise<void>

Properties

Private Readonly logger
Type : unknown
Default value : new Logger(KeyChainService.name)
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { InjectRepository } from "@nestjs/typeorm";
import * as x509 from "@peculiar/x509";
import type { Signer } from "@sd-jwt/types";
import type { JWK, JWSHeaderParameters, JWTPayload } from "jose";
import { Repository } from "typeorm";
import { v4 } from "uuid";
import { TenantEntity } from "../../auth/tenant/entitites/tenant.entity";
import { CertificateBuilderService } from "./cert/certificate-builder.service";
import { KeyChainCreateDto, KeyChainType } from "./dto/key-chain-create.dto";
import { KeyChainExportDto } from "./dto/key-chain-export.dto";
import { KeyChainImportDto } from "./dto/key-chain-import.dto";
import {
    CertificateInfoDto,
    KeyChainResponseDto,
    PublicKeyInfoDto,
} from "./dto/key-chain-response.dto";
import { KeyChainUpdateDto } from "./dto/key-chain-update.dto";
import { KmsProvidersResponseDto } from "./dto/kms-providers-response.dto";
import {
    KeyChainEntity,
    KeyUsage,
    KeyUsageType,
} from "./entities/key-chain.entity";
import type { KmsAdapter, KmsKeyRef, KmsSigningAlg } from "./kms/kms-adapter";
import { KmsProviderRegistry } from "./kms/kms-provider.registry";
import { KeyChainImportService } from "./key-chain-import.service";
import { KeyChainSigningService } from "./key-chain-signing.service";

/**
 * KeyChainService manages the unified key chain model.
 *
 * Acts as a facade: all key generation, import and signing is delegated
 * to a {@link KmsAdapter} resolved via {@link KmsProviderRegistry}.
 * Certificate construction is delegated to
 * {@link CertificateBuilderService}.
 */
@Injectable()
export class KeyChainService {
    private readonly logger = new Logger(KeyChainService.name);

    constructor(
        @InjectRepository(KeyChainEntity)
        private readonly keyChainRepository: Repository<KeyChainEntity>,
        @InjectRepository(TenantEntity)
        private readonly tenantRepository: Repository<TenantEntity>,
        private readonly configService: ConfigService,
        private readonly kmsRegistry: KmsProviderRegistry,
        private readonly certBuilder: CertificateBuilderService,
        private readonly signingService: KeyChainSigningService,
        private readonly importService: KeyChainImportService,
    ) {}

    /** Return registered KMS providers (delegated to the registry). */
    getProviders(): KmsProvidersResponseDto {
        return this.kmsRegistry.list();
    }

    /** Run a health probe for every registered KMS provider. */
    getProviderHealth(): Promise<
        Array<{
            providerId: string;
            type: string;
            ok: boolean;
            latencyMs?: number;
            error?: string;
        }>
    > {
        return this.kmsRegistry.health();
    }

    async create(tenantId: string, dto: KeyChainCreateDto): Promise<string> {
        const id = v4();
        const tenant = await this.tenantRepository.findOneByOrFail({
            id: tenantId,
        });
        const hostname = this.getHostname();
        const subjectCN = tenant.name;

        const now = new Date();
        const certValidityDays = dto.rotationPolicy?.certValidityDays || 365;
        const notAfter = new Date(
            now.getTime() + certValidityDays * 24 * 60 * 60 * 1000,
        );

        const adapter = this.kmsRegistry.resolve(dto.kmsProvider);

        let keyChain: Partial<KeyChainEntity>;

        if (dto.type === KeyChainType.InternalChain) {
            keyChain = await this.createInternalChain(
                id,
                adapter,
                subjectCN,
                hostname,
                now,
                notAfter,
            );
        } else {
            keyChain = await this.createStandaloneKey(
                id,
                adapter,
                subjectCN,
                hostname,
                now,
                notAfter,
            );
        }

        await this.keyChainRepository.save({
            ...keyChain,
            id,
            tenantId,
            usageType: dto.usageType,
            usage: KeyUsage.Sign,
            description: dto.description,
            kmsProvider: adapter.providerId,
            rotationEnabled: dto.rotationPolicy?.enabled ?? false,
            rotationIntervalDays: dto.rotationPolicy?.intervalDays,
            certValidityDays: dto.rotationPolicy?.certValidityDays,
        } as KeyChainEntity);

        this.logger.log(
            `Created key chain ${id} for tenant ${tenantId} (type: ${dto.type}, provider: ${adapter.providerId})`,
        );
        return id;
    }

    async createStandalone(options: {
        tenantId: string;
        description?: string;
        usageType: KeyUsageType;
        privateKey: JWK;
    }): Promise<string> {
        const id = v4();
        const { tenantId, privateKey, usageType, description } = options;

        if (!privateKey.kid) {
            privateKey.kid = `${id}-active`;
        }

        await this.keyChainRepository.save({
            id,
            tenantId,
            usageType,
            usage: KeyUsage.Encrypt,
            description: description || "Encryption key",
            kmsProvider: "db",
            activeJwk: privateKey,
            activeCertificate: "",
            rotationEnabled: false,
        } as KeyChainEntity);

        this.logger.log(
            `Created standalone key chain ${id} for tenant ${tenantId}`,
        );
        return id;
    }

    private async createInternalChain(
        id: string,
        adapter: KmsAdapter,
        subjectCN: string,
        hostname: string,
        notBefore: Date,
        notAfter: Date,
    ): Promise<Partial<KeyChainEntity>> {
        const rootMat = await adapter.generateKey({ kid: `${id}-root` });
        const rootNotAfter = new Date(
            notBefore.getTime() + 10 * 365 * 24 * 60 * 60 * 1000,
        );
        const rootCertificate = await this.certBuilder.createSelfSignedCaCert(
            adapter,
            rootMat.ref,
            `${subjectCN} Root CA`,
            hostname,
            notBefore,
            rootNotAfter,
        );

        const activeMat = await adapter.generateKey({ kid: `${id}-active` });
        const { chain } = await this.certBuilder.createCaSignedCert({
            caAdapter: adapter,
            caRef: rootMat.ref,
            caCertPem: rootCertificate,
            subjectPublicJwk: activeMat.ref.publicJwk,
            subjectCN,
            hostname,
            notBefore,
            notAfter,
        });

        return {
            rootJwk: this.storedKeyForEntity(adapter, rootMat.ref),
            rootCertificate,
            activeJwk: this.storedKeyForEntity(adapter, activeMat.ref),
            activeCertificate: chain.join("\n"),
            externalKeyId: activeMat.ref.externalKeyId,
        };
    }

    private async createStandaloneKey(
        id: string,
        adapter: KmsAdapter,
        subjectCN: string,
        hostname: string,
        notBefore: Date,
        notAfter: Date,
    ): Promise<Partial<KeyChainEntity>> {
        const mat = await adapter.generateKey({ kid: `${id}-active` });
        const certificate = await this.certBuilder.createSelfSignedCert(
            adapter,
            mat.ref,
            subjectCN,
            hostname,
            notBefore,
            notAfter,
        );

        return {
            activeJwk: this.storedKeyForEntity(adapter, mat.ref),
            activeCertificate: certificate,
            externalKeyId: mat.ref.externalKeyId,
        };
    }

    async getAll(
        tenantId: string,
        usageType?: KeyUsageType,
    ): Promise<KeyChainResponseDto[]> {
        const keyChains = await this.keyChainRepository.find({
            where: { tenantId, ...(usageType ? { usageType } : {}) },
        });

        return keyChains.map((kc) => this.toResponseDto(kc));
    }

    async getById(tenantId: string, id: string): Promise<KeyChainResponseDto> {
        const keyChain = await this.keyChainRepository.findOne({
            where: { tenantId, id },
        });

        if (!keyChain) {
            throw new NotFoundException(`Key chain ${id} not found`);
        }

        return this.toResponseDto(keyChain);
    }

    async export(tenantId: string, id: string): Promise<KeyChainExportDto> {
        const keyChain = await this.getEntity(tenantId, id);

        const exportDto: KeyChainExportDto = {
            id: keyChain.id,
            description: keyChain.description,
            usageType: keyChain.usageType,
            key: keyChain.hasInternalCa()
                ? (keyChain.rootJwk as KeyChainExportDto["key"])
                : (keyChain.activeJwk as KeyChainExportDto["key"]),
            kmsProvider: keyChain.kmsProvider,
        };

        const certs: string[] = [];
        if (keyChain.hasInternalCa()) {
            if (keyChain.rootCertificate) {
                certs.push(keyChain.rootCertificate.trim());
            }
        } else if (keyChain.activeCertificate) {
            certs.push(
                ...this.certBuilder.splitPemChain(keyChain.activeCertificate),
            );
        }
        if (certs.length > 0) {
            exportDto.crt = certs;
        }

        if (keyChain.rotationEnabled) {
            exportDto.rotationPolicy = {
                enabled: true,
                intervalDays: keyChain.rotationIntervalDays,
                certValidityDays: keyChain.certValidityDays,
            };
        }

        return exportDto;
    }

    async getEntity(tenantId: string, id: string): Promise<KeyChainEntity> {
        const keyChain = await this.keyChainRepository.findOne({
            where: { tenantId, id },
        });

        if (!keyChain) {
            throw new NotFoundException(`Key chain ${id} not found`);
        }

        return keyChain;
    }

    async findByUsageType(
        tenantId: string,
        usageType: KeyUsageType,
        keyId?: string,
        fallbackUsageType?: KeyUsageType,
    ): Promise<KeyChainEntity> {
        const whereClause: Record<string, unknown> = {
            tenantId,
            usageType,
        };

        if (keyId) {
            whereClause.id = keyId;
        }

        let keyChain = await this.keyChainRepository.findOne({
            where: whereClause,
        });

        if (!keyChain && fallbackUsageType) {
            const fallbackWhere: Record<string, unknown> = {
                tenantId,
                usageType: fallbackUsageType,
            };
            if (keyId) {
                fallbackWhere.id = keyId;
            }
            keyChain = await this.keyChainRepository.findOne({
                where: fallbackWhere,
            });
        }

        if (!keyChain) {
            const types = fallbackUsageType
                ? `'${usageType}' or '${fallbackUsageType}'`
                : `'${usageType}'`;
            throw new NotFoundException(
                `No key chain found with usage type ${types} for tenant ${tenantId}`,
            );
        }

        return keyChain;
    }

    async update(
        tenantId: string,
        id: string,
        dto: KeyChainUpdateDto,
    ): Promise<void> {
        await this.getEntity(tenantId, id);

        const updates: Partial<KeyChainEntity> = {};

        if (dto.description !== undefined) {
            updates.description = dto.description;
        }

        if (dto.rotationPolicy) {
            if (dto.rotationPolicy.enabled !== undefined) {
                updates.rotationEnabled = dto.rotationPolicy.enabled;
            }
            if (dto.rotationPolicy.intervalDays !== undefined) {
                updates.rotationIntervalDays = dto.rotationPolicy.intervalDays;
            }
            if (dto.rotationPolicy.certValidityDays !== undefined) {
                updates.certValidityDays = dto.rotationPolicy.certValidityDays;
            }
        }

        if (dto.activeCertificate !== undefined) {
            updates.activeCertificate = dto.activeCertificate;
        }

        await this.keyChainRepository.update({ tenantId, id }, updates);
        this.logger.log(`Updated key chain ${id}`);
    }

    async delete(tenantId: string, id: string): Promise<void> {
        const keyChain = await this.keyChainRepository.findOne({
            where: { tenantId, id },
        });
        if (!keyChain) {
            throw new NotFoundException(`Key chain ${id} not found`);
        }

        // Best-effort: ask the adapter to clean up external key material.
        try {
            const adapter = this.kmsRegistry.resolve(keyChain.kmsProvider);
            if (adapter.capabilities.canDelete) {
                await adapter.deleteKey(this.refFromEntity(keyChain));
            }
        } catch (err) {
            this.logger.warn(
                `Failed to delete external key material for key chain ${id}: ${String(err)}`,
            );
        }

        await this.keyChainRepository.delete({ tenantId, id });
        this.logger.log(`Deleted key chain ${id}`);
    }

    // ─────────────────────── config import ───────────────────────

    async importKeyChain(
        tenantId: string,
        dto: KeyChainImportDto,
    ): Promise<string> {
        return this.importService.importKeyChain(tenantId, dto);
    }

    async rotate(tenantId: string, id: string): Promise<void> {
        const keyChain = await this.getEntity(tenantId, id);
        const hostname = this.getHostname();
        const tenant = await this.tenantRepository.findOneByOrFail({
            id: tenantId,
        });
        const subjectCN = tenant.name;

        const now = new Date();
        const certValidityDays = keyChain.certValidityDays || 365;
        const notAfter = new Date(
            now.getTime() + certValidityDays * 24 * 60 * 60 * 1000,
        );

        const gracePeriodDays = 30;
        const previousKeyExpiry = new Date(
            now.getTime() + gracePeriodDays * 24 * 60 * 60 * 1000,
        );

        const previousJwk = keyChain.activeJwk;
        const previousCertificate = keyChain.activeCertificate;

        const adapter = this.kmsRegistry.resolve(keyChain.kmsProvider);
        const newMat = await adapter.generateKey({
            kid: `${id}-${Date.now()}`,
        });

        let newCertificate: string;
        if (keyChain.hasInternalCa()) {
            // The root key already lives in the adapter's backing store.
            // Build a reference to it without re-importing (which would
            // create a duplicate key in external KMS backends).
            const caRef = this.refForStoredKey(adapter, keyChain.rootJwk!);
            const { chain } = await this.certBuilder.createCaSignedCert({
                caAdapter: adapter,
                caRef,
                caCertPem: keyChain.rootCertificate!,
                subjectPublicJwk: newMat.ref.publicJwk,
                subjectCN,
                hostname,
                notBefore: now,
                notAfter,
            });
            newCertificate = chain.join("\n");
        } else {
            newCertificate = await this.certBuilder.createSelfSignedCert(
                adapter,
                newMat.ref,
                subjectCN,
                hostname,
                now,
                notAfter,
            );
        }

        await this.keyChainRepository.update(
            { tenantId, id },
            {
                activeJwk: this.storedKeyForEntity(adapter, newMat.ref),
                activeCertificate: newCertificate,
                externalKeyId: newMat.ref.externalKeyId,
                previousJwk,
                previousCertificate,
                previousKeyExpiry,
                lastRotatedAt: now,
            },
        );

        this.logger.log(`Rotated key chain ${id}`);
    }

    async getActiveKey(tenantId: string, id: string): Promise<JWK> {
        const keyChain = await this.getEntity(tenantId, id);
        return keyChain.activeJwk;
    }

    async getPublicKeys(tenantId: string, id: string): Promise<JWK[]> {
        const keyChain = await this.getEntity(tenantId, id);
        return keyChain.getPublicKeys();
    }

    async getActiveCertificate(tenantId: string, id: string): Promise<string> {
        const keyChain = await this.getEntity(tenantId, id);
        return keyChain.activeCertificate;
    }

    // ─────────────────────────────────────────────────────────
    // SIGNING OPERATIONS — delegated to KeyChainSigningService
    // ─────────────────────────────────────────────────────────

    async signer(tenantId: string, keyId?: string): Promise<Signer> {
        return this.signingService.signer(tenantId, keyId);
    }

    async signJWT(
        payload: JWTPayload,
        header: JWSHeaderParameters,
        tenantId: string,
        keyId?: string,
    ): Promise<string> {
        return this.signingService.signJWT(payload, header, tenantId, keyId);
    }

    getPublicKey(type: "jwk", tenantId: string, keyId?: string): Promise<JWK>;
    getPublicKey(
        type: "pem",
        tenantId: string,
        keyId?: string,
    ): Promise<string>;
    getPublicKey(
        type: "pem" | "jwk",
        tenantId: string,
        keyId?: string,
    ): Promise<JWK | string> {
        if (type === "jwk") {
            return this.signingService.getPublicKey("jwk", tenantId, keyId);
        }
        return this.signingService.getPublicKey("pem", tenantId, keyId);
    }

    async getKid(tenantId: string): Promise<string> {
        return this.signingService.getKid(tenantId);
    }

    // ─────────────────────────────────────────────────────────
    // HELPER METHODS
    // ─────────────────────────────────────────────────────────

    private getHostname(): string {
        return new URL(this.configService.getOrThrow<string>("PUBLIC_URL"))
            .hostname;
    }

    /**
     * What gets persisted into the entity's `activeJwk` / `rootJwk` JWK
     * column. For the `db` provider we keep the full private JWK (so
     * signing can re-import it). For external providers we keep only
     * the public JWK — the private key lives in the KMS backend.
     */
    private storedKeyForEntity(adapter: KmsAdapter, ref: KmsKeyRef): JWK {
        if (adapter.type === "db" && ref.privateJwk) {
            return ref.privateJwk;
        }
        return ref.publicJwk;
    }

    /**
     * Reconstruct a {@link KmsKeyRef} from a persisted entity so the
     * adapter can sign / delete.
     */
    private refFromEntity(keyChain: KeyChainEntity): KmsKeyRef {
        return this.refForStoredKey(
            this.kmsRegistry.resolve(keyChain.kmsProvider),
            keyChain.activeJwk,
            keyChain.externalKeyId ?? undefined,
        );
    }

    /**
     * Build a {@link KmsKeyRef} for any stored JWK (active or root).
     *
     * - `db` provider: the stored JWK is the private JWK; derive the
     *   public JWK from it.
     * - external providers: the stored JWK is the public JWK; the
     *   private key lives in the backend, identified by
     *   `externalKeyId` (falling back to `storedJwk.kid` which is
     *   what adapters use as the external key identifier).
     */
    private refForStoredKey(
        adapter: KmsAdapter,
        storedJwk: JWK,
        externalKeyId?: string,
    ): KmsKeyRef {
        const alg =
            (storedJwk.alg as KmsSigningAlg | undefined) ??
            adapter.capabilities.defaultAlg;
        if (adapter.type === "db") {
            const publicJwk = this.getPublicJwk(storedJwk);
            return { privateJwk: storedJwk, publicJwk, alg };
        }
        return {
            externalKeyId: externalKeyId ?? storedJwk.kid,
            publicJwk: storedJwk,
            alg,
        };
    }

    private getPublicJwk(jwk: JWK): JWK {
        const { d, p, q, dp, dq, qi, k, ...publicJwk } = jwk as Record<
            string,
            unknown
        >;
        return publicJwk as JWK;
    }

    private toResponseDto(keyChain: KeyChainEntity): KeyChainResponseDto {
        const type = keyChain.hasInternalCa()
            ? KeyChainType.InternalChain
            : KeyChainType.Standalone;

        const response: KeyChainResponseDto = {
            id: keyChain.id,
            usageType: keyChain.usageType,
            type,
            description: keyChain.description,
            kmsProvider: keyChain.kmsProvider,
            activePublicKey: this.toPublicKeyInfo(keyChain.activeJwk),
            rotationPolicy: {
                enabled: keyChain.rotationEnabled,
                intervalDays: keyChain.rotationIntervalDays,
                certValidityDays: keyChain.certValidityDays,
                nextRotationAt: this.calculateNextRotation(keyChain),
            },
            createdAt: keyChain.createdAt,
            updatedAt: keyChain.updatedAt,
        };

        if (keyChain.activeCertificate) {
            response.activeCertificate = this.toCertificateInfo(
                keyChain.activeCertificate,
            );
        }

        if (keyChain.rootCertificate) {
            response.rootCertificate = this.toCertificateInfo(
                keyChain.rootCertificate,
            );
        }

        if (keyChain.previousJwk) {
            response.previousPublicKey = this.toPublicKeyInfo(
                keyChain.previousJwk,
            );
            response.previousCertificate = this.toCertificateInfo(
                keyChain.previousCertificate!,
            );
            response.previousKeyExpiry = keyChain.previousKeyExpiry;
        }

        return response;
    }

    private toPublicKeyInfo(jwk: JWK): PublicKeyInfoDto {
        const publicJwk = this.getPublicJwk(jwk);
        return {
            kty: publicJwk.kty as string,
            alg: publicJwk.alg as string | undefined,
            kid: publicJwk.kid as string | undefined,
            crv: (publicJwk as Record<string, unknown>).crv as
                | string
                | undefined,
        };
    }

    private toCertificateInfo(pem: string): CertificateInfoDto {
        const firstCertPem =
            pem.split("-----END CERTIFICATE-----")[0] +
            "-----END CERTIFICATE-----";

        try {
            const cert = new x509.X509Certificate(firstCertPem);
            return {
                pem,
                subject: cert.subject,
                issuer: cert.issuer,
                notBefore: cert.notBefore,
                notAfter: cert.notAfter,
                serialNumber: cert.serialNumber,
            };
        } catch {
            return { pem };
        }
    }

    private calculateNextRotation(keyChain: KeyChainEntity): Date | undefined {
        if (!keyChain.rotationEnabled || !keyChain.rotationIntervalDays) {
            return undefined;
        }

        const baseDate = keyChain.lastRotatedAt || keyChain.createdAt;
        return new Date(
            baseDate.getTime() +
                keyChain.rotationIntervalDays * 24 * 60 * 60 * 1000,
        );
    }
}

results matching ""

    No results matching ""