Skip to content

Commit

Permalink
TLS Certificate Pinning and Force TLS Checks (#295)
Browse files Browse the repository at this point in the history
* feat: add key pinning

* refactor: naming, placement

* refactor: error message

* refactor: make feature even safer

* fix: force override TLS_REJECT_UNAUTHORIZED

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: Julian König <[email protected]>
  • Loading branch information
3 people authored Oct 30, 2024
1 parent 81b16bf commit 47304af
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 4 deletions.
43 changes: 41 additions & 2 deletions src/ConnectorRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import fs from "fs";
import { HttpsProxyAgent } from "https-proxy-agent";
import { validate as validateSchema } from "jsonschema";
import path from "path";
import { checkServerIdentity, PeerCertificate } from "tls";
import { ConnectorMode } from "./ConnectorMode";
import { ConnectorRuntimeConfig } from "./ConnectorRuntimeConfig";
import { ConnectorRuntimeModule, ConnectorRuntimeModuleConfiguration } from "./ConnectorRuntimeModule";
Expand Down Expand Up @@ -76,11 +77,12 @@ export class ConnectorRuntime extends Runtime<ConnectorRuntimeConfig> {
throw new Error(errorMessage);
}

this.forceEnableMandatoryModules(connectorConfig);

const loggerFactory = new NodeLoggerFactory(connectorConfig.logging);
ConnectorLoggerFactory.init(loggerFactory);

this.setServerIdentityCheckFromKeyPinning(connectorConfig, loggerFactory.getLogger(ConnectorRuntime));
this.forceEnableMandatoryModules(connectorConfig);

const runtime = new ConnectorRuntime(connectorConfig, loggerFactory);
await runtime.init();

Expand All @@ -92,6 +94,43 @@ export class ConnectorRuntime extends Runtime<ConnectorRuntimeConfig> {
return runtime;
}

private static setServerIdentityCheckFromKeyPinning(connectorConfig: ConnectorRuntimeConfig, logger: ILogger) {
if (!connectorConfig.pinnedTLSCertificateSHA256Fingerprints) return;
const pinnedFingerprints = connectorConfig.pinnedTLSCertificateSHA256Fingerprints;

for (const host in pinnedFingerprints) {
if (!host.match(/^((([A-Za-z0-9]+(-[A-Za-z0-9]+)*)\.)+[a-z]{2,}|localhost)$/)) {
throw new Error(`Invalid host '${host}' in pinnedTLSCertificateSHA256Fingerprints. The host must not contain a protocol, path or query.`);
}

logger.info(`Certificate pinning is enforced for host '${host}' with fingerprint(s) '${pinnedFingerprints[host].join(", ")}'.`);
}

connectorConfig.transportLibrary.httpsAgentOptions = {
...connectorConfig.transportLibrary.httpsAgentOptions,
checkServerIdentity: (host: string, certificate: PeerCertificate) => {
const error = checkServerIdentity(host, certificate);
if (error) return error;

if (connectorConfig.enforceCertificatePinning && !(host in pinnedFingerprints)) {
return new Error(
`Certificate verification error: Certificate pinning is enforced, but no pinned certificate fingerprint is provided in the configuration for the requested host '${host}'.`
);
}

if (!(host in pinnedFingerprints)) return;
const pinnedFingerprintsForHost = pinnedFingerprints[host];

const fingerprint = certificate.fingerprint256.replaceAll(":", "").toLocaleLowerCase();
if (pinnedFingerprintsForHost.find((e) => e.replaceAll(":", "").toLocaleLowerCase() === fingerprint)) return;

return new Error(
`Certificate verification error: The SHA256 fingerprint of the received certificate '${fingerprint}' doesn't match a pinned certificate fingerprint for host '${host}'.`
);
}
};
}

private static forceEnableMandatoryModules(connectorConfig: ConnectorRuntimeConfig) {
connectorConfig.modules.decider.enabled = true;
connectorConfig.modules.request.enabled = true;
Expand Down
3 changes: 3 additions & 0 deletions src/ConnectorRuntimeConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,7 @@ export interface ConnectorRuntimeConfig extends RuntimeConfig {
infrastructure: {
httpServer: HttpServerConfiguration;
};

pinnedTLSCertificateSHA256Fingerprints?: Record<string, string[]>;
enforceCertificatePinning?: boolean;
}
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ function parseString(value: string) {

async function run() {
const config = createConnectorConfig();
if (!config.debug) process.env.TLS_REJECT_UNAUTHORIZED = "1";

const runtime = await ConnectorRuntime.create(config);
await runtime.start();
}
Expand Down
16 changes: 14 additions & 2 deletions src/jsonSchemas/connectorConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -1053,13 +1053,25 @@
},
"type": "object"
},
"pinnedTLSCertificateSHA256Fingerprints": {
"additionalProperties": {
"items": {
"type": "string"
},
"type": "array"
},
"type": "object"
},
"enforceCertificatePinning": {
"type": "boolean"
},
"transportLibrary": {
"additionalProperties": false,
"properties": {
"baseUrl": {
"addressGenerationHostnameOverride": {
"type": "string"
},
"addressGenerationHostnameOverride": {
"baseUrl": {
"type": "string"
},
"datawalletEnabled": {
Expand Down
3 changes: 3 additions & 0 deletions src/jsonSchemas/connectorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ export interface ConnectorConfig {
modules: Record<string, ModuleConfiguration>;
infrastructure: InfrastructureConfiguration;

pinnedTLSCertificateSHA256Fingerprints?: Record<string, string[]>;
enforceCertificatePinning?: boolean;

[key: string]: any;
}

Expand Down

0 comments on commit 47304af

Please sign in to comment.