Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Keep LF in paths this PR touches. CRLF in *.sh breaks Linux CI (`$'\r': command not found`).
*.sh text eol=lf
.github/workflows/*.yml text eol=lf
.github/workflows/*.yaml text eol=lf
.gitignore text eol=lf
.licenserc.yaml text eol=lf
.gitattributes text eol=lf
docs/**/*.md text eol=lf
tests/remote-e2e/mtls/** text eol=lf
*.crt -text
*.pem -text
35 changes: 35 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,41 @@ jobs:
npm i
npm run test tests/remote-e2e/static-failover/

TestRemoteE2EMTLS:
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
node-version: [ 20, 22, 24 ]
env:
SW_NODE_VERSION: ${{ matrix.node-version }}
SW_OAP_IMAGE: ghcr.io/apache/skywalking/oap@sha256:f3fd1982c0344db2150780ea71bd51afc1f8fd7f482f73f01429a72151c857f6
steps:
- uses: actions/checkout@v4
with:
submodules: true

- name: Generate mTLS test certificates
run: bash tests/remote-e2e/mtls/generate-certs.sh

- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-

- name: Set Up NodeJS ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Remote E2E On Node@${{ matrix.node-version }} (real OAP mTLS)
run: |
npm i
npm run test tests/remote-e2e/mtls/

TestLib:
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,9 @@ scripts/.finalize-work/
skywalking-nodejs-src-*.tgz
skywalking-nodejs-src-*.tgz.asc
skywalking-nodejs-src-*.tgz.sha512

# mTLS e2e test private keys (generated by generate-certs.sh, never commit)
tests/remote-e2e/mtls/**/*.pem
tests/remote-e2e/mtls/**/*.key
tests/remote-e2e/mtls/**/*.srl
tests/remote-e2e/mtls/**/*.csr
3 changes: 3 additions & 0 deletions .licenserc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,14 @@ header:
- 'LICENSE'
- 'NOTICE'
- '.gitignore'
- '.gitattributes'
- '.gitmodules'
- '.prettierrc'
- 'lib'
- '.husky'
- '.file-headerrc'
- '.eslintrc.js'
- '**/*.crt'
- '**/*.pem'

comment: on-failure
20 changes: 17 additions & 3 deletions docs/en/advanced/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,30 @@ An option passed to `agent.start()` replaces the environment value. This include
agent.start({ collectorAddress: '' });
```

For a TLS OAP endpoint, set `SW_AGENT_SECURE=true`. The agent uses the system trust store. It has no
configuration for a custom CA or client certificate. Set `SW_AGENT_AUTHENTICATION` if OAP requires
an agent token.
For a TLS OAP endpoint, set `SW_AGENT_SECURE=true`. The agent uses the system trust store by
default. Set `SW_AGENT_AUTHENTICATION` if OAP requires an agent token.

For a custom CA or mTLS, configure `SW_AGENT_SSL_TRUSTED_CA_PATH`. For mTLS, also configure both
`SW_AGENT_SSL_KEY_PATH` and `SW_AGENT_SSL_CERT_CHAIN_PATH`; all three paths are optional relative
to the Node.js process working directory. The client key and certificate must be a matching pair.
The agent fails channel creation when either client path is missing or unreadable, rather than
silently falling back to one-way TLS.

With Apache OAP, point the agent at the mTLS-enabled `receiver-sharing-server` gRPC listener
(commonly port `11801`, or the port selected by `SW_RECEIVER_GRPC_PORT`). Do not assume that the
regular OAP agent listener on port `11800` requests client certificates.

Under TLS with multiple hostnames, certificate verification follows the channel authority (the first
list entry in the configured target). Endpoint pick order may be shuffled by grpc-js, but the target
string — and therefore authority / SNI — stays in config order. Every backend must present a
certificate that shares the needed SANs, or failover handshakes fail. Prefer one DNS name with
multiple A/AAAA records for TLS high availability.

If the agent connects with a hostname or IP that does not appear in the OAP certificate SAN, the
handshake fails with a hostname mismatch. Set `SW_AGENT_SSL_TARGET_NAME_OVERRIDE` to the SAN name
that OAP does present — for example the DNS name when the agent connects via an IP literal.
This overrides both the TLS SNI and the gRPC default authority.

Channel disconnect lines are logged at `error` and recover lines at `warn` (throttled separately so a
recover line is not swallowed by the disconnect window). For per-address grpc-js detail, set
`GRPC_TRACE=pick_first,subchannel`.
Expand Down
13 changes: 13 additions & 0 deletions docs/en/contribution/build-and-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ npm run test tests/plugins/http/
Plugin tests start a mock SkyWalking collector and target services with Docker Compose. Make sure
Docker is running and that the test ports are free.

### Real OAP mTLS remote e2e

The suite under `tests/remote-e2e/mtls/` talks to a real OAP over mutual TLS. Private keys are not
in git — generate them first (needs `openssl` and `bash`):

```bash
bash tests/remote-e2e/mtls/generate-certs.sh
npm run test tests/remote-e2e/mtls/
```

See `tests/remote-e2e/mtls/README.md` for details. CI runs the same script before the Jest job.

Test the built package in a container:

```bash
Expand All @@ -81,6 +93,7 @@ docker run --rm skywalking-nodejs:test
| `src/trace/` | Trace context, spans, segments, and component IDs |
| `src/aws/`, `src/azure/` | Serverless wrappers and AWS helpers |
| `tests/plugins/` | Docker-based plugin integration tests |
| `tests/remote-e2e/` | Real-collector / real-OAP remote e2e (incl. mTLS) |
| `tests/remote/`, `tests/runtime/` | Reporter and runtime meter tests |

