diff --git a/.changeset/tasty-shoes-joke.md b/.changeset/tasty-shoes-joke.md new file mode 100644 index 0000000000..8db3ff13f7 --- /dev/null +++ b/.changeset/tasty-shoes-joke.md @@ -0,0 +1,5 @@ +--- +"@sap-cloud-sdk/connectivity": minor +--- + +[New Functionality] Support certificates in PEM format for `ClientCertificateAuthentication`. diff --git a/packages/connectivity/src/http-agent/http-agent.ts b/packages/connectivity/src/http-agent/http-agent.ts index fc85dca925..41a05ec0e6 100644 --- a/packages/connectivity/src/http-agent/http-agent.ts +++ b/packages/connectivity/src/http-agent/http-agent.ts @@ -114,24 +114,29 @@ function getTrustStoreOptions(destination: HttpDestination): { /** * @internal - * The http agents (proxy and default) use node tls for the certificate handling. This method creates the options with the pfx and passphrase. + * The http agent uses node tls for the certificate handling. This method creates the options with the pfx and passphrase or key, cert and passphrase, depending on the format of the certificate. * https://nodejs.org/api/tls.html#tls_tls_createsecurecontext_options - * @param destination - Destination object + * @param destination - Destination object. * @returns Options, which can be used later by tls.createSecureContext() e.g. pfx and passphrase or an empty object, if the protocol is not 'https:' or no client information are in the definition. */ -function getKeyStoreOptions(destination: Destination): { - pfx?: Buffer; - passphrase?: string; -} { +function getKeyStoreOptions(destination: Destination): + | { + pfx?: Buffer; + passphrase?: string; + } + | { + cert?: Buffer; + key?: Buffer; + passphrase?: string; + } { if ( - // Only add certificates, when using MTLS (https://github.com/SAP/cloud-sdk-js/issues/3544) + // Only add certificates, when using ClientCertificateAuthentication (https://github.com/SAP/cloud-sdk-js/issues/3544) destination.authentication === 'ClientCertificateAuthentication' && - // pfx is an alternative to providing key and cert individually - // For mTLS we provide key and cert, in non-mTLS cases we provide pfx !mtlsIsEnabled(destination) && destination.keyStoreName ) { const certificate = selectCertificate(destination); + validateFormat(certificate); logger.debug(`Certificate with name "${certificate.name}" selected.`); @@ -140,8 +145,21 @@ function getKeyStoreOptions(destination: Destination): { `Destination '${destination.name}' does not have a keystore password.` ); } + + const certBuffer = Buffer.from(certificate.content, 'base64'); + + // if the format is pem, the key and certificate needs to be passed separately + // it could be required to separate the string into two parts, but this seems to work as well + if (getFormat(certificate) === 'pem') { + return { + cert: certBuffer, + key: certBuffer, + passphrase: destination.keyStorePassword + }; + } + // pfx is a format that combines key and cert return { - pfx: Buffer.from(certificate.content, 'base64'), + pfx: certBuffer, passphrase: destination.keyStorePassword }; } @@ -169,7 +187,10 @@ export interface MtlsOptions { async function getMtlsOptions( destination: Destination ): Promise> { - if (!mtlsIsEnabled(destination) && destination.mtls) { + if ( + destination.mtls && + !(process.env.CF_INSTANCE_CERT && process.env.CF_INSTANCE_KEY) + ) { logger.warn( `Destination ${ destination.name ? destination.name : '' @@ -202,14 +223,10 @@ function mtlsIsEnabled(destination: Destination) { /* The node client supports only these store formats https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions. */ -const supportedCertificateFormats = ['p12', 'pfx']; +const supportedCertificateFormats = ['p12', 'pfx', 'pem']; -function hasSupportedFormat(certificate: DestinationCertificate): boolean { - const certificateFormat = last(certificate.name.split('.')); - if (certificateFormat) { - return supportedCertificateFormats.includes(certificateFormat); - } - return false; +function isSupportedFormat(format: string | undefined): boolean { + return !!format && supportedCertificateFormats.includes(format); } function selectCertificate(destination): DestinationCertificate { @@ -223,8 +240,16 @@ function selectCertificate(destination): DestinationCertificate { ); } - if (!hasSupportedFormat(certificate)) { - const format: string | undefined = last(certificate.name.split('.')); + return certificate; +} + +function getFormat(certificate: DestinationCertificate): string | undefined { + return last(certificate.name.split('.')); +} + +function validateFormat(certificate: DestinationCertificate) { + const format = getFormat(certificate); + if (!isSupportedFormat(format)) { throw Error( `The format of the provided certificate '${ certificate.name @@ -237,8 +262,6 @@ function selectCertificate(destination): DestinationCertificate { }` ); } - - return certificate; } /** diff --git a/packages/http-client/src/http-client.ts b/packages/http-client/src/http-client.ts index 44e1917c0a..73b06d708b 100644 --- a/packages/http-client/src/http-client.ts +++ b/packages/http-client/src/http-client.ts @@ -142,8 +142,8 @@ export function buildHttpRequestConfigWithOrigin( } /** - * This method does nothing and is only there to indicated that the call was made by Odata or OpenApi client and encoding is already done on filter and key parameters. - * @param params - Parameters which are returned + * This method does nothing and is only there to indicate that the call was made by an OData or OpenApi client and encoding is already done on filter and key parameters. + * @param params - Parameters which are returned. * @returns The parameters as they are without encoding. * @internal */