From 44a1c9f67ac86c0066da33b1865bd6aa9550619f Mon Sep 17 00:00:00 2001 From: Jhonatan Sandoval Velasco <122501764+JhontSouth@users.noreply.github.com> Date: Thu, 7 Dec 2023 12:24:00 -0500 Subject: [PATCH] feat: Extraction of thumbprint value through the certificate (#4580) * extract thumbprint from cert value * fix documentation * keep previous constructor with new structure * include validation to avoid undefined.trim() --- libraries/botframework-connector/package.json | 3 +- ...tificateServiceClientCredentialsFactory.ts | 57 ++++++++++++++++--- yarn.lock | 5 ++ 3 files changed, 57 insertions(+), 8 deletions(-) diff --git a/libraries/botframework-connector/package.json b/libraries/botframework-connector/package.json index 5ca4e70ea1..f6c2d51294 100644 --- a/libraries/botframework-connector/package.json +++ b/libraries/botframework-connector/package.json @@ -37,7 +37,8 @@ "cross-fetch": "^3.0.5", "jsonwebtoken": "^9.0.0", "rsa-pem-from-mod-exp": "^0.8.4", - "zod": "^3.22.4" + "zod": "^3.22.4", + "openssl-wrapper": "^0.3.4" }, "devDependencies": { "@types/jsonwebtoken": "8.3.5", diff --git a/libraries/botframework-connector/src/auth/certificateServiceClientCredentialsFactory.ts b/libraries/botframework-connector/src/auth/certificateServiceClientCredentialsFactory.ts index 65b0682751..407cb62b63 100644 --- a/libraries/botframework-connector/src/auth/certificateServiceClientCredentialsFactory.ts +++ b/libraries/botframework-connector/src/auth/certificateServiceClientCredentialsFactory.ts @@ -10,6 +10,9 @@ import type { ServiceClientCredentials } from '@azure/ms-rest-js'; import { ServiceClientCredentialsFactory } from './serviceClientCredentialsFactory'; import { ok } from 'assert'; import { CertificateAppCredentials } from './certificateAppCredentials'; +import { promisify } from 'util'; +import * as opensslWrapper from 'openssl-wrapper'; +const openssl = promisify(opensslWrapper.default); /** * A Certificate implementation of the [ServiceClientCredentialsFactory](xref:botframework-connector.ServiceClientCredentialsFactory) abstract class. @@ -37,23 +40,51 @@ export class CertificateServiceClientCredentialsFactory extends ServiceClientCre certificatePrivateKey: string, tenantId?: string, x5c?: string + ); + + /** + * Initializes a new instance of the CertificateServiceClientCredentialsFactory class. + * + * @param appId Microsoft application Id related to the certificate. + * @param x5c Value that enables application developers to achieve easy certificates roll-over in Azure AD + * set this parameter to send the public certificate (BEGIN CERTIFICATE) to Azure AD, so that Azure AD can use it to validate the subject name based on a trusted issuer policy. + * @param certificatePrivateKey A PEM encoded certificate private key. + * @param tenantId Optional. The oauth token tenant. + */ + constructor(appId: string, x5c: string, certificatePrivateKey: string, tenantId?: string); + + /** + * @internal + */ + constructor( + appId: string, + certificateThumbprintOrx5c: string, + certificatePrivateKey: string, + tenantId?: string, + x5c?: string ) { super(); + ok(appId?.trim(), 'CertificateServiceClientCredentialsFactory.constructor(): missing appId.'); - ok( - certificateThumbprint?.trim(), - 'CertificateServiceClientCredentialsFactory.constructor(): missing certificateThumbprint.' - ); ok( certificatePrivateKey?.trim(), 'CertificateServiceClientCredentialsFactory.constructor(): missing certificatePrivateKey.' ); + if (certificateThumbprintOrx5c?.includes('-----BEGIN CERTIFICATE-----')) { + this.x5c = certificateThumbprintOrx5c; + } else { + ok( + certificateThumbprintOrx5c?.trim(), + 'CertificateServiceClientCredentialsFactory.constructor(): missing certificateThumbprint or x5c value.' + ); + this.certificateThumbprint = certificateThumbprintOrx5c; + this.x5c = x5c; + } + this.appId = appId; - this.certificateThumbprint = certificateThumbprint; this.certificatePrivateKey = certificatePrivateKey; this.tenantId = tenantId; - this.x5c = x5c; } /** @@ -63,6 +94,18 @@ export class CertificateServiceClientCredentialsFactory extends ServiceClientCre return appId === this.appId; } + /** + * @param cert Value with the certificate content. + * @returns The thumbprint value calculated from the cert content. + */ + private async getThumbprint(cert) { + const fingerprintResponse = await openssl('x509', Buffer.from(cert), { fingerprint: true, noout: true }); + return Buffer.from(fingerprintResponse) + .toString() + .replace(/^.*Fingerprint=/, '') + .replace(/:/g, '') + .trim(); + } /** * @inheritdoc */ @@ -82,7 +125,7 @@ export class CertificateServiceClientCredentialsFactory extends ServiceClientCre return new CertificateAppCredentials( this.appId, - this.certificateThumbprint, + this.certificateThumbprint ?? (await this.getThumbprint(this.x5c)), this.certificatePrivateKey, this.tenantId, audience, diff --git a/yarn.lock b/yarn.lock index e7a453183e..916ca71cc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10254,6 +10254,11 @@ open@8.4.0, open@^8.0.0: is-docker "^2.1.1" is-wsl "^2.2.0" +openssl-wrapper@^0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/openssl-wrapper/-/openssl-wrapper-0.3.4.tgz#c01ec98e4dcd2b5dfe0b693f31827200e3b81b07" + integrity sha512-iITsrx6Ho8V3/2OVtmZzzX8wQaKAaFXEJQdzoPUZDtyf5jWFlqo+h+OhGT4TATQ47f9ACKHua8nw7Qoy85aeKQ== + optionator@^0.8.1: version "0.8.3" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.3.tgz#84fa1d036fe9d3c7e21d99884b601167ec8fb495"