Skip to content

Commit c4bb5cc

Browse files
authored
feat(client): Allow specifying grpc CallCredentials (#1261)
1 parent 35c6005 commit c4bb5cc

File tree

7 files changed

+407
-13
lines changed

7 files changed

+407
-13
lines changed

packages/client/src/connection.ts

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,21 +21,47 @@ export interface ConnectionOptions {
2121
address?: string;
2222

2323
/**
24-
* TLS configuration.
25-
* Pass a falsy value to use a non-encrypted connection or `true` or `{}` to
26-
* connect with TLS without any customization.
24+
* TLS configuration. Pass a falsy value to use a non-encrypted connection,
25+
* or `true` or `{}` to connect with TLS without any customization.
26+
*
27+
* For advanced scenario, a prebuilt {@link grpc.ChannelCredentials} object
28+
* may instead be specified using the {@link credentials} property.
2729
*
2830
* Either {@link credentials} or this may be specified for configuring TLS
31+
*
32+
* @default TLS is disabled
2933
*/
3034
tls?: TLSConfig | boolean | null;
3135

3236
/**
33-
* Channel credentials, create using the factory methods defined {@link https://grpc.github.io/grpc/node/grpc.credentials.html | here}
37+
* gRPC channel credentials.
38+
*
39+
* `ChannelCredentials` are things like SSL credentials that can be used to secure a connection.
40+
* There may be only one `ChannelCredentials`. They can be created using some of the factory
41+
* methods defined {@link https://grpc.github.io/grpc/node/grpc.credentials.html | here}
42+
*
43+
* Specifying a prebuilt `ChannelCredentials` should only be required for advanced use cases.
44+
* For simple TLS use cases, using the {@link tls} property is recommended. To register
45+
* `CallCredentials` (eg. metadata-based authentication), use the {@link callCredentials} property.
3446
*
3547
* Either {@link tls} or this may be specified for configuring TLS
3648
*/
3749
credentials?: grpc.ChannelCredentials;
3850

51+
/**
52+
* gRPC call credentials.
53+
*
54+
* `CallCredentials` generaly modify metadata; they can be attached to a connection to affect all method
55+
* calls made using that connection. They can be created using some of the factory methods defined
56+
* {@link https://grpc.github.io/grpc/node/grpc.credentials.html | here}
57+
*
58+
* If `callCredentials` are specified, they will be composed with channel credentials
59+
* (either the one created implicitely by using the {@link tls} option, or the one specified
60+
* explicitly through {@link credentials}). Notice that gRPC doesn't allow registering
61+
* `callCredentials` on insecure connections.
62+
*/
63+
callCredentials?: grpc.CallCredentials[];
64+
3965
/**
4066
* GRPC Channel arguments
4167
*
@@ -84,7 +110,9 @@ export interface ConnectionOptions {
84110
connectTimeout?: Duration;
85111
}
86112

87-
export type ConnectionOptionsWithDefaults = Required<Omit<ConnectionOptions, 'tls' | 'connectTimeout'>> & {
113+
export type ConnectionOptionsWithDefaults = Required<
114+
Omit<ConnectionOptions, 'tls' | 'connectTimeout' | 'callCredentials'>
115+
> & {
88116
connectTimeoutMs: number;
89117
};
90118

@@ -114,7 +142,7 @@ function addDefaults(options: ConnectionOptions): ConnectionOptionsWithDefaults
114142
* - Add default port to address if port not specified
115143
*/
116144
function normalizeGRPCConfig(options?: ConnectionOptions): ConnectionOptions {
117-
const { tls: tlsFromConfig, credentials, ...rest } = options || {};
145+
const { tls: tlsFromConfig, credentials, callCredentials, ...rest } = options || {};
118146
if (rest.address) {
119147
// eslint-disable-next-line prefer-const
120148
let [host, port] = rest.address.split(':', 2);
@@ -128,10 +156,9 @@ function normalizeGRPCConfig(options?: ConnectionOptions): ConnectionOptions {
128156
}
129157
return {
130158
...rest,
131-
credentials: grpc.credentials.createSsl(
132-
tls.serverRootCACertificate,
133-
tls.clientCertPair?.key,
134-
tls.clientCertPair?.crt
159+
credentials: grpc.credentials.combineChannelCredentials(
160+
grpc.credentials.createSsl(tls.serverRootCACertificate, tls.clientCertPair?.key, tls.clientCertPair?.crt),
161+
...(callCredentials ?? [])
135162
),
136163
channelArgs: {
137164
...rest.channelArgs,
@@ -144,7 +171,13 @@ function normalizeGRPCConfig(options?: ConnectionOptions): ConnectionOptions {
144171
},
145172
};
146173
} else {
147-
return rest;
174+
return {
175+
...rest,
176+
credentials: grpc.credentials.combineChannelCredentials(
177+
credentials ?? grpc.credentials.createInsecure(),
178+
...(callCredentials ?? [])
179+
),
180+
};
148181
}
149182
}
150183

packages/test/src/test-client-connection.ts

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import util from 'node:util';
22
import path from 'node:path';
3+
import fs from 'node:fs/promises';
34
import test from 'ava';
45
import * as grpc from '@grpc/grpc-js';
56
import * as protoLoader from '@grpc/proto-loader';
@@ -20,6 +21,23 @@ async function bindLocalhost(server: grpc.Server): Promise<number> {
2021
return await util.promisify(server.bindAsync.bind(server))('127.0.0.1:0', grpc.ServerCredentials.createInsecure());
2122
}
2223

24+
async function bindLocalhostTls(server: grpc.Server): Promise<number> {
25+
const caCert = await fs.readFile(path.resolve(__dirname, `../tls_certs/test-ca.crt`));
26+
const serverChainCert = await fs.readFile(path.resolve(__dirname, `../tls_certs/test-server-chain.crt`));
27+
const serverKey = await fs.readFile(path.resolve(__dirname, `../tls_certs/test-server.key`));
28+
const credentials = grpc.ServerCredentials.createSsl(
29+
caCert,
30+
[
31+
{
32+
cert_chain: serverChainCert,
33+
private_key: serverKey,
34+
},
35+
],
36+
true
37+
);
38+
return await util.promisify(server.bindAsync.bind(server))('localhost:0', credentials);
39+
}
40+
2341
test('withMetadata / withDeadline set the CallContext for RPC call', async (t) => {
2442
const server = new grpc.Server();
2543
let gotTestHeaders = false;
@@ -32,7 +50,7 @@ test('withMetadata / withDeadline set the CallContext for RPC call', async (t) =
3250
temporal.api.workflowservice.v1.IRegisterNamespaceRequest,
3351
temporal.api.workflowservice.v1.IRegisterNamespaceResponse
3452
>,
35-
callback: grpc.sendUnaryData<temporal.api.workflowservice.v1.IRegisterNamespaceResponse>
53+
callback: grpc.sendUnaryData<temporal.api.workflowservice.v1.IDescribeWorkflowExecutionResponse>
3654
) {
3755
const [testValue] = call.metadata.get('test');
3856
const [otherValue] = call.metadata.get('otherKey');
@@ -111,7 +129,7 @@ test('grpc retry passes request and headers on retry, propagates responses', asy
111129
temporal.api.workflowservice.v1.IDescribeWorkflowExecutionRequest,
112130
temporal.api.workflowservice.v1.IDescribeWorkflowExecutionResponse
113131
>,
114-
callback: grpc.sendUnaryData<temporal.api.workflowservice.v1.IRegisterNamespaceResponse>
132+
callback: grpc.sendUnaryData<temporal.api.workflowservice.v1.IDescribeWorkflowExecutionResponse>
115133
) {
116134
const { namespace } = call.request;
117135
if (typeof namespace === 'string') {
@@ -172,3 +190,89 @@ test('Default keepalive settings are set while maintaining user provided channel
172190
// User setting overrides default
173191
t.is(channelArgs['grpc.keepalive_permit_without_calls'], 0);
174192
});
193+
194+
test('Can configure TLS + call credentials', async (t) => {
195+
const meta = Array<string[]>();
196+
197+
const server = new grpc.Server();
198+
199+
server.addService(workflowServiceProtoDescriptor.temporal.api.workflowservice.v1.WorkflowService.service, {
200+
getSystemInfo(
201+
call: grpc.ServerUnaryCall<
202+
temporal.api.workflowservice.v1.IGetSystemInfoRequest,
203+
temporal.api.workflowservice.v1.IGetSystemInfoResponse
204+
>,
205+
callback: grpc.sendUnaryData<temporal.api.workflowservice.v1.IGetSystemInfoResponse>
206+
) {
207+
const [aValue] = call.metadata.get('a');
208+
const [authorizationValue] = call.metadata.get('authorization');
209+
if (typeof aValue === 'string' && typeof authorizationValue === 'string') {
210+
meta.push([aValue, authorizationValue]);
211+
}
212+
213+
const response: temporal.api.workflowservice.v1.IGetSystemInfoResponse = {
214+
serverVersion: 'test',
215+
capabilities: undefined,
216+
};
217+
callback(null, response);
218+
},
219+
220+
describeWorkflowExecution(
221+
call: grpc.ServerUnaryCall<
222+
temporal.api.workflowservice.v1.IDescribeWorkflowExecutionRequest,
223+
temporal.api.workflowservice.v1.IDescribeWorkflowExecutionResponse
224+
>,
225+
callback: grpc.sendUnaryData<temporal.api.workflowservice.v1.IDescribeWorkflowExecutionResponse>
226+
) {
227+
const [aValue] = call.metadata.get('a');
228+
const [authorizationValue] = call.metadata.get('authorization');
229+
if (typeof aValue === 'string' && typeof authorizationValue === 'string') {
230+
meta.push([aValue, authorizationValue]);
231+
}
232+
233+
const response: temporal.api.workflowservice.v1.IDescribeWorkflowExecutionResponse = {
234+
workflowExecutionInfo: { execution: { workflowId: 'test' } },
235+
};
236+
callback(null, response);
237+
},
238+
});
239+
const port = await bindLocalhostTls(server);
240+
server.start();
241+
242+
let callNumber = 0;
243+
const oauth2Client: grpc.OAuth2Client = {
244+
getRequestHeaders: async () => {
245+
const accessToken = `oauth2-access-token-${++callNumber}`;
246+
return { authorization: `Bearer ${accessToken}` };
247+
},
248+
};
249+
250+
// Default interceptor config with backoff factor of 1 to speed things up
251+
// const interceptor = makeGrpcRetryInterceptor(defaultGrpcRetryOptions({ factor: 1 }));
252+
const conn = await Connection.connect({
253+
address: `localhost:${port}`,
254+
metadata: { a: 'bc' },
255+
tls: {
256+
serverRootCACertificate: await fs.readFile(path.resolve(__dirname, `../tls_certs/test-ca.crt`)),
257+
clientCertPair: {
258+
crt: await fs.readFile(path.resolve(__dirname, `../tls_certs/test-client-chain.crt`)),
259+
key: await fs.readFile(path.resolve(__dirname, `../tls_certs/test-client.key`)),
260+
},
261+
serverNameOverride: 'Server',
262+
},
263+
callCredentials: [grpc.credentials.createFromGoogleCredential(oauth2Client)],
264+
});
265+
266+
// Make three calls
267+
await conn.workflowService.describeWorkflowExecution({ namespace: 'a' });
268+
await conn.workflowService.describeWorkflowExecution({ namespace: 'b' });
269+
await conn.workflowService.describeWorkflowExecution({ namespace: 'c' });
270+
271+
// Check that both connection level metadata and call credentials metadata are sent correctly
272+
t.deepEqual(meta, [
273+
['bc', 'Bearer oauth2-access-token-1'],
274+
['bc', 'Bearer oauth2-access-token-2'],
275+
['bc', 'Bearer oauth2-access-token-3'],
276+
['bc', 'Bearer oauth2-access-token-4'],
277+
]);
278+
});

packages/test/tls_certs/test-ca.crt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIFYzCCA0ugAwIBAgIUK/OctBa1W2oRcLVgf08rSIeRuE4wDQYJKoZIhvcNAQEL
3+
BQAwQTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMSUwIwYDVQQDDBwgVGVzdCBS
4+
b290IENBIC0gRE8gTk9UIFRSVVNUMB4XDTIzMTAxNjIxMDcwMVoXDTMzMTAxMzIx
5+
MDcwMVowQTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMSUwIwYDVQQDDBwgVGVz
6+
dCBSb290IENBIC0gRE8gTk9UIFRSVVNUMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
7+
MIICCgKCAgEAx6jqT2kK4dFoqj+4rWAdLO6h3rTfsh1PTYBiPZrZCvi3+DEoZRi8
8+
yy8XkJzH4wZ43EG0Q52CYVNKAM9auh9ahu5g00h4kOCL3hjVIsG9Pnw9k/ArXNcC
9+
WRC3R5Gv18cDq9U8mDxJk4P6d3Tx0iWEZ5/+6dEtSWlIWhHFj1zU+VoCB0FLvQrr
10+
tbPYzV8DCkXo62h78EssFQbz4Fqs5htpRN4EUESofQKq3VOlKzgq1NyJsxw9xILY
11+
sYeHo9PVMbPtGvPY118qQWGS3/eCAVmHknOYWVYjk1TRYNgE/EdoD82Psm1Xcf7+
12+
/2NQnGsQgGUvls68Q2kVzJbybxmlAF8u188DPy5qHrn3hAViDFKfbjOuW/GoF5uA
13+
ZHrCAlG+zrhAEdDo5gtCE2MFE2J+mk7R29VOqDOy7IElJwNkh8NXrIdsWwkLTPSo
14+
hXFjZkh2yxqmHJp2mEZoI7mrPEjpTkmIGKq/QQXh4e/GAKUmA/bYUGXeAWRQNH+P
15+
/mygHYRJXtN7ulx1vAb2WlV8fuy9X3cqW95B3pLNwpcD5nwVrLGcMWtZd5kKJifW
16+
ZzDlxEfGcubIGHCqyhw90nNJwsrRMGpTIl4c174MrYqy/t3uPwhmsg5tipedn5xn
17+
GqQ1TO2jpOuiBMv1x4ccsg5cV28ZGawWIpp+KpYY+HcgZ+W3SNm6zYUCAwEAAaNT
18+
MFEwHQYDVR0OBBYEFPlVgrLSBFw9+0jg8CZdAzLBNhSuMB8GA1UdIwQYMBaAFPlV
19+
grLSBFw9+0jg8CZdAzLBNhSuMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
20+
BQADggIBAGI7XXLC3qwHM01H6vlmmCkgeQMcHCeCaQSxB77039R1gBizX1SXMHmi
21+
I5ThmmOAGaXHgQkT6js2j4nUqAZV++bNWAJVmHnMIGKbLMH5VAOx0XczENllpAnb
22+
5jK/vE5Zz473DERY7Tj3YHE+JeD+XIBDz0ngcCHMJo3yig05kl2Oq29QvseUAwiQ
23+
mZ5Rt3LmnG2q/21JykELqB/eowFoKqAt/Y+KIISJH1gKtZNKUP2LUCZsR76AUcsD
24+
UqZ2FnpuD6c1zW3vOx3r3g3iAdDOy0NvVJbTsfPDMxL2Kxq+f7m0JiifJ3lmdDfd
25+
2tjP4O2ZwC8lgKS2T/HYaHZ8zL06DYaVceG5u1f0qit+ubTDYXp47bk5LEiGDhxo
26+
m7kKoxAuh2vzf2QRO70QfJKScMY5/mKPq5Pku4DkW2N7m9X1HabDeo82sGzNlj2E
27+
UxDs2p1gPuIy/USF/PC06TYDuUkbYQCS6YZSwnw2XZ7mOZgprsiOROSuhFuNTODM
28+
yQLdw2i/Z/PaSzeDjf2JNf0Rou/Qmfc8wbtQ8rKIm9J24a6Tg0odHTw49ipr4d5e
29+
jRNXZCHoVmQg9ar6vSf+ncz9Iw9iAhZqPV9RL5Jr7IdYVEqRjt96vsITwF5s2A+0
30+
d3pn2Qj1WDucVFguQqJM+5BIcMg4RjJEaDDRYDSMHGTW1e3ACt3O
31+
-----END CERTIFICATE-----
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIFMzCCAxugAwIBAgIUEIHOmHY61jeOj4iMtUsM2CgIcmcwDQYJKoZIhvcNAQEL
3+
BQAwQTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMSUwIwYDVQQDDBwgVGVzdCBS
4+
b290IENBIC0gRE8gTk9UIFRSVVNUMB4XDTIzMTAxNjIxMDk1MVoXDTMzMTAxMzIx
5+
MDk1MVowETEPMA0GA1UEAwwGQ2xpZW50MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
6+
MIICCgKCAgEAvxSSdvnEvhCoRs+rN82BE/bUi/5bPtQJJdo4Qb2CDosu0Yqn2JgP
7+
k6dnIYn7/PXqsH+xNANQuFP9CQl54k4WtKAhq6J1krhyTDo0D8F6+jz/CisThVYF
8+
bfU4Uos7qiRxIZ8Iqek3RPsWvfd4FLrys4rW5cQ3bzlN3soH0TxPifYv1M9kVl3m
9+
7IkJIGzsQQcbMmt8/Fu9gfAJyWCx1BSVKdcWGtoKddok7FWlMcENE1h130r8oNvP
10+
CnMJ1pYTRAx+oKdeTK+88gxCt4LlhMmr6UHHJ/1NrjsLtouYw/gImeikkefEW23g
11+
b6irKlJCYu3H+MntclbkJYEHp92KIP3BNQ5NI2d5Tig8YyJLsQ00VzTe+RK9dvsN
12+
4ocD4aXToSvFRJaS/a6J35rKt7+5gN+v4dX7G2s3d/39HkKhBeVegXecCl4oUIRl
13+
3l4xaIXKNCHLho5g+S7wxa+xW+uwI7Z1LnxxpN77XhEEISjQQ/B8qYdrE86tibr8
14+
TQ6j0lQ1kj3foRDc2nfi+rq0trgIry2GRKP0KJ41nLtS0S2EUPiaBjV8Pgm0N3Um
15+
mErrxsb1dRDNPMNpIq/sc9Xs0j5QqeZ5EoKYOwc21n+KpArZAq+wQ2BBsjzIA4Fb
16+
lmdCHF+mIE2TFmJ9K2wdpXaGN62LFGLO2eA2HDoUAJhKZWtSGz8W4cECAwEAAaNT
17+
MFEwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUwR2Ye/aZnGUGN93QEXRc6g+r
18+
/30wHwYDVR0jBBgwFoAU+VWCstIEXD37SODwJl0DMsE2FK4wDQYJKoZIhvcNAQEL
19+
BQADggIBAIaoNgVnmlxUY51sC2SxDrFN+k0P9dAF+ZAmtY+S6dsQP9YevLWb0l+i
20+
PtI66uIuZ+2YdGsyuNecudstNw4+pA2gGow8ZzuPE4VH9HfQZBFNv9dK7e1rWbol
21+
XpChLDvjFJtmuyPJeafeEs6u3xbjPVcQ7VPSGQbe0n7LhrFRFn+hRdyHVXBzcuUJ
22+
Z4/FQs52P0ontgR707Jc4+xNcEUgkCSmNbxenRGr6NPIA/C6fIOZ4qQiNK4qAtOe
23+
k9nQsc5Dda1XbrfYOaNY5wmme7jQFqJtvbS+JuAAjWuPfyw28mUJ7PwlQGo/lU15
24+
DzV+eUw7OEXBygpy9YXE6rDb96cbx8Ne7065gzq24Ucl5bo+tCnoONNv8Pvb/NHM
25+
Ewy2RYXSq5iTtxdiRE4J1PW1dxEAddBhvT6kf0Rr0rZqIdmtEVZOoLDu3c96wbB4
26+
9EzfqIQOGbnEgCvneRL3VMFeezVksMdRR6XTvsdZlhYcxjkmNdbQOh9pPebnn9cm
27+
5mJiGoRvv0Hmhwsiecs9fSVgWY0qZm/2m8bfxueAgbSVjy7Cd7gqiKB//s8Ws1bi
28+
mhWOd5CTifpjjqxX9SBFei+Z2BB04DD107Hkf4d/DRbKk29FNFjjrVzpsvItvwud
29+
1bhj31jtdKvcs7dNKOwRF38jE6dDM9x12SFTviCR/WLQs3+tomQB
30+
-----END CERTIFICATE-----
31+
-----BEGIN CERTIFICATE-----
32+
MIIFYzCCA0ugAwIBAgIUK/OctBa1W2oRcLVgf08rSIeRuE4wDQYJKoZIhvcNAQEL
33+
BQAwQTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMSUwIwYDVQQDDBwgVGVzdCBS
34+
b290IENBIC0gRE8gTk9UIFRSVVNUMB4XDTIzMTAxNjIxMDcwMVoXDTMzMTAxMzIx
35+
MDcwMVowQTELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAldBMSUwIwYDVQQDDBwgVGVz
36+
dCBSb290IENBIC0gRE8gTk9UIFRSVVNUMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
37+
MIICCgKCAgEAx6jqT2kK4dFoqj+4rWAdLO6h3rTfsh1PTYBiPZrZCvi3+DEoZRi8
38+
yy8XkJzH4wZ43EG0Q52CYVNKAM9auh9ahu5g00h4kOCL3hjVIsG9Pnw9k/ArXNcC
39+
WRC3R5Gv18cDq9U8mDxJk4P6d3Tx0iWEZ5/+6dEtSWlIWhHFj1zU+VoCB0FLvQrr
40+
tbPYzV8DCkXo62h78EssFQbz4Fqs5htpRN4EUESofQKq3VOlKzgq1NyJsxw9xILY
41+
sYeHo9PVMbPtGvPY118qQWGS3/eCAVmHknOYWVYjk1TRYNgE/EdoD82Psm1Xcf7+
42+
/2NQnGsQgGUvls68Q2kVzJbybxmlAF8u188DPy5qHrn3hAViDFKfbjOuW/GoF5uA
43+
ZHrCAlG+zrhAEdDo5gtCE2MFE2J+mk7R29VOqDOy7IElJwNkh8NXrIdsWwkLTPSo
44+
hXFjZkh2yxqmHJp2mEZoI7mrPEjpTkmIGKq/QQXh4e/GAKUmA/bYUGXeAWRQNH+P
45+
/mygHYRJXtN7ulx1vAb2WlV8fuy9X3cqW95B3pLNwpcD5nwVrLGcMWtZd5kKJifW
46+
ZzDlxEfGcubIGHCqyhw90nNJwsrRMGpTIl4c174MrYqy/t3uPwhmsg5tipedn5xn
47+
GqQ1TO2jpOuiBMv1x4ccsg5cV28ZGawWIpp+KpYY+HcgZ+W3SNm6zYUCAwEAAaNT
48+
MFEwHQYDVR0OBBYEFPlVgrLSBFw9+0jg8CZdAzLBNhSuMB8GA1UdIwQYMBaAFPlV
49+
grLSBFw9+0jg8CZdAzLBNhSuMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL
50+
BQADggIBAGI7XXLC3qwHM01H6vlmmCkgeQMcHCeCaQSxB77039R1gBizX1SXMHmi
51+
I5ThmmOAGaXHgQkT6js2j4nUqAZV++bNWAJVmHnMIGKbLMH5VAOx0XczENllpAnb
52+
5jK/vE5Zz473DERY7Tj3YHE+JeD+XIBDz0ngcCHMJo3yig05kl2Oq29QvseUAwiQ
53+
mZ5Rt3LmnG2q/21JykELqB/eowFoKqAt/Y+KIISJH1gKtZNKUP2LUCZsR76AUcsD
54+
UqZ2FnpuD6c1zW3vOx3r3g3iAdDOy0NvVJbTsfPDMxL2Kxq+f7m0JiifJ3lmdDfd
55+
2tjP4O2ZwC8lgKS2T/HYaHZ8zL06DYaVceG5u1f0qit+ubTDYXp47bk5LEiGDhxo
56+
m7kKoxAuh2vzf2QRO70QfJKScMY5/mKPq5Pku4DkW2N7m9X1HabDeo82sGzNlj2E
57+
UxDs2p1gPuIy/USF/PC06TYDuUkbYQCS6YZSwnw2XZ7mOZgprsiOROSuhFuNTODM
58+
yQLdw2i/Z/PaSzeDjf2JNf0Rou/Qmfc8wbtQ8rKIm9J24a6Tg0odHTw49ipr4d5e
59+
jRNXZCHoVmQg9ar6vSf+ncz9Iw9iAhZqPV9RL5Jr7IdYVEqRjt96vsITwF5s2A+0
60+
d3pn2Qj1WDucVFguQqJM+5BIcMg4RjJEaDDRYDSMHGTW1e3ACt3O
61+
-----END CERTIFICATE-----
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
-----BEGIN PRIVATE KEY-----
2+
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
51+
Zr1g+Q1GkJfMU7DDI5U9/RYdKJbu9A==
52+
-----END PRIVATE KEY-----

0 commit comments

Comments
 (0)