## Before opening a pull request
Expand Down
38 changes: 36 additions & 2 deletions docs/en/setup/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,48 @@ Node.js process. `SW_DISABLE` is checked when `start()` is called.
| `SW_AGENT_INSTANCE` | `serviceInstance` | Host name | Service instance name shown in SkyWalking. |
| `SW_AGENT_COLLECTOR_BACKEND_SERVICES` | `collectorAddress` | `127.0.0.1:11800` | OAP gRPC address(es). One `host:port` uses grpc-js `dns:` (all A/AAAA become endpoints; periodically re-resolved). A comma-separated list uses a static resolver with `pick_first` (literal endpoints only; no per-name DNS expansion or re-resolution). Endpoint pick order is shuffled by grpc-js (`shuffleAddressList`); the target string keeps config order so TLS authority / SNI stay on the first list entry. Under TLS, all backends must present certificates that share the needed SANs. Prefer one DNS name with multiple A/AAAA records for TLS high availability. |
| `SW_AGENT_SECURE` | `secure` | `false` | Use TLS for the OAP gRPC connection. |
| `SW_AGENT_SSL_TRUSTED_CA_PATH` | `sslTrustedCaPath` | Not set | Optional trusted CA certificate file for TLS server verification. Relative paths are resolved from the process working directory. |
| `SW_AGENT_SSL_KEY_PATH` | `sslKeyPath` | Not set | Client private key file for mTLS. Must be configured together with `sslCertChainPath` and `secure=true`. |
| `SW_AGENT_SSL_CERT_CHAIN_PATH` | `sslCertChainPath` | Not set | Client certificate chain file for mTLS. Must be configured together with `sslKeyPath` and `secure=true`. |
| `SW_AGENT_SSL_TARGET_NAME_OVERRIDE` | `sslTargetNameOverride` | Not set | Override the hostname used for TLS server name indication (SNI) and certificate verification. Useful when the OAP certificate SAN does not include the literal hostname or IP the agent connects to. |
| `SW_AGENT_AUTHENTICATION` | `authorization` | Not set | Authentication token sent to OAP. |
| `SW_AGENT_TRACE_TIMEOUT` | `traceTimeout` | `10000` | gRPC deadline in milliseconds for trace and meter reports and service management requests. Must be a positive integer. |

