Skip to content

Commit ab96aff

Browse files
feat: Add jks to supported formats (#6153)
1 parent fd016ea commit ab96aff

File tree

5 files changed

+89
-13
lines changed

5 files changed

+89
-13
lines changed

.changeset/legal-pandas-yell.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@sap-cloud-sdk/connectivity': minor
3+
---
4+
5+
[New Functionality] Support certificates in JKS format for `ClientCertificateAuthentication`.

packages/connectivity/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@sap/xssec": "^4.12.1",
4747
"async-retry": "^1.3.3",
4848
"axios": "^1.13.2",
49+
"jks-js": "^1.1.4",
4950
"jsonwebtoken": "^9.0.3"
5051
},
5152
"devDependencies": {

packages/connectivity/src/http-agent/http-agent.spec.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import { X509Certificate } from 'node:crypto';
22
import mock from 'mock-fs';
33
import { createLogger } from '@sap-cloud-sdk/util';
4+
5+
// Mock jks-js module
6+
jest.mock('jks-js', () => ({
7+
toPem: jest.fn()
8+
}));
9+
import * as jks from 'jks-js';
410
import { registerDestinationCache } from '../scp-cf/destination/register-destination-cache';
511
import { certAsString } from '../../../../test-resources/test/test-util/test-certificate';
612
import { getAgentConfig } from './http-agent';
@@ -172,7 +178,17 @@ describe('createAgent', () => {
172178
).toMatchObject(expectedOptions);
173179
});
174180

175-
it('throws an error if the format is not supported', async () => {
181+
it('does not throw an error for supported JKS format', async () => {
182+
const mockPemKeystore = {
183+
alias1: {
184+
cert: '-----BEGIN CERTIFICATE-----\nMII...\n-----END CERTIFICATE-----',
185+
key: '-----BEGIN PRIVATE KEY-----\nMII...\n-----END PRIVATE KEY-----'
186+
}
187+
};
188+
(jks.toPem as jest.MockedFunction<typeof jks.toPem>).mockReturnValue(
189+
mockPemKeystore
190+
);
191+
176192
const destination: HttpDestination = {
177193
url: 'https://destination.example.com',
178194
authentication: 'ClientCertificateAuthentication',
@@ -187,8 +203,34 @@ describe('createAgent', () => {
187203
]
188204
};
189205

206+
const expectedOptions = {
207+
rejectUnauthorized: true,
208+
cert: Buffer.from(mockPemKeystore['alias1'].cert, 'utf8'),
209+
key: Buffer.from(mockPemKeystore['alias1'].key, 'utf8')
210+
};
211+
212+
expect(
213+
(await getAgentConfig(destination))['httpsAgent']['options']
214+
).toMatchObject(expectedOptions);
215+
});
216+
217+
it('throws an error if the format is not supported', async () => {
218+
const destination: HttpDestination = {
219+
url: 'https://destination.example.com',
220+
authentication: 'ClientCertificateAuthentication',
221+
keyStoreName: 'cert.unknown',
222+
keyStorePassword: 'password',
223+
certificates: [
224+
{
225+
name: 'cert.unknown',
226+
content: 'base64string',
227+
type: 'CERTIFICATE'
228+
}
229+
]
230+
};
231+
190232
expect(async () => getAgentConfig(destination)).rejects.toThrow(
191-
"The format of the provided certificate 'cert.jks' is not supported. Supported formats are: p12, pfx, pem. You can convert Java Keystores (.jks, .keystore) into PKCS#12 keystores using the JVM's keytool CLI: keytool -importkeystore -srckeystore your-keystore.jks -destkeystore your-keystore.p12 -deststoretype pkcs12"
233+
"The format of the provided certificate 'cert.unknown' is not supported. Supported formats are: p12, pfx, pem, jks, keystore."
192234
);
193235
});
194236

packages/connectivity/src/http-agent/http-agent.ts

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { readFile } from 'fs/promises';
22
import http from 'http';
33
import https from 'https';
4+
import * as jks from 'jks-js';
45
import { createLogger, last } from '@sap-cloud-sdk/util';
56
/* Careful the proxy imports cause circular dependencies if imported from scp directly */
67
// eslint-disable-next-line import/no-internal-modules
@@ -132,6 +133,37 @@ function getKeyStoreOptions(destination: Destination):
132133

133134
const certBuffer = Buffer.from(certificate.content, 'base64');
134135

136+
if (
137+
getFormat(certificate) === 'jks' ||
138+
getFormat(certificate) === 'keystore'
139+
) {
140+
const pemKeystore = jks.toPem(
141+
certBuffer,
142+
destination.keyStorePassword || ''
143+
);
144+
const aliases = Object.keys(pemKeystore);
145+
if (aliases.length === 0) {
146+
throw Error('No entries found in JKS keystore');
147+
}
148+
const alias = aliases[0];
149+
150+
if (aliases.length > 1) {
151+
logger.debug(
152+
`JKS keystore contains ${aliases.length} aliases. ` +
153+
'Using the first one. ' +
154+
'If this is not the correct certificate, please use a JKS file with only one entry.'
155+
);
156+
}
157+
158+
const entry = pemKeystore[alias];
159+
if (!entry.cert || !entry.key) {
160+
throw Error('Invalid JKS entry: missing cert or key');
161+
}
162+
return {
163+
cert: Buffer.from(entry.cert, 'utf8'),
164+
key: Buffer.from(entry.key, 'utf8')
165+
};
166+
}
135167
// if the format is pem, the key and certificate needs to be passed separately
136168
// it could be required to separate the string into two parts, but this seems to work as well
137169
if (getFormat(certificate) === 'pem') {
@@ -207,7 +239,7 @@ function mtlsIsEnabled(destination: Destination) {
207239
/*
208240
The node client supports only these store formats https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions.
209241
*/
210-
const supportedCertificateFormats = ['p12', 'pfx', 'pem'];
242+
const supportedCertificateFormats = ['p12', 'pfx', 'pem', 'jks', 'keystore'];
211243

212244
function isSupportedFormat(format: string | undefined): boolean {
213245
return !!format && supportedCertificateFormats.includes(format);
@@ -235,15 +267,7 @@ function validateFormat(certificate: DestinationCertificate) {
235267
const format = getFormat(certificate);
236268
if (!isSupportedFormat(format)) {
237269
throw Error(
238-
`The format of the provided certificate '${
239-
certificate.name
240-
}' is not supported. Supported formats are: ${supportedCertificateFormats.join(
241-
', '
242-
)}. ${
243-
format && ['jks', 'keystore'].includes(format)
244-
? "You can convert Java Keystores (.jks, .keystore) into PKCS#12 keystores using the JVM's keytool CLI: keytool -importkeystore -srckeystore your-keystore.jks -destkeystore your-keystore.p12 -deststoretype pkcs12"
245-
: ''
246-
}`
270+
`The format of the provided certificate '${certificate.name}' is not supported. Supported formats are: ${supportedCertificateFormats.join(', ')}.`
247271
);
248272
}
249273
}

packages/util/src/error-with-cause.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ export class ErrorWithCause extends Error {
3838
let response = '';
3939
if (cause.response?.data) {
4040
try {
41-
response = `${unixEOL}${JSON.stringify(cause.response?.data, null, 2)}`;
41+
response = `${unixEOL}${JSON.stringify(
42+
cause.response?.data,
43+
null,
44+
2
45+
)}`;
4246
} catch (error) {
4347
logger.warn(`Failed to stringify response data: ${error.message}`);
4448
response = `${unixEOL}${cause.response?.data}`;

0 commit comments

Comments
 (0)