Project: pro-desktop · Branch: master · Mode: ` · **Model:**claude-sonnet-4-5-20250929`
Stats: 1 user messages, 40 tool calls, tools used: Read, Glob, Grep, Bash.
Conversation
Turn 1 — User
Design an implementation plan for tracking ECC vs RSA client certificate usage for German TI environments (ticket FHIR-1996).
Requirements
- Track client certificate algorithm type (ECC/RSA) for German TI users when TI environment is configured
- Capture curve name for ECC certificates if possible (e.g., brainpoolP256r1)
- Send an event to Datadog when TI env is configured
- Enable creation of a Datadog dashboard showing ECC vs RSA distribution
Context from Codebase Exploration
TI Configuration Module
- Location:
/src/modules/gematik-ti-environment/ - Owner: FIRE team
- Key components:
- TiEnvRepository - Stores/retrieves TI env config (tiEnvValue: RU/PU/TITUS, tiEnvData)
- ConfigurationAutomator - Handles automatic TI setup (S3Configurator for prod, LocalConfigurator for dev)
- TiEnvCertificateProvider - Manages client certificates for TI
- IPC Events:
TiEnvConfigureevent triggered when environment is configured - Module Initialization:
gematik-ti-environment/main/module.ts- initialization completes here
Related Module: kbv-ti-connect
- Location:
/src/modules/kbv-ti-connect/ - Uses TI config via
isTiConfigured(settings)check - Handles actual certificate loading from PKCS#12 containers
Certificate Handling
- PKCS#12 parsing:
/src/modules/gematik-crypto/services/node/keystore/pkcs12.ts - Uses node-forge library to extract certificates from keystore
-
Methods:
getAllCertificates(),getLatestUserCertificate() -
Certificate type detection:
/src/modules/gematik-crypto/services/node/jose/JWSUtils.ts deriveJWAFromCertificate(certificate: string)method- Uses pkijs library to parse X.509 certificates
- Extracts algorithm name from public key
- For RSA: checks
publicKey.algorithm.name.match(/RSA/) - For ECC: extracts
publicKey.algorithm.namedCurve(e.g., 'brainpoolP256r1', 'P-256') - Special handling for BrainpoolP256R1 with OID 1.3.36.3.3.2.8.1.1.7
Datadog Tracking
- Observability system:
/src/core/observability/ sendToDatadog()- Direct HTTP POST to DatadogenqueueCustomDatadogEvent()- Queue-based sending-
Auto-enriches with: userId, accountId, country, OS info, app version
-
Tracking constants:
/src/core/monitoring/constants/tracking.ts - Define TrackingEvent enum values
-
Example:
TrackingEvent.KbvTiConnectalready exists -
Product events:
/src/core/product-events/ capturePro()- High-level product event API- Define events in
constants/product-events.ts
Libraries Available
- pkijs (v3.3.3) - X.509 certificate parsing
- asn1js - ASN.1 structure parsing
- @doctolib/node-forge (v1.3.7) - PKCS#12 parsing
- @doctolib/node-jose (v2.2.5) - JWK operations
Design Considerations
-
Where to hook the tracking? - Option A: In TiEnvModule initialization when config is loaded - Option B: In kbv-ti-connect when certificate is actually used - Option C: Listen to
TiEnvConfigureIPC event -
When to extract certificate info? - Need access to actual user certificate (PKCS#12) - kbv-ti-connect loads certificates from user's smart card/file - May need to track at certificate load time, not just config time
-
What data to track? - certificate_type: 'ECC' | 'RSA' - curve_name: string (for ECC, e.g., 'brainpoolP256r1', 'P-256') - ti_environment: 'RU' | 'PU' | 'TITUS' - account_id: from ObservabilityState - country: should be 'de' for German TI
-
Event naming? - Observability:
TrackingEvent.TiCertificateTypeDetected- Or product event:ddv__ti_certificate__detected
Please design: 1. The hook point for detecting when TI is configured with a certificate 2. The service/utility for extracting certificate type and curve name 3. The Datadog event structure and tracking implementation 4. File modifications needed 5. Testing approach
Consider the existing patterns in the codebase and make recommendations on the best ap …(truncated, 7 more chars)
Turn 1 — Assistant
I'll explore the codebase to understand the TI environment configuration flow and design a comprehensive implementation plan.
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/main/module.ts
Tool results:
→ result (ok): 1→import { isEmpty } from 'lodash' 2→ 3→import { getDefaultTiEnv } from 'src/constants' 4→ 5→import { isDev } from 'src/constants/env' 6→ 7→import { createLogger } from '@core/logger' 8→import { DdvModule } from '@core/modules-management/main' 9→ 10→import { TiEnvRepository } from '@module/gematik-ti-environment/services/tiEnvRepository' 11→import { registerTiEnvEvents, removeTiEnvEvents } from '@module/gematik-ti-environment/main/tiEnv' 12→imp …(truncated, 1785 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/services/TiEnvRepository.ts
Tool results:
→ result (ok): 1→import { getDefaultTiEnv } from 'src/constants' 2→ 3→import { 4→ TiEnvDataSchema, 5→ TiEnvValueSet, 6→ TI_ENV_DATA_KEY, 7→ TI_ENV_VALUE_KEY, 8→ TiEnvSchema, 9→ TI_ENV_LOCAL_ARTIFACT_KEY, 10→ TiEnvLocalArtifactConfig, 11→} from '@module/gematik-ti-environment/types/tiEnvStorage' 12→import { getTiEnvStore } from '@module/gematik-ti-environment/services/tiEnvStorage' 13→ 14→const DEFAULT_TI_ENV_ARTIFACT = { filePath: '', enabled: …(truncated, 2332 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/kbv-ti-connect/main/module.ts
Tool results:
→ result (ok): 1→import { v4 } from 'uuid' 2→ 3→import { DdvModule } from '@core/modules-management/main' 4→ 5→import { extraLogParser } from 'src/utils/extraLogParser' 6→ 7→import TiConnectionChecker from '../services/connection-checker' 8→import { isTiConfigured } from '../services/ti-connect-settings' 9→import { registerTiConnectEvents, removeTiConnectEvents } from './ti-connect' 10→import { TiConnectRepository } from '../services/ti-connect-repository' 11→imp …(truncated, 2123 more chars)
Tools used:
Glob:**/gematik-ti-environment/**/*TiEnvConfigure*.ts
Tool results:
→ result (ok): No files found
Tools used:
Glob:**/gematik-ti-environment/**/*tiEnv*.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-crypto/services/node/jose/JWSUtils.ts
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/types/tiEnvStorage.ts /Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/main/tiEnv.ts /Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/services/tiEnvProvider.ts /Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/services/tiEnvCertificateProvider.ts /Users/USER/workspace/doctolib/doctolib-pro-desktop/s …(truncated, 665 more chars)
Tool results:
→ result (ok): 1→import { Certificate } from 'pkijs' 2→import * as Asn1 from 'asn1js' 3→ 4→enum JWAlgorithms { 5→ HMAC = 'HS', // There is no MAC expected, so keeping here for the compatibility 6→ 'RSASSA-PKCS1-v1_5' = 'RS', 7→ ECDSA = 'ES', 8→ 'RSASSA-PSS' = 'PS', 9→} 10→ 11→type JWSAlgorithms = 'HMAC' | 'RSASSA-PKCS1-v1_5' | 'ECDSA' | 'RSASSA-PSS' 12→export class JWSUtils { 13→ static concatenateECDSASignature(signature: string, outputLength: numbe …(truncated, 5381 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-crypto/services/node/keystore/pkcs12.ts
Tool results:
→ result (ok): 1→import forge from '@doctolib/node-forge' 2→ 3→export class Pkcs12 { 4→ // Get certificate "bags" by type (http://oid-info.com/get/1.2.840.113549.1.1.5) 5→ static getBagsByBagType = (pkcs12: forge.pkcs12.Pkcs12Pfx, bagType: string): forge.pkcs12.Bag[] => { 6→ const bags = pkcs12.getBags({ bagType }) 7→ if (!bags) { 8→ throw new Error('No bags of this type') 9→ } 10→ 11→ const bagArr = bags[bagType] 12→ if (!bagArr) {
…(truncated, 4419 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/main/tiEnv.ts
Tool results:
→ result (ok): 1→import { isRelease } from 'src/constants' 2→ 3→import { ipcManager } from '@core/ipc-manager' 4→import { sendPreferences } from '@core/app/main/helpers/preferences-helper' 5→import { getTabsManager } from '@core/tabs/main/getTabsManager' 6→ 7→import { TiEnvDataSchema, TiEnvValueSet } from '@module/gematik-ti-environment/types/tiEnvStorage' 8→import { 9→ decryptTiEnvValues, 10→ setTiEnvValues, 11→ getTiEnvironment, 12→ getTiEnvLocalArtifac …(truncated, 5453 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/kbv-ti-connect/services/ti-connect-settings.ts
Tool results:
→ result (ok): 1→import { TiConnectSettings } from './types' 2→ 3→export const EMPTY_TI_SETTINGS: TiConnectSettings = { 4→ connectorHost: '', 5→ connectorPort: '', 6→ eventServiceHost: '', 7→ eventServicePort: '', 8→ keyStorePath: '', 9→ cert: '', 10→ privateKey: '', 11→ clientSystemId: '', 12→ mandantId: '', 13→ workplaceId: '', 14→ userId: '', 15→ useConnectorContextFromElectronSettings: false, 16→ serverCertificateFingerprint: …(truncated, 1005 more chars)
Tools used:
Grep:TiEnvConfigure|tiEnvConfigure
Tool results:
→ result (ok): Found 3 files src/modules/gematik-ti-environment/renderer/components/preferences/sections/TiEnvironment.tsx src/modules/gematik-ti-environment/main/tiEnv.ts src/modules/gematik-ti-environment/constants/events.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/constants/events.ts
Tool results:
→ result (ok): 1→/* 2→ * List of events happen during TiEnv IPC Events 3→ / 4→export enum TiEnvEvent { 5→ TiEnvChange = 'dl_ti_env_change', 6→ TiEnvDecrypt = 'dl_ti_env_decrypt', 7→ TiEnvConfigure = 'dl_ti_env_configure', 8→ TiEnvDisplay = 'dl_ti_env_display', 9→ TiEnvLocalArtifactsFile = 'dl_ti_env_local_artifacts_file', 10→ TiEnvUseLocalArtifacts = 'dl_ti_env_use_local_artifacts', 11→} 12→
<system-reminder> Whenever you read a file, you should cons …(truncated, 278 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/core/observability/services/sendToDatadog.ts
Tool results:
→ result (ok): 1→import { flatten } from 'flat' 2→ 3→import { isDevOrTest } from 'src/constants' 4→ 5→import { getDoctolibWebContents } from '@core/app/main/helpers/webcontents-helper' 6→import { fetch } from '@core/network/services/network' 7→ 8→import { ZipperWebRepository } from '@module/zipper-web/services/zipperWebRepository' 9→import { ZipperDesktopRepo } from '@module/zipper-desktop/services/zipperDesktopRepo' 10→ 11→import { isNodeError } from 'src/utils' …(truncated, 3857 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/core/monitoring/constants/tracking.ts
Tool results:
→ result (ok): 1→/* 2→ * List of constants used for Tracking Events 3→ / 4→import { PerformanceMonitoringEvent } from '@core/monitoring' 5→ 6→import { TabName } from 'src/shared/constants/tabName' 7→ 8→export enum TrackingEvent { 9→ // To be Migrated to Product Events and currently on Observability only 10→ OpenTab = 'dpd_tab_open', 11→ UpdateDownloadSpeed = 'dpd_update_download_speed', 12→ // Updater 13→ CheckUpdateMenuClicked = 'dpd_check_for_upd …(truncated, 12129 more chars)
Tools used:
Grep:enqueueCustomDatadogEvent
Tool results:
→ result (ok): Found 4 files src/core/monitoring/index.ts src/core/observability/services/tests/datadogTransport.test.ts src/core/observability/services/observability-winston-transport.ts src/core/observability/index.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/core/observability/services/enqueueCustomDatadogEvent.ts
Tool results:
→ result (ERROR): <tool_use_error>File does not exist.</tool_use_error>
Tools used:
Grep:TrackingEvent\.in**/gematik*/**/*.ts
Tool results:
→ result (ERROR): <tool_use_error>Sibling tool call errored</tool_use_error>
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/core/observability/index.ts
Tool results:
→ result (ok): 1→import { isTest } from 'src/constants' 2→ 3→import { TrackingEventMap } from '@core/monitoring/constants/tracking' 4→ 5→import { Label, TrackingObservabilityEventMap } from './interfaces/tracking' 6→import observabilityQueue from './main/ObservabilityQueue' 7→ 8→export * from './interfaces/observability' 9→export * from './interfaces/tracking' 10→export { default as observabilityState } from './main/ObservabilityState' 11→export { default as getO …(truncated, 1905 more chars)
Tools used:
Grep:TrackingEvent\.KbvTiConnect
Tool results:
→ result (ok): Found 2 files src/modules/kbv-ti-connect/services/service-directory-service/connectorProductInformation.ts src/modules/kbv-ti-connect/main/ti-connect.ts
Tools used:
Grep:import.*TrackingEventin**/kbv-ti-connect/**/*.ts
Tool results:
→ result (ok): Found 3 files src/modules/kbv-ti-connect/services/service-directory-service/connectorProductInformation.ts src/modules/kbv-ti-connect/main/tests/ti-connect.test.ts src/modules/kbv-ti-connect/main/ti-connect.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/kbv-ti-connect/main/ti-connect.ts
Tool results:
→ result (ok): 1→import { ipcManager } from '@core/ipc-manager' 2→import { sendPreferences } from '@core/app/main/helpers/preferences-helper' 3→import { TrackingEvent } from '@core/monitoring' 4→ 5→import { kbvTiConnectLogger } from '@module/kbv-ti-connect/services/logger' 6→import tiConnect from '@module/kbv-ti-connect/services' 7→import { decryptKeyStore, getNewKeyStore, readKeyStoreFromFile } from '@module/kbv-ti-connect/services/keyStore' 8→import { TiConnectEvent } from …(truncated, 7746 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/kbv-ti-connect/services/service-directory-service/connectorProductInformation.ts
Tool results:
→ result (ok): 1→import * as xml2js from 'xml2js' 2→ 3→import { TrackingEvent } from '@core/monitoring' 4→ 5→import { 6→ ConnectorConfiguration, 7→ ProductInformationExtract, 8→ ServiceDirectory, 9→} from '@module/kbv-ti-connect/services/types' 10→import { kbvTiConnectLogger } from '@module/kbv-ti-connect/services/logger' 11→import { tiConnectModuleMetadata } from '@module/kbv-ti-connect/constants/module' 12→ 13→import { extraLogParser, getNestedKeys } …(truncated, 3507 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/kbv-ti-connect/services/keyStore.ts
Tool results:
→ result (ok): 1→import { KeystoreFile, Pkcs12 } from '@module/gematik-crypto' 2→ 3→export const readKeyStoreFromFile = async (keyStorePath: string): Promise<string> => { 4→ return await KeystoreFile.read(keyStorePath) 5→} 6→ 7→export const getNewKeyStore = async (oldPath: string): Promise<string> => { 8→ return await KeystoreFile.defineNewPath(oldPath, ['openFile'], [{ extensions: ['p12'], name: '.p12' }]) 9→} 10→ 11→export const decryptKeyStore = async (
…(truncated, 541 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/utils/ti-connect-event-observability.ts
Tool results:
→ result (ok): 1→import winston from 'winston' 2→ 3→import { sendCustomTrackingEventToObservability, TrackingEvent } from '@core/monitoring' 4→ 5→import { ExtraLog, extraLogParser } from 'src/utils/extraLogParser' 6→ 7→export class TiConnectEventObservability { 8→ constructor( 9→ private readonly logger: winston.Logger, 10→ private readonly trackingEvent: TrackingEvent, 11→ private readonly moduleName: string, 12→ ) {} 13→ 14→ send(extra: …(truncated, 1036 more chars)
Tools used:
Grep:class.*ObservabilityState
Tool results:
→ result (ok): Found 1 file src/core/observability/main/ObservabilityState.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/core/observability/main/ObservabilityState.ts
Tool results:
→ result (ok): 1→import { 2→ CommonInfoObservability, 3→ SystemCPUInfoObservability, 4→ SystemMemoryInfoObservability, 5→ SystemRuntimeInfoObservability, 6→ SystemStorageInfoObservability, 7→ updaterObservability, 8→} from '@core/observability/interfaces/observability' 9→import { ProductLineConfig } from '@core/variation-management' 10→import { getAnonymisedHostname } from '@core/system-information/services/get-hostname' 11→ 12→function getAnonymisedUse …(truncated, 1862 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/core/monitoring/index.ts
Tool results:
→ result (ok): 1→export * from './constants' 2→export { default as performanceMonitoring } from './services/performance-monitoring' 3→export { enqueueCustomDatadogEvent as sendCustomTrackingEventToObservability } from '@core/observability' 4→export { enqueueObservabilityEvent as sendObservabilityEvent } from '@core/observability' 5→export { enqueueDatadogEvent as sendTrackingEventToObservability } from '@core/observability' 6→
<system-reminder> Whenever you read a file, you should con …(truncated, 279 more chars)
Tools used:
Grep:sendCustomTrackingEventToObservability
Tool results:
→ result (ok): Found 31 files src/modules/gematik-idp-authentication/services/tests_electron/idpAuthenticator.test.ts src/modules/billeo/services/billeo.ts src/modules/billeo/services/installer.ts src/modules/billeo/services/process/process-manager.ts src/core/print/services/printManager.ts src/core/print/services/tests/printManager.test.ts src/core/print/main/tests/handleFileDownloadAndPrint.test.ts src/core/print/main/handleFileDownloadAndPrint.ts src/core/print/main/index.ts src/core/auto-update …(truncated, 1109 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/services/configurators/s3Configurator.ts
Tool results:
→ result (ok): 1→import { TiEnvDataSchema, TiEnvValueSet } from '@module/gematik-ti-environment/types/tiEnvStorage' 2→import S3Reader from '@module/gematik-ti-environment/services/s3Reader' 3→ 4→import BaseConfigurator from './baseConfigurator' 5→ 6→class S3Configurator extends BaseConfigurator { 7→ name = 'S3Configurator' 8→ 9→ async getConfigurationData(env: TiEnvValueSet): Promise<TiEnvDataSchema> { 10→ return await S3Reader.getConfigurationData(env) 11→ …(truncated, 409 more chars)
Tools used:
Glob:**/gematik-ti-environment/services/configurationAutomator.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/core/observability/services/get-common-information.ts
Tool results:
→ result (ok): 1→import { APP_VERSION } from 'src/constants' 2→ 3→import { installerVersion } from '@core/system-information/services/get-os-architecture' 4→import { Country } from '@core/variation-management' 5→import { hostnameInfo, osInfo } from '@core/system-information' 6→ 7→import State from 'src/utils/State' 8→ 9→import { 10→ CommonInfoObservability, 11→ ddvInfoObservability, 12→ DDV_EVENT_TYPE, 13→ DoctolibAccountObservability, 14→ Doctol …(truncated, 4498 more chars)
Tool results:
→ result (ok): /Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/services/configurationAutomator.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/services/configurationAutomator.ts
Tool results:
→ result (ok): 1→import { isRelease } from 'src/constants' 2→ 3→import { getTiEnvLocalArtifacts } from '@module/gematik-ti-environment/services/tiEnv' 4→import S3Configurator from '@module/gematik-ti-environment/services/configurators/s3Configurator' 5→import LocalConfigurator from '@module/gematik-ti-environment/services/configurators/localConfigurator' 6→import { TiEnvValueSet } from '@module/gematik-ti-environment//types/tiEnvStorage' 7→ 8→export class ConfigurationAutomat …(truncated, 703 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/services/configurators/baseConfigurator.ts
Tool results:
→ result (ok): 1→import { TiEnvValueSet, TiEnvDataSchema } from '@module/gematik-ti-environment/types/tiEnvStorage' 2→import { setTiEnvironment, setTiEnvValues } from '@module/gematik-ti-environment/services/tiEnv' 3→ 4→abstract class BaseConfigurator { 5→ abstract name: string 6→ 7→ abstract getConfigurationData(env: TiEnvValueSet): Promise<TiEnvDataSchema> 8→ 9→ async configure(env: TiEnvValueSet): Promise<void> { 10→ const configurationData = await this.getC …(truncated, 521 more chars)
Tools used:
Bash:find src/modules/gematik-ti-environment -name "*.test.ts" -type f | head -5
Tool results:
→ result (ok): src/modules/gematik-ti-environment/main/tests/module.test.ts src/modules/gematik-ti-environment/services/tests/s3Reader.test.ts src/modules/gematik-ti-environment/services/tests/configurationAutomator.test.ts src/modules/gematik-ti-environment/services/tests/archive.test.ts src/modules/gematik-ti-environment/services/tests/configurators/localConfigurator.test.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/services/tiEnv.ts
Tool results:
→ result (ok): 1→/ eslint-disable @typescript-eslint/explicit-function-return-type / 2→import { 3→ TiEnvValueSet, 4→ TiEnvDataSchema, 5→ TiEnvLocalArtifactConfig, 6→} from '@module/gematik-ti-environment/types/tiEnvStorage' 7→import { TiEnvRepository } from '@module/gematik-ti-environment/services/tiEnvRepository' 8→import TiSafeStorage from '@module/gematik-ti-environment/services/tiSafeStorage' 9→ 10→type Encryptable = string | EncryptableObject 11→ 12 …(truncated, 2456 more chars)
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-ti-environment/types/tiEnvStorage.ts
Tool results:
→ result (ok): 1→/ eslint-disable @typescript-eslint/consistent-type-definitions / 2→ 3→/* 4→ * TI Env safe storage schema 5→ * Name: 'tienv' 6→ / 7→export enum TiEnvValueSet { 8→ RU = 'RU', 9→ PU = 'PU', 10→ TITUS = 'TITUS', 11→} 12→ 13→export type eRezeptSchema = { 14→ hostUrl: string 15→ userAgent: string 16→} 17→ 18→export type SandboxSchema = { 19→ serviceUrl: string 20→ userAgent: string 21→} 22→
…(truncated, 1804 more chars)
Tools used:
Bash:grep -r "decryptKeyStore\|exctractPkCertificatePair" src/modules/kbv-ti-connect --include="*.ts" | head -10
Tool results:
→ result (ok): src/modules/kbv-ti-connect/main/tests/ti-connect.test.ts:import { decryptKeyStore, getNewKeyStore, readKeyStoreFromFile } from '@module/kbv-ti-connect/services/keyStore' src/modules/kbv-ti-connect/main/tests/ti-connect.test.ts: decryptKeyStore: vi.fn(), src/modules/kbv-ti-connect/main/tests/ti-connect.test.ts: mockDecryptKeyStore = vi.mocked(decryptKeyStore) src/modules/kbv-ti-connect/main/ti-connect.ts:import { decryptKeyStore, getNewKeyStore, readKeyStoreFromFile } from '@modul …(truncated, 808 more chars)
Now I have a comprehensive understanding of the codebase. Let me look at some example tracking implementations to understand the pattern better.
Tools used:
Grep:TrackingEvent\.GematikQes
Tool results:
→ result (ok): Found 2 files src/modules/gematik-qes/main/tests/module.test.ts src/modules/gematik-qes/main/module.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-qes/main/module.ts
Tool results:
→ result (ok): 1→import { ipcManager } from '@core/ipc-manager' 2→import { DdvModule } from '@core/modules-management/main' 3→import { TrackingEvent } from '@core/monitoring' 4→ 5→import { qes } from '@module/gematik-qes/services' 6→import { QesEvent } from '@module/gematik-qes/constants/events' 7→import { QesRequestPayload } from '@module/gematik-qes/services/types' 8→import { gematikQesLogger } from '@module/gematik-qes/main/logger' 9→import { qesModuleMetadata } from …(truncated, 1761 more chars)
Perfect! Now I have all the information I need. Let me look at one more pattern to understand how certificate type info could be extracted.
Tools used:
Grep:deriveJWAFromCertificate
Tool results:
→ result (ok): Found 2 files src/modules/gematik-crypto/services/node/jose/tests_electron/JWSUtils.test.ts src/modules/gematik-crypto/services/node/jose/JWSUtils.ts
Tools used:
Read:/Users/USER/workspace/doctolib/doctolib-pro-desktop/src/modules/gematik-crypto/index.ts
Tool results:
→ result (ok): 1→export { DoctoCryptExecutor } from './services/docto-crypt' 2→ 3→export * from './services/crypto-adapter' 4→export { Pkcs12Adapter as Pkcs12 } from '@module/gematik-crypto/services/crypto-adapter/pkcs12Adapter' 5→export { KeystoreFile, type DialogOptionProperties } from '@module/gematik-crypto/services/node/keystore/keystoreFile' 6→export { 7→ LdapAdapter as LdapClient, 8→ type SearchOptions, 9→} from '@module/gematik-crypto/services/crypto-adapter/ld …(truncated, 357 more chars)
Excellent! I now have a comprehensive understanding of the codebase. Let me create a detailed implementation plan.
Based on my thorough exploration of the codebase, I can now design a comprehensive implementation plan for tracking ECC vs RSA client certificate usage for German TI environments.
Implementation Plan: FHIR-1996 - Track ECC vs RSA Client Certificate Usage
Architecture Overview
After analyzing the codebase, I've identified the optimal architecture that follows existing patterns:
Key Hook Point: The best place to track certificate type is in the onTiConnectDecryptKeyStoreListener function in /src/modules/kbv-ti-connect/main/ti-connect.ts (lines 98-152). This is where:
1. User uploads their PKCS#12 certificate file
2. Certificate is decrypted and extracted
3. TI configuration becomes active
4. We have access to both the certificate data and TI environment information
Why this is the optimal location: - It's the single point where certificates are loaded into the system - We have access to the actual certificate in PEM format after decryption - It happens when TI is being configured (matches requirement) - It already has observability tracking infrastructure in place - It's owned by FIRE team (same team as TI environment module)
Design Decisions
1. Certificate Type Extraction Strategy
- Create a new utility service: CertificateAnalyzer in gematik-crypto module
- Leverage existing JWSUtils.deriveJWAFromCertificate() logic (already handles ECC/RSA detection)
- Extract algorithm type and curve name from X.509 certificate
- Return structured data: { certificateType: 'ECC' | 'RSA', curveName?: string }
2. Datadog Event Structure
{
trackingLabel: 'TrackingEvent.TiCertificateConfigured',
certificate_type: 'ECC' | 'RSA',
curve_name: 'brainpoolP256r1' | 'P-256' | undefined (for RSA),
ti_environment: 'RU' | 'PU' | 'TITUS',
account_id: number, // auto-enriched by Datadog transport
country: 'de', // auto-enriched from ObservabilityState
timestamp: ISO string,
}
3. Event Naming Convention
Following existing patterns like TrackingEvent.KbvTiConnect, I recommend:
- TrackingEvent.TiCertificateConfigured - New enum value in tracking constants
- This aligns with existing TI-related events in the FIRE team namespace
4. Data Flow
User uploads .p12 file
→ onTiConnectDecryptKeyStoreListener
→ decryptKeyStore() extracts cert/privateKey
→ NEW: CertificateAnalyzer.analyze(cert)
→ Extract: certificateType, curveName
→ Get TI env from TiEnvRepository.tiEnvValue
→ sendCustomTrackingEventToObservability()
→ Datadog (auto-enriched with account_id, country, OS info)
Detailed Implementation Plan
Step 1: Create Certificate Analysis Utility
File: /src/modules/gematik-crypto/services/node/certificate/CertificateAnalyzer.ts (NEW)
// Responsibilities:
// - Parse PEM certificate using pkijs
// - Detect RSA vs ECC algorithm
// - Extract curve name for ECC certificates
// - Handle brainpoolP256r1 special case (OID 1.3.36.3.3.2.8.1.1.7)
// - Return { certificateType: 'ECC' | 'RSA', curveName?: string }
// Implementation approach:
// - Reuse logic from JWSUtils.deriveJWAFromCertificate()
// - Parse certificate from PEM format (Buffer.from(cert, 'base64'))
// - Use pkijs Certificate class
// - Check publicKey.algorithm.name for RSA/ECDSA
// - For ECC: extract publicKey.algorithm.namedCurve
// - Handle brainpoolP256r1 exception case
File: /src/modules/gematik-crypto/services/node/certificate/index.ts (NEW)
// Export CertificateAnalyzer
File: /src/modules/gematik-crypto/index.ts (MODIFY)
// Add export for CertificateAnalyzer
Step 2: Add Tracking Event Constant
File: /src/core/monitoring/constants/tracking.ts (MODIFY)
Add to TrackingEvent enum (around line 45, in FIRE section):
// FIRE
KbvTiConnect = 'kbv_ti_connect',
GematikQes = 'gematik_qes',
GematikSpecialistService = 'gematik-specialist-service',
TiCertificateConfigured = 'ti_certificate_configured', // NEW
Add to TrackingEventMap interface (after line 264):
[TrackingEvent.TiCertificateConfigured]: {
certificate_type: 'ECC' | 'RSA'
curve_name?: string
ti_environment: 'RU' | 'PU' | 'TITUS'
}
Step 3: Implement Certificate Tracking in TI Connect
File: /src/modules/kbv-ti-connect/main/ti-connect.ts (MODIFY)
Modify onTiConnectDecryptKeyStoreListener function (lines 98-152):
// 1. Add imports:
import { CertificateAnalyzer } from '@module/gematik-crypto'
import { TiEnvRepository } from '@module/gematik-ti-environment/services/tiEnvRepository'
import { sendCustomTrackingEventToObservability, TrackingEvent } from '@core/monitoring'
// 2. After successful decryption (after line 131), add:
try {
const certificateInfo = await CertificateAnalyzer.analyze(cert)
const tiEnvironment = TiEnvRepository.tiEnvValue
sendCustomTrackingEventToObservability(TrackingEvent.TiCertificateConfigured, {
certificate_type: certificateInfo.certificateType,
curve_name: certificateInfo.curveName,
ti_environment: tiEnvironment,
})
kbvTiConnectLogger.info(tiConnectModuleMetadata.name, {
extra: extraLogParser({
traceId,
functionName: 'onTiConnectDecryptKeyStoreListener',
operation: 'tracking',
direction: 'output',
data: JSON.stringify({
certificate_type: certificateInfo.certificateType,
curve_name: certificateInfo.curveName,
ti_environment: tiEnvironment,
}),
}),
})
} catch (error) {
// Log but don't fail the main flow
kbvTiConnectLogger.warn(tiConnectModuleMetadata.name, {
error,
extra: extraLogParser({
traceId,
functionName: 'onTiConnectDecryptKeyStoreListener',
operation: 'tracking',
direction: 'output',
error: error as Error,
}),
})
}
Step 4: Testing Strategy
Unit Tests:
-
File:
/src/modules/gematik-crypto/services/node/certificate/__tests__/CertificateAnalyzer.test.ts(NEW) - Test RSA certificate detection - Test ECC certificate detection (P-256, brainpoolP256r1) - Test error handling for invalid certificates - Use real certificate samples from existing test fixtures -
File:
/src/modules/kbv-ti-connect/main/__tests__/ti-connect.test.ts(MODIFY) - Add test case for certificate tracking on successful decryption - Mock CertificateAnalyzer.analyze() - Mock TiEnvRepository.tiEnvValue - Verify sendCustomTrackingEventToObservability called with correct payload - Verify tracking doesn't fail main certificate decryption flow
Integration Tests: - Manual testing with actual .p12 certificates (RSA and ECC) - Verify events appear in Datadog with correct structure - Test all TI environments: RU, PU, TITUS
Step 5: Datadog Dashboard Setup
Once events are flowing to Datadog, create dashboard with:
- Metric 1: Count of certificates by type (ECC vs RSA)
- Filter: trackingLabel: 'ti_certificate_configured'
- Group by: certificate_type
- Metric 2: Distribution of ECC curve types
- Filter:
certificate_type: 'ECC' -
Group by:
curve_name -
Metric 3: Certificate type by TI environment
- Filter:
trackingLabel: 'ti_certificate_configured' -
Group by:
ti_environment,certificate_type -
Metric 4: Trend over time
- Time series showing ECC adoption rate
Error Handling & Edge Cases
- Certificate Analysis Failure: Don't block TI configuration if tracking fails
- Missing TI Environment: Send event with
ti_environment: 'unknown'if not set - Invalid Certificate Format: Log warning, don't send tracking event
- Development Environment: Event sending is already disabled in dev/test via
isTest()check
Dependencies & Sequencing
Order of implementation: 1. Create CertificateAnalyzer utility (independent, can be …(truncated, 2311 more chars)