For token authentication, set the same token in OAP with `SW_AUTHENTICATION`. See
[OAP token authentication](https://skywalking.apache.org/docs/main/next/en/setup/backend/backend-token-auth/).

When `secure` is enabled, the agent uses the system trust store. It does not provide options for a
custom CA, client certificate, or mutual TLS.
When `secure` is enabled, the agent uses the system trust store unless `sslTrustedCaPath` is set.
Configure both `sslKeyPath` and `sslCertChainPath` to enable mTLS. The private key and certificate
files are read when the gRPC channel is created. Incomplete or unreadable certificate configuration
fails closed instead of silently downgrading to one-way TLS.

If the OAP server certificate SAN does not contain the hostname or IP that the agent connects to —
for example when the agent reaches OAP via an IP literal but the certificate is issued for a DNS
name — set `sslTargetNameOverride` to the SAN value that the certificate does include. This is
equivalent to grpc-js `grpc.ssl_target_name_override` and also updates `grpc.default_authority`.

Relative certificate paths are resolved from the process working directory. Prefer absolute paths
(`/etc/sw/ca.crt`) or ensure the working directory is stable (e.g. Docker containers with WORKDIR
set). The agent accepts both PKCS#1 (`-----BEGIN RSA PRIVATE KEY-----`) and PKCS#8
(`-----BEGIN PRIVATE KEY-----`) private key formats via Node's OpenSSL-backed `crypto` module.

For Apache OAP mTLS, enable the `receiver-sharing-server` gRPC listener with its trusted client CA
and point `collectorAddress` to that listener (commonly port `11801`, or the port selected by
`SW_RECEIVER_GRPC_PORT`). The standard OAP mTLS setup does not use the regular agent listener on
port `11800`.

For example:

```typescript
agent.start({
collectorAddress: 'oap.example.com:11801',
secure: true,
sslTrustedCaPath: './certs/ca.crt',
sslKeyPath: './certs/client.key',
sslCertChainPath: './certs/client.crt',
});
```


## Agent control and logging

Expand Down
71 changes: 64 additions & 7 deletions src/agent/core/remote/TLSChannelBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,76 @@
*
*/

import * as fs from 'fs';
import * as path from 'path';
import * as grpc from '@grpc/grpc-js';
import { ChannelOptions } from '@grpc/grpc-js';
import config from '../../../config/AgentConfig';
import { createLogger } from '../../../logging';
import ChannelBuilder, { ChannelBuildContext } from './ChannelBuilder';

/** When SW_AGENT_SECURE=true, upgrade channel credentials to TLS (Java TLSChannelBuilder simplified). */
const logger = createLogger(__filename);

function configuredPath(value: string | undefined): string | undefined {
const trimmed = value?.trim();
return trimmed ? path.resolve(trimmed) : undefined;
}

function readCertificateFile(filePath: string, description: string): Buffer {
try {
return fs.readFileSync(filePath);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to read ${description} at [${filePath}]: ${message}`);
}
}

/**
* Build TLS or mTLS credentials from the agent configuration.
* Client authentication is enabled only when both the private key and cert chain are configured.
*/
export default class TLSChannelBuilder implements ChannelBuilder {
build(context: ChannelBuildContext): ChannelBuildContext {
if (config.secure) {
return {
...context,
credentials: grpc.credentials.createSsl(),
};
const trustedCaPath = configuredPath(config.sslTrustedCaPath);
const keyPath = configuredPath(config.sslKeyPath);
const certChainPath = configuredPath(config.sslCertChainPath);
const hasTlsMaterial = trustedCaPath || keyPath || certChainPath;

if (!config.secure) {
if (hasTlsMaterial) {
throw new Error('TLS certificate configuration requires secure=true');
}
return context;
}
return context;

if (Boolean(keyPath) !== Boolean(certChainPath)) {
throw new Error('Both sslKeyPath and sslCertChainPath must be configured to enable mTLS');
}

const rootCerts = trustedCaPath ? readCertificateFile(trustedCaPath, 'the trusted CA certificate') : undefined;
const privateKey = keyPath ? readCertificateFile(keyPath, 'the client private key') : undefined;
const certChain = certChainPath ? readCertificateFile(certChainPath, 'the client certificate chain') : undefined;

const credentials = grpc.credentials.createSsl(rootCerts, privateKey, certChain);

logger.debug(
`gRPC TLS credentials built: ca=${trustedCaPath ?? '(system)'} key=${keyPath ?? '(none)'} cert=${
certChainPath ?? '(none)'
}`,
);
Comment on lines +72 to +76

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor logging issue (P3): these new calls use printf-style %s placeholders, but createLogger() intentionally omits winston.format.splat(), so the certificate paths and hostname override are not included in the messages.

For example, with SW_AGENT_LOGGING_LEVEL=debug and sslTargetNameOverride: 'example.test', I reproduced:

gRPC TLS credentials built: ca=%s key=%s cert=%s
gRPC TLS hostname override set to [%s]

The second message should say:

gRPC TLS hostname override set to [example.test]

Could these calls use template strings or structured metadata? For example, the hostname message can be:

logger.debug(`gRPC TLS hostname override set to [${sslTargetNameOverride}]`);

The credential-path message needs the same adjustment. This only affects troubleshooting output; TLS/mTLS behavior is unaffected.


const sslTargetNameOverride = config.sslTargetNameOverride?.trim();
const extraOptions: ChannelOptions = {};
if (sslTargetNameOverride) {
extraOptions['grpc.ssl_target_name_override'] = sslTargetNameOverride;
extraOptions['grpc.default_authority'] = sslTargetNameOverride;
logger.debug(`gRPC TLS hostname override set to [${sslTargetNameOverride}]`);
}

return {
...context,
credentials,
options: { ...context.options, ...extraOptions },
};
}
}
8 changes: 8 additions & 0 deletions src/config/AgentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ export type AgentConfig = {
serviceInstance?: string;
collectorAddress?: string;
secure?: boolean;
sslTrustedCaPath?: string;
sslKeyPath?: string;
sslCertChainPath?: string;
sslTargetNameOverride?: string;
authorization?: string;
maxBufferSize?: number;
coldEndpoint?: boolean;
Expand Down Expand Up @@ -182,6 +186,10 @@ const _config = {
})(),
collectorAddress: process.env.SW_AGENT_COLLECTOR_BACKEND_SERVICES || '127.0.0.1:11800',
secure: process.env.SW_AGENT_SECURE?.toLowerCase() === 'true',
sslTrustedCaPath: process.env.SW_AGENT_SSL_TRUSTED_CA_PATH || undefined,
sslKeyPath: process.env.SW_AGENT_SSL_KEY_PATH || undefined,
sslCertChainPath: process.env.SW_AGENT_SSL_CERT_CHAIN_PATH || undefined,
sslTargetNameOverride: process.env.SW_AGENT_SSL_TARGET_NAME_OVERRIDE || undefined,
authorization: process.env.SW_AGENT_AUTHENTICATION,
maxBufferSize: ((n) => (Number.isSafeInteger(n) && n > 0 ? n : 1000))(
Number.parseInt(process.env.SW_AGENT_MAX_BUFFER_SIZE ?? '', 10),
Expand Down
29 changes: 29 additions & 0 deletions tests/remote-e2e/mtls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Real OAP mTLS remote e2e

End-to-end coverage of Agent → OAP gRPC over mutual TLS against a real OAP +
BanyanDB stack (see `docker-compose.yml`).

## Prerequisites

- Docker
- Node.js (same majors as CI: 20 / 22 / 24)
- `openssl` and `bash` (Git Bash or WSL on Windows)

## Generate certificates

Private keys are not committed. Generate them before running the suite:

```bash
bash tests/remote-e2e/mtls/generate-certs.sh
```

This writes `server/server.pem`, `client/client.pem`, and refreshes the CA /
leaf certificates under `server/` and `client/`. Those `.pem` files are gitignored.

## Run

```bash
npm run test tests/remote-e2e/mtls/
```

CI runs the same `generate-certs.sh` step before the Jest suite.
29 changes: 29 additions & 0 deletions tests/remote-e2e/mtls/client/ca.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIE6DCCAtCgAwIBAgIUckh59cIFCT3jBextzlcwZ/bkrKswDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPc3ctbXRscy10ZXN0LWNhMB4XDTI2MDkxNTA2NDczM1oX
DTM2MDkxMjA2NTIzM1owGjEYMBYGA1UEAwwPc3ctbXRscy10ZXN0LWNhMIICIjAN
BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAwLZ4PzlE2nSSZO+SePU1Rll6gkcQ
QvDAOZR8o+3IQPGL8g4oZInJreTvwavVxpT1n+MlCZRPLrngpGg7+c33+FNhDmIm
sl19WgknMk82ISTKVEaMmEXq/bKko3VBsi8GIAq2rsTfaEV4wt0SoX+sMagg6FWw
wRiYEJgn01F7t1R9nc+/18VGpPGpk7CK+u6UfHCAHUFU0IUmPXHlM4ZO1GrO0F/3
E9oIyr1xWAJtbbQ9lDb2W9z+KRDJPUIZzuE2ffIYIv+hGWxU4guzoCYFCGoYW/Rp
C1A3rXTza6HDs0nBA2pnVNV0j25Z6JA8PEqRnunxc9Q6TYuz7SxBmUelnHN77iu5
ZT8Hg5fto1+kuAXXFr4YqpZlSTzDtlRW1Ts4hROk01ZpRpt4P9fhxUGJU9XwHWcj
fWv8U5yJZFqyWf/ubf12ybqbvBYutj5iVtxkz3KayHN88THwigZg7Epy+tGjE+EU
rvrC1o7My1hOb6ZwEUQBpeQ3b/w9Fq1AK4aAkrYp8dx/DYDYuefNseYKkb41UV2r
93WLUqdVxbPiZEeCEcnDgzmSI6BFyd78s3WsjD5CYj4TvV3XrfeIXXvWsPSqovY3
mHu1/Ye6y8vBal5TkN/+tYVOL/D9TQD/W/VjLD/HbppuKJk/IRNf93RgxhVEt+6g
NuPVyedFqD3ADgMCAwEAAaMmMCQwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8B
Af8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBAHQfCal9ZlCKu+/J9GKpJvovmAlu
LlRuGurXPH7pN3IPZo+AAxyzTbDIX9TU4IPOgfhL+k4OKBs0R5PmST7DBn/Q4Ile
4c/EPuY3iDS+RrTebgDQ+zPrkeJ30SAK5wG/ZDNPqaBNxHaIepZnh0qb+65H10e7
PuecuL8U0ZrpfhrXPodYgYPTM5tf8A7tEJKnx8AjKb4a0zoSq/0Wk9ScnNxi02Is
yvqwhTK7RZX/TlJlrIXTiDTFDT9RS4dmvSaWurrDM7qcUkFkqErnBX+3BREJ5m6g
pmGUdoRq+D9KWhnas5ATJB2sExzP/7J2JTA01/5KHBbg+k064DE/N70Cvh+10x0V
pSXJd4BgE7aRjB3BClGSvGLve+KKRSxU9I/6GHMmW2N9/t1IJau4QjqlnxggDqlu
r6VwCBVPuOUbR0K0pXdiILcFrGYg3OhYnjqti/IHPvSmmILzNIDknEsuTlw4lwlE
Oqhul9wh6EJH/JTCydMg/0twrn8CHTcNzX6VDpXb16F+d65HnOo5jeabAOjWVlCb
b42qOUsUqSckQs8Ag7aLnhhq7s99q19hZoC9BjS9dFEB2iPzq4wa/kO4gsVrLSBv
pAQMz3/DnLV/G81l2RnMotJi2s9S4t18uQTjsyPID5Uh0uT1rwGxDGhkCp7PhrsD
RXjsLdjVuANlkHYs
-----END CERTIFICATE-----
30 changes: 30 additions & 0 deletions tests/remote-e2e/mtls/client/client.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
-----BEGIN CERTIFICATE-----
MIIFGzCCAwOgAwIBAgIUZ8coAXTH7vRCPc1VeMw3MPvtGiQwDQYJKoZIhvcNAQEL
BQAwGjEYMBYGA1UEAwwPc3ctbXRscy10ZXN0LWNhMB4XDTI2MDkxNTA2NDczNVoX
DTM2MDkxMjA2NTIzNVowGDEWMBQGA1UEAwwNc3ctbm9kZS1hZ2VudDCCAiIwDQYJ
KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKbtLmGtHDBAO3oUvHn9P9zmR2W8tx24
1OKOXkJNwpYc2ud6IyspbQCml/vsX5jq183mhrC5ARt9kjt5J8ZrBOgtn3wgjzVv
pFQfjPKaKv5iR2AZzdvdBztzUZeUZmqVKWiAkVrapVyd4Z14LCLwXv89rz4AgSMR
BCm4V/8hhkO4ynSay46tql7/NB8O406Om7rTA07XwZh8Q8xav1eBW7/tMWXSuO1j
/Uu2nQ6hLcQTu/ENgpQsI4j2Vifm9taqgXRpP2KCwZODqBZiHSCRbPIfAklIeVM+
0fGAoY3UV/XiObD13PqZu5rneybv9IuK37haNFkzXbR9w7K0SWZNBzFymva18+eN
FXsZhsfQMTcX0hZkFVQC53Qw0nOfhhMnVmFeFmRP7QC+7pS39VNBD8jB05K8wrUg
qZ/ADexxqzJPc1e6EWmZqcvqJC2w4cIBiXhuQAwts54NR1twAzVnRE4j05C99ajg
rmFRRPLOHZ9XogQH+nbJJ2ZyWRXFZb4JC71S2nULXiIIRwa0aRUyrL/yG4n5p6Rd
QuNCBMXrgf/zjTkBMJcgFSNvsGVoHE6txSv6L4UfMBzoEmjvfQmZzz5YwH+oQ7N5
c1AEqQPmlAEqC8gQQpuL3sWNXCNLcZgJ5Qu0OUrciuAiAE1bGRWPrsgHhFdMHvMl
u/3xlNRGypW/AgMBAAGjWzBZMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgWg
MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAaBgNVHREEEzARgglsb2Nh
bGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBACsv8Gbbx81BfrWsi+lCzDYw
F4D1YtFCc9IZStMj3m/aAxH/HjY8Sm04YCn6q56B3B+kY7jPfGCYChqNYwqNjhKO
C/nKBFkhvNtP57+5btmOetWI0y36yTeuoc0ATHeW6aZihmDn6fsbVKwg6qJYB2Zm
2InLsdFHZffj/sLqqhKeThNF7tyt92uSh6y8/mAbTyDaz/YItr/V9rUHRFvLQ46r
jSJxe3VbUuTFmh5/o5eURNewYTGz1YnFyNs33J8riG8eWaxyYCh3yw1vk4tj1nxC
kJhylAym8Qbka71sBU32ptq26wFKsohbzHoiY9oPr092CJ2Yql5O6j7qtTAo9s+4
qvqqpcMQOC0jtA4DuYipy8JgW5/Hbx4WbQ/ybJL8EzEdJMwOrhjm3sFihXkT0o4q
B76mRs3KusnvXsJIyyYgmF6vlzgaTyKTWPXgr49VxCgsKEoZCdyTTCeOODfEorcR
MZb7TDL+NMHhxAJzQzD2MGrR7LfFAtN9ziq5bRub+WIbtUjEPr84Djr0Ah2HDYaA
uQkX8xWCx2xICfUbsXiFl3jUXVPqTvDFRXD2Tieiz4jYeg1w5TMapsevlFI7/9ci
y32gvZW4oTOvwSZ/v/mhZSWA/iGuJXEUVeaNM4eC5zwPCnsFJcPiAeJV5H1K9pG4
L7k3VHYUnfo1SHh9kx7V
-----END CERTIFICATE-----
Loading
Loading