diff --git a/.env.demo b/.env.demo index aafeab62..5ce6f718 100644 --- a/.env.demo +++ b/.env.demo @@ -60,6 +60,14 @@ DCS_START_FROM_CURRENT_MONTH=true NODE_ENV=DEV +# Authentication type for trust-service calls. Supported: NoAuth | ClientAuth (defaults to NoAuth if not set) +TRUST_SERVICE_AUTH_TYPE= +# Full token endpoint URL for ClientAuth (e.g. http://host:5000/v1/orgs/{clientId}/token) +TRUST_SERVICE_TOKEN_URL= +# Client credentials used for trust-service authentication (ClientAuth only) +TRUST_SERVICE_CLIENT_ID= +TRUST_SERVICE_CLIENT_SECRET= +# Trust list URL — for NoAuth: GitHub/static JSON URL; for ClientAuth: trust-service base URL TRUST_LIST_URL= # Expiry is in seconds diff --git a/.env.sample b/.env.sample index 3983fe58..f5fb4426 100644 --- a/.env.sample +++ b/.env.sample @@ -42,15 +42,15 @@ INDICIO_TEST_GENESIS=`{"reqSignature":{},"txn":{"data":{"data":{"alias":"OpsNode {"reqSignature":{},"txn":{"data":{"data":{"alias":"lorica-identity-node1","blskey":"wUh24sVCQ8PHDgSb343g2eLxjD5vwxsrETfuV2sbwMNnYon9nhbaK5jcWTekvXtyiwxHxuiCCoZwKS97MQEAeC2oLbbMeKjYm212QwSnm7aKLEqTStXht35VqZvZLT7Q3mPQRYLjMGixdn4ocNHrBTMwPUQYycEqwaHWgE1ncDueXY","blskey_pop":"R2sMwF7UW6AaD4ALa1uB1YVPuP6JsdJ7LsUoViM9oySFqFt34C1x1tdHDysS9wwruzaaEFui6xNPqJ8eu3UBqcFKkoWhdsMqCALwe63ytxPwvtLtCffJLhHAcgrPC7DorXYdqhdG2cevdqc5oqFEAaKoFDBf12p5SsbbM4PYWCmVCb","client_ip":"35.225.220.151","client_port":"9702","node_ip":"35.224.26.110","node_port":"9701","services":["VALIDATOR"]},"dest":"k74ZsZuUaJEcB8RRxMwkCwdE5g1r9yzA3nx41qvYqYf"},"metadata":{"from":"Ex6hzsJFYzNJ7kzbfncNeU"},"type":"0"},"txnMetadata":{"seqNo":6,"txnId":"6880673ce4ae4a2352f103d2a6ae20469dd070f2027283a1da5e62a64a59d688"},"ver":"1"} {"reqSignature":{},"txn":{"data":{"data":{"alias":"cysecure-itn","blskey":"GdCvMLkkBYevRFi93b6qaj9G2u1W6Vnbg8QhRD1chhrWR8vRE8x9x7KXVeUBPFf6yW5qq2JCfA2frc8SGni2RwjtTagezfwAwnorLhVJqS5ZxTi4pgcw6smebnt4zWVhTkh6ugDHEypHwNQBcw5WhBZcEJKgNbyVLnHok9ob6cfr3u","blskey_pop":"RbH9mY7M5p3UB3oj4sT1skYwMkxjoUnja8eTYfcm83VcNbxC9zR9pCiRhk4q1dJT3wkDBPGNKnk2p83vaJYLcgMuJtzoWoJAWAxjb3Mcq8Agf6cgQpBuzBq2uCzFPuQCAhDS4Kv9iwA6FsRnfvoeFTs1hhgSJVxQzDWMVTVAD9uCqu","client_ip":"35.169.19.171","client_port":"9702","node_ip":"54.225.56.21","node_port":"9701","services":["VALIDATOR"]},"dest":"4ETBDmHzx8iDQB6Xygmo9nNXtMgq9f6hxGArNhQ6Hh3u"},"metadata":{"from":"uSXXXEdBicPHMMhr3ddNF"},"type":"0"},"txnMetadata":{"seqNo":7,"txnId":"3c21718b07806b2f193b35953dda5b68b288efd551dce4467ce890703d5ba549"},"ver":"1"}` -PLATFORM_BASE_URL= #CREDEBL BASE URL -#if the agent is dedicated -PLATFORM_DEDICATED_CLIENT_ID= -PLATFORM_DEDICATED_CLIENT_SECRET= -#If the agent is shared -PLATFORM_SHARED_AGENT_CLIENT_ID= -PLATFORM_SHARED_AGENT_CLIENT_SECRET= -#Trust service url to fetch trusted certificates for TLS pinning -TRUST_SERVICE_URL= +# Authentication type for trust-service calls. Supported: NoAuth | ClientAuth (defaults to NoAuth if not set) +TRUST_SERVICE_AUTH_TYPE= +# Full token endpoint URL for ClientAuth (e.g. http://host:5000/v1/orgs/{clientId}/token) +TRUST_SERVICE_TOKEN_URL= +# Client credentials used for trust-service authentication (ClientAuth only) +TRUST_SERVICE_CLIENT_ID= +TRUST_SERVICE_CLIENT_SECRET= +# Trust list URL — for NoAuth: GitHub/static JSON URL; for ClientAuth: trust-service base URL +TRUST_LIST_URL= APP_URL= AGENT_HTTP_URL= @@ -66,7 +66,6 @@ ROOT_CA_START_FROM_CURRENT_MONTH= DCS_START_FROM_CURRENT_MONTH= NODE_ENV= -TRUST_LIST_URL= # Expiry is in seconds OID4VCI_CRED_OFFER_EXPIRY=3600 diff --git a/src/cliAgent.ts b/src/cliAgent.ts index 25cecab8..51bab10e 100644 --- a/src/cliAgent.ts +++ b/src/cliAgent.ts @@ -65,7 +65,8 @@ import { IndicioAcceptanceMechanism, IndicioTransactionAuthorAgreement, Network, import { setupServer } from './server' import { generateSecretKey } from './utils/helpers' import { TsLogger } from './utils/logger' -import { getMixedCredentialRequestToCredentialMapper, getTrustedCerts } from './utils/oid4vc-agent' +import { getMixedCredentialRequestToCredentialMapper, getX509CertsByClientToken, getX509CertsByUrl } from './utils/oid4vc-agent' +import { AuthTypes, getAuthType } from './utils/auth' import { PolygonDidRegistrar, PolygonDidResolver, PolygonModule } from '@ayanworks/credo-polygon-w3c-module' export type Transports = 'ws' | 'http' @@ -272,14 +273,21 @@ const getModules = ( x509: new X509Module({ getTrustedCertificatesForVerification: async ( agentContext, - { certificateChain: _certificateChain, verification: _verification }, + { certificateChain, verification: _verification }, ) => { //TODO: We need to trust the certificate tenant wise, for that we need to fetch those details from platform const tenantId = agentContext.contextCorrelationId console.log('[getTrustedCertificatesForVerification] tenantId from agentContext:', tenantId) - const certs: string[] = await getTrustedCerts(tenantId) - return certs + const authType = getAuthType() + console.log('[getTrustedCertificatesForVerification] authType:', authType) + + if (authType === AuthTypes.ClientAuth) { + return await getX509CertsByClientToken(tenantId, certificateChain) + } + + // NoAuth: return all certs from the static trust list URL + return await getX509CertsByUrl() }, }), } diff --git a/src/controllers/auth/AuthController.ts b/src/controllers/auth/AuthController.ts index eba1e892..9f7c66d5 100644 --- a/src/controllers/auth/AuthController.ts +++ b/src/controllers/auth/AuthController.ts @@ -1,11 +1,9 @@ import axios from 'axios' import { Request as Req } from 'express' -import { Body, Controller, Get, Path, Post, Request, Route, Tags } from 'tsoa' +import { Body, Controller, Path, Post, Request, Route, Tags } from 'tsoa' import { injectable } from 'tsyringe' import { BadRequestError } from '../../errors' -import { fetchDedicatedX509Certificates, fetchSharedAgentX509Certificates } from '../../utils/helpers' -import { getTrustedCerts } from '../../utils/oid4vc-agent' interface OrgTokenRequest { clientId: string @@ -30,35 +28,17 @@ export class AuthController extends Controller { @Path('orgId') orgId: string, @Body() body: OrgTokenRequest, ): Promise { - const platformBaseUrl = process.env.PLATFORM_BASE_URL - if (!platformBaseUrl) { - throw new BadRequestError('PLATFORM_BASE_URL is not configured') + const trustServiceTokenUrl = process.env.TRUST_SERVICE_TOKEN_URL + if (!trustServiceTokenUrl) { + throw new BadRequestError('TRUST_SERVICE_TOKEN_URL is not configured') } const response = await axios.post( - `${platformBaseUrl}/v1/orgs/${orgId}/token`, + `${trustServiceTokenUrl}`, { clientId: body.clientId, clientSecret: body.clientSecret }, { headers: { 'Content-Type': 'application/json', accept: 'application/json' } }, ) return response.data } - // TODO: Remove these test endpoints after manual testing is done - @Get('/test/dedicated-x509-certificates') - public async testFetchDedicatedX509Certificates(@Request() _request: Req): Promise { - return fetchDedicatedX509Certificates() - } - - @Get('/test/shared-agent-x509-certificates') - public async testFetchSharedAgentX509Certificates(@Request() _request: Req): Promise { - return fetchSharedAgentX509Certificates() - } - - /** - * [TEMP] Manually trigger getTrustedCerts to test agent type detection and trust list fetch - */ - @Get('/test/trusted-certs') - public async testGetTrustedCerts(@Request() _request: Req): Promise { - return getTrustedCerts() - } } diff --git a/src/routes/routes.ts b/src/routes/routes.ts index 6af71e35..9bf88cb2 100644 --- a/src/routes/routes.ts +++ b/src/routes/routes.ts @@ -4051,111 +4051,6 @@ export function RegisterRoutes(app: Router) { } }); // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - const argsAuthController_testFetchDedicatedX509Certificates: Record = { - _request: {"in":"request","name":"_request","required":true,"dataType":"object"}, - }; - app.get('/v1/orgs/test/dedicated-x509-certificates', - ...(fetchMiddlewares(AuthController)), - ...(fetchMiddlewares(AuthController.prototype.testFetchDedicatedX509Certificates)), - - async function AuthController_testFetchDedicatedX509Certificates(request: ExRequest, response: ExResponse, next: any) { - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - let validatedArgs: any[] = []; - try { - validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_testFetchDedicatedX509Certificates, request, response }); - - const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; - - const controller: any = await container.get(AuthController); - if (typeof controller['setStatus'] === 'function') { - controller.setStatus(undefined); - } - - await templateService.apiHandler({ - methodName: 'testFetchDedicatedX509Certificates', - controller, - response, - next, - validatedArgs, - successStatus: undefined, - }); - } catch (err) { - return next(err); - } - }); - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - const argsAuthController_testFetchSharedAgentX509Certificates: Record = { - _request: {"in":"request","name":"_request","required":true,"dataType":"object"}, - }; - app.get('/v1/orgs/test/shared-agent-x509-certificates', - ...(fetchMiddlewares(AuthController)), - ...(fetchMiddlewares(AuthController.prototype.testFetchSharedAgentX509Certificates)), - - async function AuthController_testFetchSharedAgentX509Certificates(request: ExRequest, response: ExResponse, next: any) { - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - let validatedArgs: any[] = []; - try { - validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_testFetchSharedAgentX509Certificates, request, response }); - - const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; - - const controller: any = await container.get(AuthController); - if (typeof controller['setStatus'] === 'function') { - controller.setStatus(undefined); - } - - await templateService.apiHandler({ - methodName: 'testFetchSharedAgentX509Certificates', - controller, - response, - next, - validatedArgs, - successStatus: undefined, - }); - } catch (err) { - return next(err); - } - }); - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - const argsAuthController_testGetTrustedCerts: Record = { - _request: {"in":"request","name":"_request","required":true,"dataType":"object"}, - }; - app.get('/v1/orgs/test/trusted-certs', - ...(fetchMiddlewares(AuthController)), - ...(fetchMiddlewares(AuthController.prototype.testGetTrustedCerts)), - - async function AuthController_testGetTrustedCerts(request: ExRequest, response: ExResponse, next: any) { - - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa - - let validatedArgs: any[] = []; - try { - validatedArgs = templateService.getValidatedArgs({ args: argsAuthController_testGetTrustedCerts, request, response }); - - const container: IocContainer = typeof iocContainer === 'function' ? (iocContainer as IocContainerFactory)(request) : iocContainer; - - const controller: any = await container.get(AuthController); - if (typeof controller['setStatus'] === 'function') { - controller.setStatus(undefined); - } - - await templateService.apiHandler({ - methodName: 'testGetTrustedCerts', - controller, - response, - next, - validatedArgs, - successStatus: undefined, - }); - } catch (err) { - return next(err); - } - }); - // WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa const argsAgentController_getAgentInfo: Record = { request: {"in":"request","name":"request","required":true,"dataType":"object"}, }; diff --git a/src/routes/swagger.json b/src/routes/swagger.json index 787e08ae..ec6dd5e7 100644 --- a/src/routes/swagger.json +++ b/src/routes/swagger.json @@ -7677,82 +7677,6 @@ } } }, - "/v1/orgs/test/dedicated-x509-certificates": { - "get": { - "operationId": "TestFetchDedicatedX509Certificates", - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { - "items": { - "type": "string" - }, - "type": "array" - } - } - } - } - }, - "tags": [ - "Auth" - ], - "security": [], - "parameters": [] - } - }, - "/v1/orgs/test/shared-agent-x509-certificates": { - "get": { - "operationId": "TestFetchSharedAgentX509Certificates", - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { - "items": { - "type": "string" - }, - "type": "array" - } - } - } - } - }, - "tags": [ - "Auth" - ], - "security": [], - "parameters": [] - } - }, - "/v1/orgs/test/trusted-certs": { - "get": { - "operationId": "TestGetTrustedCerts", - "responses": { - "200": { - "description": "Ok", - "content": { - "application/json": { - "schema": { - "items": { - "type": "string" - }, - "type": "array" - } - } - } - } - }, - "description": "[TEMP] Manually trigger getTrustedCerts to test agent type detection and trust list fetch", - "tags": [ - "Auth" - ], - "security": [], - "parameters": [] - } - }, "/agent": { "get": { "operationId": "GetAgentInfo", diff --git a/src/server.ts b/src/server.ts index f94c23c6..75c315f6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -18,6 +18,7 @@ import { ValidateError } from 'tsoa' import { container } from 'tsyringe' import { setDynamicApiKey } from './authentication' +import { validateAuthConfig } from './utils/auth' import { ErrorMessages } from './enums' import { BaseError } from './errors/errors' import { basicMessageEvents } from './events/BasicMessageEvents' @@ -41,6 +42,7 @@ export const setupServer = async ( ) => { await otelSDK.start() agent.config.logger.info('OpenTelemetry SDK started') + validateAuthConfig() container.registerInstance(Agent, agent as Agent) fs.writeFileSync('config.json', JSON.stringify(config, null, 2)) diff --git a/src/utils/auth.ts b/src/utils/auth.ts new file mode 100644 index 00000000..f3f677da --- /dev/null +++ b/src/utils/auth.ts @@ -0,0 +1,57 @@ +export interface NoAuth { + type: 'NoAuth' + trustListUrl: string +} + +export interface ClientAuth { + type: 'ClientAuth' + trustServiceTokenUrl: string + trustListUrl: string + trustServiceClientId: string + trustServiceClientSecret: string +} + +export type Auth = NoAuth | ClientAuth + +export type AuthType = Auth['type'] + +export const AuthTypes = { + NoAuth: 'NoAuth', + ClientAuth: 'ClientAuth', +} as const satisfies Record + +const SUPPORTED_AUTH_TYPES = Object.values(AuthTypes) satisfies AuthType[] + +export function getAuthType(): AuthType { + const authType = process.env.TRUST_SERVICE_AUTH_TYPE as AuthType + if (!authType) { + console.warn('[getAuthType] TRUST_SERVICE_AUTH_TYPE is not set — defaulting to NoAuth') + return AuthTypes.NoAuth + } + if (!SUPPORTED_AUTH_TYPES.includes(authType)) { + throw new Error( + `TRUST_SERVICE_AUTH_TYPE '${authType}' is not supported. Supported types: ${SUPPORTED_AUTH_TYPES.join(', ')}`, + ) + } + return authType +} + +export function validateAuthConfig(): void { + const authType = getAuthType() + console.log('[validateAuthConfig] TRUST_SERVICE_AUTH_TYPE:', authType) + + const validators: Record void> = { + NoAuth: () => { + if (!process.env.TRUST_LIST_URL) throw new Error('[validateAuthConfig] TRUST_LIST_URL is required for NoAuth') + }, + ClientAuth: () => { + if (!process.env.TRUST_SERVICE_TOKEN_URL) throw new Error('[validateAuthConfig] TRUST_SERVICE_TOKEN_URL is required for ClientAuth') + if (!process.env.TRUST_LIST_URL) throw new Error('[validateAuthConfig] TRUST_LIST_URL is required for ClientAuth') + if (!process.env.TRUST_SERVICE_CLIENT_ID) throw new Error('[validateAuthConfig] TRUST_SERVICE_CLIENT_ID is required for ClientAuth') + if (!process.env.TRUST_SERVICE_CLIENT_SECRET) throw new Error('[validateAuthConfig] TRUST_SERVICE_CLIENT_SECRET is required for ClientAuth') + }, + } + + validators[authType]() + console.log('[validateAuthConfig] configuration valid for auth type:', authType) +} diff --git a/src/utils/constant.ts b/src/utils/constant.ts index 7ffe094c..737b1e54 100644 --- a/src/utils/constant.ts +++ b/src/utils/constant.ts @@ -3,6 +3,12 @@ import type { Curve } from '../controllers/types' import { KeyAlgorithm } from '@openwallet-foundation/askar-nodejs' export const X509_CERTIFICATE_RECORD = 'X509_CERTIFICATE' +export const TRUST_SERVICE_ENV_KEYS = { + TOKEN_URL: 'TRUST_SERVICE_TOKEN_URL', + CLIENT_ID: 'TRUST_SERVICE_CLIENT_ID', + CLIENT_SECRET: 'TRUST_SERVICE_CLIENT_SECRET', + TRUST_LIST_URL: 'TRUST_LIST_URL', +} as const export const keyAlgorithmToCurve: Partial> = { [KeyAlgorithm.Ed25519]: 'Ed25519', [KeyAlgorithm.X25519]: 'X25519', diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index ef33bafc..e94bb9a5 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -5,7 +5,29 @@ import { JsonEncoder, JsonTransformer } from '@credo-ts/core' import axios from 'axios' import { randomBytes } from 'crypto' -import { curveToKty, keyAlgorithmToCurve } from './constant' +import { TRUST_SERVICE_ENV_KEYS, curveToKty, keyAlgorithmToCurve } from './constant' +const TOKEN_EXPIRY_BUFFER_SECONDS = 60 +const tokenCache = new Map() + +function getTokenExpiry(token: string): number { + try { + const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64url').toString('utf-8')) + return typeof payload.exp === 'number' ? payload.exp : 0 + } catch { + return 0 + } +} + +function getCachedToken(clientId: string): string | null { + const cached = tokenCache.get(clientId) + if (!cached) return null + const nowSeconds = Math.floor(Date.now() / 1000) + if (nowSeconds < cached.expiresAt - TOKEN_EXPIRY_BUFFER_SECONDS) { + return cached.token + } + tokenCache.delete(clientId) + return null +} export function objectToJson(result: T) { const serialized = JsonTransformer.serialize(result) @@ -95,23 +117,28 @@ export function getTypeFromCurve(key: Curve | KeyAlgorithm): OkpType | EcType { } async function fetchPlatformToken( - platformBaseUrl: string, + tokenUrl: string, clientId: string, clientSecret: string, label: string, ): Promise { - if (!platformBaseUrl) throw new Error(`[${label}] platformBaseUrl is required`) + if (!tokenUrl) throw new Error(`[${label}] tokenUrl is required`) if (!clientId) throw new Error(`[${label}] clientId is required`) if (!clientSecret) throw new Error(`[${label}] clientSecret is required`) - const tokenUrl = `${platformBaseUrl}/v1/orgs/${clientId}/token` + const cachedToken = getCachedToken(clientId) + if (cachedToken) { + console.log(`[${label}] using cached token for clientId:`, clientId) + return cachedToken + } + console.log(`[${label}] fetching token from:`, tokenUrl) let tokenResponse try { tokenResponse = await axios.post( tokenUrl, - { clientSecret }, + { clientId, clientSecret }, { headers: { 'Content-Type': 'application/json', accept: 'application/json' } }, ) } catch (error) { @@ -139,118 +166,90 @@ async function fetchPlatformToken( throw new Error(`[${label}] access_token not found in platform response`) } + const expiresAt = getTokenExpiry(token) + tokenCache.set(clientId, { token, expiresAt }) + console.log(`[${label}] token cached for clientId:`, clientId, '| expires at:', new Date(expiresAt * 1000).toISOString()) + return token } -async function fetchTrustServiceCertificates( +async function checkTrustCertificatesExist( trustServiceUrl: string, - token: string, - ecosystemIds: string[], + x509: string[], label: string, -): Promise { - const certsUrl = `${trustServiceUrl}/api/x509-certificates/ecosystems` - console.log(`[${label}] fetching certificates from:`, certsUrl, 'ecosystemIds:', ecosystemIds) + tenantId?: string, + token?: string, +): Promise { + const matchUrl = trustServiceUrl + console.log(`[${label}] calling match API:`, matchUrl) + + const authHeaders = token ? { Authorization: `Bearer ${token}` } : {} try { - const certResponse = await axios.get(certsUrl, { - params: { ecosystemIds: ecosystemIds.join(',') }, - headers: { accept: 'application/json', Authorization: `Bearer ${token}` }, - }) + const matchResponse = await axios.post<{ matched: boolean }>( + matchUrl, + { x509, ...(tenantId && { tenantId }) }, + { headers: { 'Content-Type': 'application/json', accept: 'application/json', ...authHeaders } }, + ) - console.log(`[${label}] certificates response status:`, certResponse.status) - console.log(`[${label}] certificates response data:`, JSON.stringify(certResponse.data, null, 2)) + console.log(`[${label}] match response status:`, matchResponse.status) - if (!Array.isArray(certResponse.data) || certResponse.data.length === 0) { - throw new Error('No certificates returned from trust-service') - } + const isTrusted = matchResponse.data?.matched === true + console.log(`[${label}] isTrusted:`, isTrusted) - const certificates: string[] = certResponse.data.map((cert: { certificateData: string }) => cert.certificateData) - console.log(`[${label}] extracted certificates count:`, certificates.length) + if (!isTrusted) { + console.warn(`[${label}] certificate chain not trusted${tenantId ? ` for tenantId: ${tenantId}` : ''}`) + } - return certificates + return isTrusted } catch (error) { if (axios.isAxiosError(error)) { - console.error(`[${label}] certificates request failed:`, { + console.error(`[${label}] match request failed:`, { + url: matchUrl, status: error.response?.status, statusText: error.response?.statusText, data: error.response?.data, message: error.message, }) throw new Error( - `Failed to fetch certificates from trust-service: ${error.response?.status} ${JSON.stringify(error.response?.data)}`, + `[${label}] trust-service match request failed with status ${error.response?.status ?? 'no response'}: ${JSON.stringify(error.response?.data ?? error.message)}`, ) } throw error } } -export async function fetchDedicatedX509Certificates(): Promise { - const platformBaseUrl = process.env.PLATFORM_BASE_URL - const clientId = process.env.PLATFORM_DEDICATED_CLIENT_ID - const clientSecret = process.env.PLATFORM_DEDICATED_CLIENT_SECRET - const trustServiceUrl = process.env.TRUST_SERVICE_URL - - if (!platformBaseUrl) throw new Error('PLATFORM_BASE_URL is not configured') - if (!clientId) throw new Error('PLATFORM_DEDICATED_CLIENT_ID is not configured') - if (!clientSecret) throw new Error('PLATFORM_DEDICATED_CLIENT_SECRET is not configured') - if (!trustServiceUrl) throw new Error('TRUST_SERVICE_URL is not configured') - - const token = await fetchPlatformToken(platformBaseUrl, clientId, clientSecret, 'fetchDedicatedX509Certificates') - return fetchTrustServiceCertificates(trustServiceUrl, token, [], 'fetchDedicatedX509Certificates') -} - -export async function fetchSharedAgentX509Certificates(tenantId?: string): Promise { - const label = 'fetchSharedAgentX509Certificates' - - const platformBaseUrl = process.env.PLATFORM_BASE_URL - const clientId = process.env.PLATFORM_SHARED_AGENT_CLIENT_ID - const clientSecret = process.env.PLATFORM_SHARED_AGENT_CLIENT_SECRET - const resolvedTenantId = tenantId ?? process.env.PLATFORM_SHARED_AGENT_TENANT_ID - const trustServiceUrl = process.env.TRUST_SERVICE_URL - - if (!platformBaseUrl) throw new Error('PLATFORM_BASE_URL is not configured') - if (!clientId) throw new Error('PLATFORM_SHARED_AGENT_CLIENT_ID is not configured') - if (!clientSecret) throw new Error('PLATFORM_SHARED_AGENT_CLIENT_SECRET is not configured') - if (!resolvedTenantId) throw new Error('tenantId not provided and PLATFORM_SHARED_AGENT_TENANT_ID is not configured') - if (!trustServiceUrl) throw new Error('TRUST_SERVICE_URL is not configured') - console.log(`[${label}] starting certificate fetch for tenantId:`, resolvedTenantId) +export async function checkX509Certificates( + x509Certificates: string[], + isDedicated: boolean, + tenantId?: string, +): Promise { + const label = 'checkX509Certificates' - console.log(`[${label}] using tenantId:`, resolvedTenantId, tenantId ? '(from agent context)' : '(from .env)') - - const token = await fetchPlatformToken(platformBaseUrl, clientId, clientSecret, label) - - const ecosystemsUrl = `${platformBaseUrl}/v1/orgs/tenant/${resolvedTenantId}/ecosystems` - console.log(`[${label}] fetching ecosystem IDs from:`, ecosystemsUrl) - - let ecosystemIds: string[] - try { - const ecosystemResponse = await axios.get<{ statusCode: number; message: string; data: string[] }>(ecosystemsUrl, { - headers: { accept: 'application/json', Authorization: `Bearer ${token}` }, - }) + if (!x509Certificates || x509Certificates.length === 0) { + throw new Error(`[${label}] certificate chain is required but was not provided`) + } - console.log(`[${label}] ecosystem response status:`, ecosystemResponse.status) - console.log(`[${label}] ecosystem response data:`, JSON.stringify(ecosystemResponse.data, null, 2)) + const tokenUrl = process.env[TRUST_SERVICE_ENV_KEYS.TOKEN_URL] + const clientId = process.env[TRUST_SERVICE_ENV_KEYS.CLIENT_ID] + const clientSecret = process.env[TRUST_SERVICE_ENV_KEYS.CLIENT_SECRET] + const trustListUrl = process.env[TRUST_SERVICE_ENV_KEYS.TRUST_LIST_URL] + + if (!tokenUrl) throw new Error(`[${label}] ${TRUST_SERVICE_ENV_KEYS.TOKEN_URL} is not configured`) + if (!clientId) throw new Error(`[${label}] ${TRUST_SERVICE_ENV_KEYS.CLIENT_ID} is not configured`) + if (!clientSecret) throw new Error(`[${label}] ${TRUST_SERVICE_ENV_KEYS.CLIENT_SECRET} is not configured`) + if (!trustListUrl) throw new Error(`[${label}] ${TRUST_SERVICE_ENV_KEYS.TRUST_LIST_URL} is not configured`) + + let resolvedTenantId: string | undefined + if (!isDedicated) { + resolvedTenantId = tenantId + if (!resolvedTenantId) throw new Error(`[${label}] tenantId is required for shared agent but was not provided`) + console.log(`[${label}] using tenantId:`, resolvedTenantId) + } - ecosystemIds = ecosystemResponse.data.data - if (!Array.isArray(ecosystemIds) || ecosystemIds.length === 0) { - throw new Error(`No ecosystem IDs found for tenant: ${resolvedTenantId}`) - } + console.log(`[${label}] agent type: ${isDedicated ? 'dedicated' : 'shared'}, certificates:`, x509Certificates) - console.log(`[${label}] ecosystem IDs:`, ecosystemIds) - } catch (error) { - if (axios.isAxiosError(error)) { - console.error(`[${label}] ecosystem IDs request failed:`, { - status: error.response?.status, - statusText: error.response?.statusText, - data: error.response?.data, - message: error.message, - }) - throw new Error( - `Failed to fetch ecosystem IDs from platform: ${error.response?.status} ${JSON.stringify(error.response?.data)}`, - ) - } - throw error - } + const token = await fetchPlatformToken(tokenUrl, clientId, clientSecret, label) - return fetchTrustServiceCertificates(trustServiceUrl, token, ecosystemIds, label) + return checkTrustCertificatesExist(trustListUrl, x509Certificates, label, resolvedTenantId, token) } diff --git a/src/utils/oid4vc-agent.ts b/src/utils/oid4vc-agent.ts index a07d5abc..e3e31350 100644 --- a/src/utils/oid4vc-agent.ts +++ b/src/utils/oid4vc-agent.ts @@ -1,9 +1,4 @@ import type { SdJwtVcHolderBinding } from '@credo-ts/core' -import type { DisclosureFrame } from '../controllers/types' -import { Agent, CredoError } from '@credo-ts/core' -import { container } from 'tsyringe' - -import { fetchDedicatedX509Certificates, fetchSharedAgentX509Certificates } from './helpers' import type { OpenId4VcCredentialHolderBinding, OpenId4VcCredentialHolderDidBinding, @@ -12,11 +7,18 @@ import type { OpenId4VciSignSdJwtCredentials, } from '@credo-ts/openid4vc' -import { DidsApi, X509Certificate, X509Service } from '@credo-ts/core' -import { ClaimFormat, X509ModuleConfig } from '@credo-ts/core' +import { Agent, ClaimFormat, CredoError, DidsApi, LogLevel, X509Certificate, X509ModuleConfig, X509Service } from '@credo-ts/core' import { OpenId4VciCredentialFormatProfile } from '@credo-ts/openid4vc' +import { container } from 'tsyringe' + +import type { DisclosureFrame } from '../controllers/types' import { SignerMethod } from '../enums/enum' +import { validateAuthConfig } from './auth' +import { checkX509Certificates } from './helpers' +import { TsLogger } from './logger' + +const logger = new TsLogger(LogLevel.info) export function getMixedCredentialRequestToCredentialMapper(): OpenId4VciCredentialRequestToCredentialMapper { return async ({ @@ -234,31 +236,84 @@ export interface OpenId4VcIssuanceSessionCreateOfferSdJwtCredentialOptions { disclosureFrame: DisclosureFrame } -export async function getTrustedCerts(tenantId?: string): Promise { - try { - const agent = container.resolve(Agent) - if (!agent) { - console.error('[getTrustedCerts] agent not available in container') - return [] - } +async function verifyX509CertificateTrust( + certificateChain: X509Certificate[], + isDedicated: boolean, + tenantId?: string, +): Promise { + const x509Certificates = certificateChain.map((cert) => cert.toString('base64')) + return checkX509Certificates(x509Certificates, isDedicated, tenantId) +} - const isDedicated = !('tenants' in agent.modules) - console.log('[getTrustedCerts] agent type:', isDedicated ? 'dedicated' : 'shared') +export async function getTrustedCerts(params: { + certificateChain: X509Certificate[] + tenantId?: string +}): Promise { + const { tenantId, certificateChain } = params - let certs: string[] - if (isDedicated) { - certs = await fetchDedicatedX509Certificates() - } else { - certs = await fetchSharedAgentX509Certificates(tenantId) - } + const agent = container.resolve(Agent) + if (!agent) { + throw new Error('[getTrustedCerts] agent not available in container') + } - if (!Array.isArray(certs) || certs.length === 0) { - console.warn('[getTrustedCerts] no certificates returned') - return [] - } - return certs - } catch (error) { - console.error('[getTrustedCerts] failed:', error instanceof Error ? error.message : error) + if (certificateChain.length === 0) { + throw new Error('[getTrustedCerts] certificate chain is required but was not provided') + } + + const isDedicated = !('tenants' in agent.modules) + logger.info(`[getTrustedCerts] agent type: ${isDedicated ? 'dedicated' : 'shared'}`) + + if (!isDedicated && !tenantId) { + throw new Error('[getTrustedCerts] tenantId is required for shared agents') + } + + const isTrusted = await verifyX509CertificateTrust(certificateChain, isDedicated, tenantId) + if (!isTrusted) { + logger.warn(`[getTrustedCerts] certificate chain not trusted${isDedicated ? '' : ` for tenantId: ${tenantId}`}`) + } + + return isTrusted +} + +/** + * ClientAuth flow: verifies the certificate chain against the trust-service using a platform token. + * Returns the PEM certs if trusted, empty array if not. + */ +export async function getX509CertsByClientToken( + tenantId: string, + certificateChain: X509Certificate[], +): Promise { + const isTrusted = await getTrustedCerts({ certificateChain, tenantId }) + + if (!isTrusted) { + logger.warn(`[getX509CertsByClientToken] certificate chain not trusted for tenantId: ${tenantId}`) return [] } + + return certificateChain.map((cert) => cert.toString('pem')) } + +export async function getX509CertsByUrl(): Promise { + const trustListUrl = process.env.TRUST_LIST_URL + if (!trustListUrl) throw new Error('[getX509CertsByUrl] TRUST_LIST_URL is not configured') + + logger.info(`[getX509CertsByUrl] fetching trust list from: ${trustListUrl}`) + + const response = await fetch(trustListUrl) + + if (!response.ok) { + throw new Error(`[getX509CertsByUrl] failed to fetch trust list: HTTP ${response.status}`) + } + + const data = await response.json() + + if (!Array.isArray(data) || data.length === 0) { + throw new Error('[getX509CertsByUrl] trust list is empty or invalid') + } + + logger.info(`[getX509CertsByUrl] fetched certificates count: ${data.length}`) + + return data as string[] +} + +export { validateAuthConfig }